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