Branch data Line data Source code
1 : : #include "TabManager.h"
2 : :
3 : : #include <QLoggingCategory>
4 : : #include <QMouseEvent>
5 : : #include <QStackedWidget>
6 : : #include <QTabBar>
7 : : #include <QVariantMap>
8 : :
9 : : #include "ui/MdiIconProvider.h"
10 : : #include "ui/ThemeManager.h"
11 : :
12 : : #include <QTimer>
13 : :
14 [ + + + - : 55 : Q_LOGGING_CATEGORY(lcTabs, "mailjd.tabs")
+ - - - ]
15 : :
16 : 86 : TabManager::TabManager(QTabBar *tabBar, QStackedWidget *stack, QObject *parent)
17 : 86 : : QObject(parent), m_tabBar(tabBar), m_stack(stack) {
18 : : // Add the main view tab (index 0, not closable)
19 : 86 : TabInfo mainTab;
20 : 86 : mainTab.type = TabInfo::MainView;
21 : 86 : mainTab.title = QStringLiteral("\U0001F4EC MailJD");
22 [ + - ]: 86 : mainTab.widget = m_stack->widget(0); // Already added by MainWindow
23 [ + - ]: 86 : m_tabs.append(mainTab);
24 : :
25 [ + - ]: 86 : m_tabBar->addTab(mainTab.title);
26 [ + - ]: 86 : m_tabBar->setTabsClosable(true);
27 : :
28 : : // MainView tab: remove close button
29 [ + - ]: 86 : m_tabBar->setTabButton(0, QTabBar::RightSide, nullptr);
30 [ + - ]: 86 : m_tabBar->setTabButton(0, QTabBar::LeftSide, nullptr);
31 : :
32 [ + - ]: 86 : updateTabBarVisibility();
33 : :
34 : : // T-215: Middle-click to close tabs
35 [ + - ]: 86 : m_tabBar->installEventFilter(this);
36 : :
37 : : // Wire tab bar signals
38 [ + - ]: 86 : connect(m_tabBar, &QTabBar::currentChanged, this, [this](int index) {
39 [ + - + - : 55 : if (index >= 0 && index < m_tabs.size()) {
+ - ]
40 : : // T-542: Track MRU tab history
41 [ + + + + : 55 : if (m_tabHistory.isEmpty() || m_tabHistory.last() != index)
+ + ]
42 : 53 : m_tabHistory.append(index);
43 : 55 : m_stack->setCurrentIndex(index);
44 : 55 : emit currentTabChanged(index, m_tabs[index].type);
45 : : }
46 : 55 : });
47 : :
48 [ + - ]: 86 : connect(m_tabBar, &QTabBar::tabCloseRequested, this, [this](int index) {
49 : 0 : closeTab(index);
50 : 0 : });
51 : 86 : }
52 : :
53 : 37 : int TabManager::openMailTab(qint64 uid, qint64 folderId,
54 : : const QString &subject,
55 : : const QString &messageId) {
56 : : // Duplicate check: if tab for this UID exists, switch to it
57 : 37 : int existing = findTabByUid(uid);
58 [ + + ]: 37 : if (existing >= 0) {
59 [ + - ]: 2 : switchToTab(existing);
60 [ + - + - : 4 : qCInfo(lcTabs) << "Switched to existing tab for UID" << uid;
+ - + - +
+ ]
61 : 2 : return existing;
62 : : }
63 : :
64 : : // Truncate title
65 : 35 : QString title = subject;
66 [ - + ]: 35 : if (title.isEmpty())
67 : 0 : title = QStringLiteral("(Kein Betreff)");
68 [ + + ]: 35 : if (title.length() > kMaxTitleLength)
69 [ + - + - ]: 1 : title = title.left(kMaxTitleLength - 1) + QStringLiteral("\u2026");
70 : :
71 [ + - + - : 70 : qCInfo(lcTabs) << "Opening new tab for UID" << uid << ":" << title;
+ - + - +
- + - +
+ ]
72 : :
73 : : // The actual widget will be set by MainWindow after this returns
74 : : // (it needs to create a MailTabWidget and load the body)
75 : 35 : TabInfo info;
76 : 35 : info.type = TabInfo::MailTab;
77 : 35 : info.title = title;
78 : 35 : info.mailUid = uid;
79 : 35 : info.folderId = folderId;
80 : 35 : info.messageId = messageId;
81 : 35 : info.widget = nullptr; // Will be set by caller
82 : :
83 [ + - ]: 35 : m_tabs.append(info);
84 [ + - ]: 35 : int tabIndex = m_tabBar->addTab(title);
85 : :
86 [ + - ]: 35 : updateTabBarVisibility();
87 [ + - ]: 35 : emit tabCountChanged(m_tabs.size());
88 : :
89 : 35 : return tabIndex;
90 : 35 : }
91 : :
92 : 17 : int TabManager::openCalendarTab(QWidget *widget) {
93 : 17 : int existing = findTabByType(TabInfo::CalendarTab);
94 [ + + ]: 17 : if (existing >= 0) {
95 [ + - ]: 8 : switchToTab(existing);
96 : 8 : return existing;
97 : : }
98 : 9 : TabInfo info;
99 : 9 : info.type = TabInfo::CalendarTab;
100 : 9 : info.title = QStringLiteral("Kalender");
101 : 9 : info.widget = widget;
102 [ + - ]: 9 : m_tabs.append(info);
103 [ + - ]: 9 : int idx = m_tabBar->addTab(info.title);
104 [ + - ]: 9 : m_tabBar->setTabIcon(
105 [ + - + - ]: 27 : idx, MdiIconProvider::instance().icon(
106 [ + - + - : 27 : QStringLiteral("calendar-month"), 16, QColor(ThemeManager::instance().color("@text_secondary"))));
+ - ]
107 [ + - ]: 9 : m_stack->addWidget(widget);
108 [ + - ]: 9 : switchToTab(idx);
109 [ + - ]: 9 : updateTabBarVisibility();
110 [ + - ]: 9 : emit tabCountChanged(m_tabs.size());
111 : 9 : return idx;
112 : 9 : }
113 : :
114 : 9 : int TabManager::openTaskTab(QWidget *widget) {
115 : 9 : int existing = findTabByType(TabInfo::TaskTab);
116 [ + + ]: 9 : if (existing >= 0) {
117 [ + - ]: 1 : switchToTab(existing);
118 : 1 : return existing;
119 : : }
120 : 8 : TabInfo info;
121 : 8 : info.type = TabInfo::TaskTab;
122 : 8 : info.title = QStringLiteral("Aufgaben");
123 : 8 : info.widget = widget;
124 [ + - ]: 8 : m_tabs.append(info);
125 [ + - ]: 8 : int idx = m_tabBar->addTab(info.title);
126 [ + - ]: 8 : m_tabBar->setTabIcon(
127 [ + - + - ]: 24 : idx, MdiIconProvider::instance().icon(
128 [ + - + - : 24 : QStringLiteral("checkbox-marked-outline"), 16, QColor(ThemeManager::instance().color("@text_secondary"))));
+ - ]
129 [ + - ]: 8 : m_stack->addWidget(widget);
130 [ + - ]: 8 : switchToTab(idx);
131 [ + - ]: 8 : updateTabBarVisibility();
132 [ + - ]: 8 : emit tabCountChanged(m_tabs.size());
133 : 8 : return idx;
134 : 8 : }
135 : :
136 : 15 : void TabManager::closeTab(int index) {
137 [ + + - + : 15 : if (index <= 0 || index >= m_tabs.size()) {
+ + ]
138 : : // Cannot close main view (index 0) or invalid index
139 : 4 : return;
140 : : }
141 : :
142 [ + - + - : 22 : qCInfo(lcTabs) << "Closing tab" << index << ":" << m_tabs[index].title;
+ - + - +
- + - + -
+ + ]
143 : :
144 [ + - ]: 11 : QWidget *widget = m_tabs[index].widget;
145 [ + - ]: 11 : m_tabs.removeAt(index);
146 : :
147 : : // T-542: Find MRU target before removing the tab from the bar.
148 : : // Walk history backwards, skip the closed index, adjust for shift.
149 : 11 : int mruTarget = -1;
150 [ + + ]: 18 : for (int i = m_tabHistory.size() - 1; i >= 0; --i) {
151 [ + - ]: 11 : int h = m_tabHistory[i];
152 [ + + ]: 11 : if (h == index) continue; // skip the tab being closed
153 [ - + ]: 4 : mruTarget = (h > index) ? h - 1 : h;
154 [ + - + - : 4 : if (mruTarget >= 0 && mruTarget < m_tabs.size()) break;
+ - ]
155 : 0 : mruTarget = -1;
156 : : }
157 : : // Purge closed index and adjust remaining entries
158 : 11 : QList<int> newHistory;
159 [ + - + - : 32 : for (int h : m_tabHistory) {
+ + ]
160 [ + + ]: 21 : if (h == index) continue;
161 [ - + + - ]: 10 : newHistory.append(h > index ? h - 1 : h);
162 : : }
163 : 11 : m_tabHistory = newHistory;
164 : :
165 : : // Block currentChanged during removal to prevent stale widget display
166 : : {
167 : 11 : QSignalBlocker blocker(m_tabBar);
168 [ + - ]: 11 : m_tabBar->removeTab(index);
169 : 11 : }
170 [ + - ]: 11 : m_stack->removeWidget(widget);
171 [ + + ]: 11 : if (widget)
172 [ + - ]: 6 : widget->deleteLater();
173 : :
174 : : // T-542: Switch to MRU target, or fall back to QTabBar's choice
175 [ + + + - ]: 11 : int newCurrent = (mruTarget >= 0) ? mruTarget : m_tabBar->currentIndex();
176 [ + - + - : 11 : if (newCurrent >= 0 && newCurrent < m_tabs.size()) {
+ - ]
177 [ + - ]: 11 : m_tabBar->setCurrentIndex(newCurrent);
178 [ + - ]: 11 : m_stack->setCurrentIndex(newCurrent);
179 : : }
180 : :
181 [ + - ]: 11 : updateTabBarVisibility();
182 [ + - ]: 11 : emit tabCountChanged(m_tabs.size());
183 [ + - + - : 11 : if (newCurrent >= 0 && newCurrent < m_tabs.size())
+ - ]
184 [ + - + - ]: 11 : emit currentTabChanged(newCurrent, m_tabs[newCurrent].type);
185 : 11 : }
186 : :
187 : 10 : void TabManager::closeCurrentTab() {
188 : 10 : closeTab(m_tabBar->currentIndex());
189 : 10 : }
190 : :
191 : 70 : void TabManager::switchToTab(int index) {
192 [ + - + + : 70 : if (index >= 0 && index < m_tabs.size()) {
+ + ]
193 : 69 : m_tabBar->setCurrentIndex(index);
194 : : }
195 : 70 : }
196 : :
197 : 8 : void TabManager::switchToMainView() { switchToTab(0); }
198 : :
199 : 8 : void TabManager::switchToNextTab() {
200 [ + + ]: 8 : if (m_tabs.size() <= 1) return;
201 : 6 : int next = (m_tabBar->currentIndex() + 1) % m_tabs.size();
202 : 6 : switchToTab(next);
203 : : }
204 : :
205 : 5 : void TabManager::switchToPreviousTab() {
206 [ + + ]: 5 : if (m_tabs.size() <= 1) return;
207 : 3 : int prev = (m_tabBar->currentIndex() - 1 + m_tabs.size()) % m_tabs.size();
208 : 3 : switchToTab(prev);
209 : : }
210 : :
211 : 30 : int TabManager::tabCount() const { return m_tabs.size(); }
212 : :
213 : 14 : int TabManager::currentIndex() const { return m_tabBar->currentIndex(); }
214 : :
215 : 3 : TabInfo TabManager::currentTabInfo() const {
216 : 3 : int idx = m_tabBar->currentIndex();
217 [ + - + - : 3 : if (idx >= 0 && idx < m_tabs.size())
+ - ]
218 : 3 : return m_tabs[idx];
219 : 0 : return {};
220 : : }
221 : :
222 : 467 : bool TabManager::isMainView() const {
223 : 467 : return m_tabBar->currentIndex() == 0;
224 : : }
225 : :
226 : 57 : int TabManager::findTabByUid(qint64 uid) const {
227 [ + + ]: 125 : for (int i = 0; i < m_tabs.size(); ++i) {
228 [ + + + + : 75 : if (m_tabs[i].type == TabInfo::MailTab && m_tabs[i].mailUid == uid)
+ + ]
229 : 7 : return i;
230 : : }
231 : 50 : return -1;
232 : : }
233 : :
234 : 28 : int TabManager::findTabByType(TabInfo::Type type) const {
235 [ + + ]: 70 : for (int i = 0; i < m_tabs.size(); ++i) {
236 [ + + ]: 53 : if (m_tabs[i].type == type)
237 : 11 : return i;
238 : : }
239 : 17 : return -1;
240 : : }
241 : :
242 : 2 : void TabManager::updateTabFolder(int index, qint64 newFolderId) {
243 [ + - + - : 2 : if (index > 0 && index < m_tabs.size()) {
+ - ]
244 : 2 : m_tabs[index].folderId = newFolderId;
245 : : }
246 : 2 : }
247 : :
248 : 0 : void TabManager::updateTabUid(int index, qint64 newUid) {
249 [ # # # # : 0 : if (index > 0 && index < m_tabs.size()) {
# # ]
250 : 0 : m_tabs[index].mailUid = newUid;
251 : : }
252 : 0 : }
253 : :
254 : 6 : void TabManager::setTabWidget(int index, QWidget *widget) {
255 [ + - + - : 6 : if (index >= 0 && index < m_tabs.size()) {
+ - ]
256 : 6 : m_tabs[index].widget = widget;
257 : : }
258 : 6 : }
259 : :
260 : 32 : QVariantList TabManager::saveState() const {
261 : 32 : QVariantList result;
262 : : // T-430: Save active tab index as metadata entry
263 : 32 : QVariantMap meta;
264 [ + - + - ]: 64 : meta[QStringLiteral("_activeIndex")] = m_tabBar->currentIndex();
265 [ + - ]: 32 : result.append(meta);
266 : : // Skip index 0 (MainView — always present)
267 [ + + ]: 73 : for (int i = 1; i < m_tabs.size(); ++i) {
268 : 41 : const auto &tab = m_tabs[i];
269 : 41 : QVariantMap map;
270 [ + - ]: 82 : map[QStringLiteral("type")] =
271 [ + + - - ]: 150 : tab.type == TabInfo::MailTab ? QStringLiteral("mail")
272 [ + + + + : 55 : : tab.type == TabInfo::CalendarTab ? QStringLiteral("calendar")
- - ]
273 [ + - + + : 45 : : tab.type == TabInfo::TaskTab ? QStringLiteral("task")
- - ]
274 [ - + + + : 164 : : QStringLiteral("unknown");
- - ]
275 [ + - ]: 82 : map[QStringLiteral("uid")] = tab.mailUid;
276 [ + - ]: 82 : map[QStringLiteral("folderId")] = tab.folderId;
277 [ + - ]: 82 : map[QStringLiteral("title")] = tab.title;
278 [ + - ]: 82 : map[QStringLiteral("messageId")] = tab.messageId;
279 [ + - ]: 41 : result.append(map);
280 : 41 : }
281 : 32 : return result;
282 : 32 : }
283 : :
284 : 5 : void TabManager::restoreState(const QVariantList &state) {
285 : 5 : int savedActiveIndex = -1;
286 [ + + ]: 17 : for (const auto &entry : state) {
287 [ + - ]: 12 : auto map = entry.toMap();
288 : : // T-430: Parse active tab index metadata
289 [ + - + + ]: 12 : if (map.contains(QStringLiteral("_activeIndex"))) {
290 [ + - + - ]: 5 : savedActiveIndex = map[QStringLiteral("_activeIndex")].toInt();
291 : 5 : continue;
292 : : }
293 [ + - + - ]: 14 : QString type = map.value(QStringLiteral("type")).toString();
294 : :
295 [ - + ]: 7 : if (type == QStringLiteral("calendar")) {
296 [ # # # # : 0 : qCInfo(lcTabs) << "Restoring calendar tab";
# # # # ]
297 [ # # ]: 0 : emit calendarTabRequested();
298 [ - + ]: 7 : } else if (type == QStringLiteral("task")) {
299 [ # # # # : 0 : qCInfo(lcTabs) << "Restoring task tab";
# # # # ]
300 [ # # ]: 0 : emit taskTabRequested();
301 [ + - ]: 7 : } else if (type == QStringLiteral("mail")) {
302 [ + - + - ]: 14 : qint64 uid = map.value(QStringLiteral("uid")).toLongLong();
303 [ + - + - ]: 14 : qint64 folderId = map.value(QStringLiteral("folderId")).toLongLong();
304 [ + - + - ]: 14 : QString title = map.value(QStringLiteral("title")).toString();
305 [ + - + - ]: 14 : QString messageId = map.value(QStringLiteral("messageId")).toString();
306 : :
307 [ - + ]: 7 : if (uid <= 0)
308 : 0 : continue;
309 : :
310 [ + - + - : 14 : qCInfo(lcTabs) << "Restoring tab for UID" << uid << ":" << title;
+ - + - +
- + - +
+ ]
311 : :
312 : 7 : TabInfo info;
313 : 7 : info.type = TabInfo::MailTab;
314 : 7 : info.title = title;
315 : 7 : info.mailUid = uid;
316 : 7 : info.folderId = folderId;
317 : 7 : info.messageId = messageId;
318 : 7 : info.widget = nullptr;
319 : :
320 [ + - ]: 7 : m_tabs.append(info);
321 [ + - ]: 7 : m_tabBar->addTab(title);
322 : :
323 [ + - ]: 7 : emit mailTabRequested(uid, folderId, messageId);
324 [ + - + - ]: 7 : }
325 [ + - + + ]: 12 : }
326 : :
327 : 5 : updateTabBarVisibility();
328 [ + + ]: 5 : if (m_tabs.size() > 1) {
329 : 4 : emit tabCountChanged(m_tabs.size());
330 : : }
331 : :
332 : : // T-430: Restore active tab (deferred to allow widgets to initialize)
333 [ + - + - : 5 : if (savedActiveIndex >= 0 && savedActiveIndex < m_tabs.size()) {
+ - ]
334 [ + - ]: 5 : QTimer::singleShot(0, this, [this, savedActiveIndex]() {
335 : 3 : switchToTab(savedActiveIndex);
336 : 3 : });
337 : : }
338 : 5 : }
339 : :
340 : 154 : void TabManager::updateTabBarVisibility() {
341 : 154 : m_tabBar->setVisible(m_tabs.size() > 1);
342 : 154 : }
343 : :
344 : 1329 : bool TabManager::eventFilter(QObject *obj, QEvent *event) {
345 [ + - + + : 1329 : if (obj == m_tabBar && event->type() == QEvent::MouseButtonRelease) {
+ + ]
346 : 2 : auto *me = static_cast<QMouseEvent *>(event);
347 [ + - ]: 2 : if (me->button() == Qt::MiddleButton) {
348 [ + - + - ]: 2 : int idx = m_tabBar->tabAt(me->pos());
349 [ + + ]: 2 : if (idx > 0) closeTab(idx);
350 : 2 : return true;
351 : : }
352 : : }
353 : 1327 : return QObject::eventFilter(obj, event);
354 : : }
|