Line data Source code
1 : #pragma once
2 :
3 : #include <QMainWindow>
4 : #include <QSettings>
5 : #include <functional>
6 :
7 : class QAction;
8 : class QDialog;
9 :
10 : #include "data/CalendarModels.h"
11 : #include "data/Models.h"
12 :
13 : class QSplitter;
14 : class QStackedWidget;
15 : class QTabBar;
16 : class QTreeView;
17 : class QLabel;
18 : class QMenu;
19 : class QSystemTrayIcon;
20 : class QThread;
21 :
22 : class CalDavClient;
23 : class CommandBar;
24 : class ComposeWindow;
25 : class ContactStore;
26 : class CardDavClient;
27 : class ConnectionHealthMonitor;
28 : class DesktopNotifier;
29 : class NotificationBatcher;
30 : class FolderPredictor;
31 : class FolderTree;
32 : class ImapService;
33 : class MailTabWidget;
34 : class TabManager;
35 : #ifdef MAILJD_KDE_INTEGRATION
36 : class KStatusNotifierItem;
37 : #endif
38 : #include "data/MailCache.h" // T-180: needed for MailCache::SearchResult
39 : class MailController;
40 : class UndoManager;
41 : class MailFilterProxyModel;
42 : class MailListModel;
43 : class MailThreadModel;
44 : class FolderOperationsController;
45 : class MailView;
46 : class SearchCoordinator;
47 : class SearchPanel;
48 : class SettingsSyncService;
49 : class ShortcutHelpOverlay;
50 : struct AccountConfig;
51 : #include "data/AccountConfig.h" // T-271: For ImapConfig in reconnect
52 :
53 : // MainWindow implements the 3-pane mail client layout:
54 : // Left: FolderTree (account folders)
55 : // Top-right: MailList (message headers)
56 : // Bottom-right: MailView (selected message content)
57 : class MainWindow : public QMainWindow {
58 1336 : Q_OBJECT
59 : #ifdef MAILJD_UNIT_TEST
60 : friend class TestMainWindow;
61 : friend class TestSprint55Ui;
62 : friend class TestSprint49Desktop;
63 : friend class TestSprint59MainWindow;
64 : friend class TestSearchCoordinator;
65 : friend class TestFolderOperations;
66 : friend class TestSprint68MainWindow;
67 : friend class TestSprint76;
68 : friend class TestSprint79MainWindow; // Sprint 79 E5/E6: reply + notification
69 : #endif
70 : #ifdef MAILJD_E2E_TESTING
71 : friend class TestE2EInteractive;
72 : #endif
73 :
74 : public:
75 : explicit MainWindow(QWidget *parent = nullptr);
76 : ~MainWindow() override;
77 :
78 : // Sprint 49: public API for D-Bus service
79 : void openComposeNew();
80 : bool openMailtoUrl(const QString &url);
81 : void triggerPollNow();
82 :
83 : // Sprint 76 (T-76.A1): central foreground-activation helper used by the
84 : // notification "Open" action, the D-Bus entry points (Activate/Compose/
85 : // ComposeMailto) and the tray click handlers. Restores from minimized,
86 : // raises the window and requests compositor focus. With
87 : // MAILJD_HAVE_KWINDOWSYSTEM this uses KWindowSystem::activateWindow
88 : // (Wayland xdg-activation-v1 / X11 _NET_ACTIVE_WINDOW); without it the
89 : // Qt-only raise()+activateWindow() fallback remains.
90 : void bringToFront();
91 :
92 : protected:
93 : void closeEvent(QCloseEvent *event) override;
94 : bool eventFilter(QObject *obj, QEvent *event) override; // T-181
95 :
96 : private:
97 : // T-304: Runtime language switching
98 : void retranslateUi();
99 :
100 : protected:
101 : void changeEvent(QEvent *event) override;
102 :
103 : void setupUi();
104 : void setupMenuBar();
105 : void setupStatusBar();
106 : void connectSignals();
107 : void loadAccounts();
108 : void reloadAccounts();
109 : void restoreLayout();
110 : void saveLayout();
111 : void refreshTreeWithBadges(); // Bug 4: restores badges after tree rebuild
112 : void setStatus(const QString &message);
113 : void setStatus(const QString &key, const QString &message, int timeoutMs = 0);
114 : void clearStatus(const QString &key);
115 :
116 : void showSettings();
117 : void showSetupWizard();
118 : void showSubscriptionDialog();
119 : void showContactManager(); // T-163
120 : // Fix B: DRY helper for wiring ContactStore + recipientsSent on ComposeWindow
121 : void configureComposeWindow(ComposeWindow *compose) const;
122 : void setupComposeTracking(ComposeWindow *compose);
123 : QString detectSpecialFolder(const QString &kind) const; // T-178
124 : void deleteOldDraft(qint64 draftUid); // T-177: STORE \Deleted + EXPUNGE
125 : void openDraftInCompose(qint64 uid, const MailHeader &header); // T-177
126 :
127 : // T-290 folder management lives in FolderOperationsController (Sprint 65
128 : // P2.2).
129 : static QString messageIdHeaderValue(const QString &messageId);
130 : static QStringList replyReferencesForHeader(const MailHeader &header);
131 : static QString attachmentSaveDialogPath(const QString &filename);
132 : // Search-mode state and logic live in SearchCoordinator (Sprint 65 P2.1).
133 :
134 : // T-092: Reply / Forward compose helpers
135 : void openReply(qint64 uid, bool replyAll);
136 : void openForward(qint64 uid);
137 :
138 : // T-099: Thread view toggle
139 : void toggleThreadView(bool threaded);
140 :
141 : // T-142: CommandBar command dispatcher
142 : void executeCommand(const QString &cmd);
143 :
144 : // T-151: Toggle normal-mode shortcuts on/off
145 : void setNormalMode(bool enabled);
146 :
147 : // T-147: Detect special folders (Trash, Archive) from folder flags
148 : void detectSpecialFolders();
149 :
150 : // T-145: Vim navigation helpers
151 : void moveMailSelection(int delta);
152 : void moveMailSelectionPage(int delta);
153 : void moveMailSelectionToEnd(bool top);
154 :
155 : // T-148: Show shortcut help overlay
156 : void showShortcutHelp();
157 :
158 : // T-147: Select next mail after a move/delete
159 : void selectNextAfterMove();
160 : void jumpToNextUnread(); // Space: jump to next unread in folder or switch folder
161 : void jumpToNextUnreadFolder(); // Helper: switch to next folder with unread mails
162 :
163 : // T-168: Update folder suggestion label for current mail
164 : void updateSuggestion();
165 :
166 : // T-169: Quick-move to suggested folder
167 : void quickMoveToSuggestion();
168 :
169 : // T-232: Viewport-based suggestion computation
170 : void computeVisibleSuggestions();
171 : QList<MailHeader> getVisibleHeaders() const;
172 :
173 : // T-407: Resolved mail identity for search-mode-safe actions
174 : struct MailId {
175 : qint64 uid = -1;
176 : qint64 folderId = -1;
177 : QString folderPath;
178 26 : bool isValid() const { return uid >= 0; }
179 20 : bool hasFolderId() const { return folderId > 0; }
180 : };
181 : MailId mailIdFromViewIndex(const QModelIndex &viewIdx) const;
182 : MailId currentMailId() const;
183 : QList<MailId> getSelectedMailIds() const;
184 : bool isSearchMode() const;
185 :
186 : // T-79.E1/L32: takes MailIds so the tab cache is copied from the mail's
187 : // own source folder (search-mode moves span folders).
188 : void copyTabCacheToFolder(const QList<MailId> &mails,
189 : const QString &targetFolder);
190 :
191 : // T-170: Train predictor after a move action
192 : void trainAfterMove(const QList<MailId> &mails, const QString &targetFolder);
193 :
194 : // T-213: Open a mail in a separate tab
195 : void openMailInTab(qint64 uid);
196 :
197 : // Get UIDs of selected mails (falls back to currentIndex if no selection)
198 : QList<qint64> getSelectedUids() const;
199 :
200 : // Session state restore helpers (called after IMAP data arrives)
201 : void restoreSessionFolder();
202 : void restoreSessionMail();
203 :
204 : // Resolves a QTreeView index to a UID,
205 : // correctly handling both flat and thread view.
206 : qint64 uidFromViewIndex(const QModelIndex &viewIdx) const;
207 :
208 : // (Re-)connects the selection model handler for mail selection.
209 : // Disconnects any previous handler to prevent accumulation.
210 : void reconnectSelectionHandler();
211 :
212 : // T-124: System tray
213 : void setupTray();
214 : void updateTrayIcon(int unreadCount);
215 : void quitApp();
216 : bool m_reallyQuit = false;
217 :
218 : // Sprint 49: Tray helpers
219 : void rebuildTrayMenu();
220 :
221 : // Sprint 49: Desktop notifications
222 : void notifyNewMail(const QString &from, const QString &subject, qint64 uid,
223 : qint64 folderId = -1);
224 : // 67.A2: summary popup for a clustered burst (replaces in place)
225 : void notifySummaryPopup(const QString &title, const QString &body,
226 : int count, qint64 folderId, qint64 newestUid);
227 : void onNotificationAction(uint id, const QString &action);
228 : DesktopNotifier *m_desktopNotifier = nullptr;
229 : NotificationBatcher *m_notificationBatcher = nullptr; // 67.A2
230 : uint m_summaryNotificationId = 0; // summary still on screen (0 = none)
231 : // 67.A1: notification id → (folderId, uid) so "open" can switch folders
232 : QMap<uint, QPair<qint64, qint64>> m_notificationUids;
233 :
234 : // 67.A1: Select + center-scroll a mail, switching folders if needed.
235 : // When the headers are not loaded yet, the reveal is deferred via
236 : // m_pendingRestoreUid (consumed by restoreSessionMail on modelReset).
237 : void selectAndRevealMail(qint64 folderId, qint64 uid);
238 : // Resolve uid through the active model (flat or thread), select the row
239 : // (ClearAndSelect) and scroll it to center. False if uid not in model.
240 : // folderId <= 0 → controller's current folder (67.A3: search results
241 : // pass their own folderId).
242 : bool trySelectMailInView(qint64 uid, qint64 folderId = -1);
243 :
244 : // T-316: Settings sync
245 : void initSettingsSync();
246 : void triggerSettingsUpload(); // Debounced wrapper (restarts 500ms timer)
247 : void doSettingsUpload(); // Actual upload (called by timer)
248 : void onRemoteSettingsReceived(const struct SyncPayload &payload);
249 :
250 : // UI Panels
251 : FolderTree *m_folderTree = nullptr;
252 : QTreeView *m_mailList = nullptr;
253 : MailView *m_mailView = nullptr;
254 : MailListModel *m_mailListModel = nullptr;
255 : MailThreadModel *m_mailThreadModel = nullptr; // T-099
256 : MailFilterProxyModel *m_mailListProxy = nullptr;
257 : CommandBar *m_commandBar = nullptr; // T-141
258 : SearchPanel *m_searchPanel = nullptr; // Sprint 59 (U2)
259 : SearchCoordinator *m_search = nullptr; // Sprint 65 (P2.1)
260 : FolderOperationsController *m_folderOps = nullptr; // Sprint 65 (P2.2)
261 : ShortcutHelpOverlay *m_helpOverlay = nullptr; // T-148
262 :
263 : // Layout
264 : QSplitter *m_horizontalSplitter = nullptr;
265 : QSplitter *m_verticalSplitter = nullptr;
266 :
267 : // T-213: Tab system
268 : QTabBar *m_tabBar = nullptr;
269 : QStackedWidget *m_tabStack = nullptr;
270 : TabManager *m_tabManager = nullptr;
271 :
272 : // Status
273 : QLabel *m_statusLabel = nullptr;
274 : QMap<QString, QString> m_statusMessages; // T-196: keyed messages
275 : QMap<QString, QTimer *> m_statusTimers; // T-196: auto-clear timers
276 : void renderStatusBar();
277 :
278 : // T-168: Folder suggestion label (right side of status bar)
279 : QLabel *m_suggestionLabel = nullptr;
280 :
281 : // Services
282 : ImapService *m_imapService = nullptr;
283 : // T-720: monitors m_imapService for dead-socket detection, exponential-
284 : // backoff reconnect, and suspend/network reactivity. Owns its own timers.
285 : ConnectionHealthMonitor *m_imapHealth = nullptr;
286 : MailCache *m_cache = nullptr;
287 : MailController *m_controller = nullptr;
288 : UndoManager *m_undoManager = nullptr; // T-211
289 :
290 : // Test seam: how modal dialogs are run. Default executes dialog->exec();
291 : // unit tests override this to run dialogs non-blocking (no real modal loop).
292 : std::function<int(QDialog *)> m_runDialog;
293 : // Test seams for the static modal helpers (QInputDialog / QMessageBox).
294 : // Defaults call the real Qt dialogs; unit tests override them.
295 : std::function<QString(const QString &title, const QString &label,
296 : const QString &initial, bool *ok)>
297 : m_promptText;
298 : std::function<bool(const QString &title, const QString &text)> m_confirm;
299 :
300 : // T-163: Contact management
301 : ContactStore *m_contactStore = nullptr;
302 : CardDavClient *m_cardDavClient = nullptr;
303 : QTimer *m_cardDavSyncTimer = nullptr;
304 :
305 : // T-339: Calendar management (Sprint 32)
306 : class CalendarStore *m_calendarStore = nullptr;
307 : class CalendarWidget *m_calendarWidget = nullptr;
308 : class TaskListWidget *m_taskListWidget = nullptr;
309 : QTimer *m_calDavSyncTimer = nullptr;
310 : void initCalendarSync();
311 : void triggerCalDavSync();
312 : void openCalendarTab();
313 : void openTaskTab();
314 :
315 : // Sprint 39: Calendar/task editing helpers
316 : CalDavClient *createCalDavWriteClient(const QString &calendarPath,
317 : const QString &accountId = {});
318 : void showEventEditDialog(const CalendarEvent &event, bool isNew,
319 : const QDate &defaultDate = {},
320 : const QTime &defaultStart = {},
321 : const QTime &defaultEnd = {});
322 : void showTaskEditDialog(const CalendarTask &task, bool isNew);
323 : void onEventSaved(const CalendarEvent &event, bool wasNew);
324 : void onEventDeleted(const CalendarEvent &event);
325 : void onTaskSaved(const CalendarTask &task, bool wasNew);
326 : void onTaskDeleted(const CalendarTask &task);
327 : void onCalDavEventWriteSucceeded(const CalendarEvent &event);
328 : void onCalDavTaskWriteSucceeded(const CalendarTask &task);
329 :
330 : // T-166/T-171: FolderPredictor for automatic folder suggestions
331 : FolderPredictor *m_folderPredictor = nullptr;
332 : QString m_currentSuggestion;
333 : double m_currentSuggestionConfidence = 0.0;
334 : QSet<qint64> m_alternateUids; // T-234: UIDs showing alternate suggestion
335 : QString m_currentAltSuggestion; // T-234: 2nd suggestion for current mail
336 : double m_currentAltConfidence = 0.0; // T-234: Confidence of 2nd suggestion
337 : bool m_suggestionColumnVisible = false; // T-232: Column toggle state
338 : QString m_predictorDbPath; // T-232: DB path for async worker
339 : QThread *m_suggestionThread = nullptr; // T-232: Persistent worker thread
340 : class SuggestionWorker *m_suggestionWorker = nullptr; // T-232: Persistent worker
341 : QTimer *m_scrollDebounce = nullptr; // T-232: Scroll debounce
342 : QSet<qint64> m_suggestedUids; // T-232: Already-computed UIDs
343 :
344 : // Configuration
345 : QSettings m_settings{"MailJD", "MailJD"};
346 : AccountConfig m_primaryAccount;
347 : bool m_hasPrimaryAccount = false;
348 :
349 : // Pending session restore state (consumed once after IMAP connects)
350 : QString m_pendingRestoreFolder;
351 : qint64 m_pendingRestoreUid = -1;
352 : QStringList m_pendingExpandedFolders;
353 : QStringList m_reconnectExpandedFolders; // T-546: expand state saved before reconnect setFolders
354 : QStringList m_allFolderPaths; // T-062: cached for subscription dialog
355 : QList<FolderInfo> m_lastFolderList; // T-069: cached for tree refresh after hide
356 : bool m_threadViewActive = false; // T-099: thread view toggle state
357 : QAction *m_threadViewAction = nullptr; // T-127: persisted thread view
358 : bool m_pendingThreadView = false; // T-127: restore on first load
359 :
360 : // T-124: System tray
361 : QSystemTrayIcon *m_trayIcon = nullptr;
362 : QMenu *m_trayMenu = nullptr;
363 : bool m_trayNotificationShown = false;
364 : #ifdef MAILJD_KDE_INTEGRATION
365 : KStatusNotifierItem *m_sni = nullptr;
366 : #endif
367 :
368 : // Selection handler connection (to disconnect before reconnecting)
369 : QMetaObject::Connection m_selectionConnection;
370 :
371 : // Bug 2: Persist expand/collapse state of threads across model resets
372 : QSet<qint64> m_expandedThreadUids;
373 : bool m_threadExpandedInitial = false; // true = user has custom state, don't expandAll
374 : void saveExpandedState();
375 : void restoreExpandedState();
376 :
377 : // T-151: Normal-mode shortcut actions (disabled when CommandBar is active)
378 : QList<QAction *> m_normalModeActions;
379 : QMap<QAction *, QKeySequence> m_savedShortcuts;
380 :
381 : // T-147: Special folder paths detected from IMAP flags
382 : QString m_trashFolder;
383 : QString m_archiveFolder;
384 : QString m_junkFolder;
385 : QString m_draftsFolder; // T-177: Cached Drafts folder path
386 : QString m_sentFolder; // T-178: Cached Sent folder path
387 : QString m_previousFolder; // T-266: Last folder for Shift+B navigation
388 : QString m_imapDelimiter; // T-290: IMAP hierarchy delimiter (e.g. "." or "/")
389 :
390 : // T-145: gg sequence detection
391 : QTimer *m_ggTimer = nullptr;
392 : bool m_gPending = false;
393 :
394 : // T-271: IMAP reconnect state
395 : // T-720: reconnect logic moved into ConnectionHealthMonitor. We keep
396 : // m_reconnectImapConfig because SettingsSyncService::configure() reads
397 : // it (MainWindow.cpp:2803).
398 : ImapConfig m_reconnectImapConfig;
399 :
400 : // T-215: Pending tab state for restore after cache init
401 : QVariantList m_pendingTabState;
402 : // True once the saved tabs have been restored. Until then we must NOT persist
403 : // tab state (the in-progress restore would overwrite the saved state).
404 : bool m_tabRestoreDone = false;
405 : // Persist open tabs + active index. Called on every tab change so the state
406 : // survives an unclean exit (e.g. Ctrl+C), where quitApp()/saveLayout() never
407 : // run.
408 : void persistTabState();
409 :
410 : // T-316: Settings sync service
411 : SettingsSyncService *m_syncService = nullptr;
412 : QString m_syncFolderPath; // e.g. "MailJD-Settings"
413 : QTimer *m_syncDebounce = nullptr; // Bundles rapid changes into one upload
414 : };
|