summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 09:15:07 +0200
committerDanilo M. <danix@danix.xyz>2026-08-04 12:52:48 +0200
commitd358e3ea59cfad6a7c93f80290bab0e19bde7bbb (patch)
tree095ac1a493d5dafed37bc3b831c2645f0500a815 /src/mainwindow.h
parent2caf15d2d032ec6fff0ca732f6ba4759ee91685d (diff)
downloadqtmaildir-d358e3ea59cfad6a7c93f80290bab0e19bde7bbb.tar.gz
qtmaildir-d358e3ea59cfad6a7c93f80290bab0e19bde7bbb.zip
feat: add MainWindow wiring query, list, message, and sync
Wires the worker thread, thread list, message pane, sync process and undo stack together, and replaces the placeholder main() with real startup: custom URL schemes registered before QApplication, a libnotmuch ABI check, and config loading. Four fixes against the drafted version: - onWorkerError() only set a status label. Its own comment elsewhere claimed it reverted the optimistic update, and the spec requires that; it did not, so a rejected write left the list showing a tag the database never received. The pending change is now recorded and rolled back, and a confirmed tagsApplied clears it so a later unrelated error cannot undo a write that succeeded. - runCurrentQuery() cleared the model but left the undo stack pointing at rows that no longer exist. Undoing after a new query would have written to the database while the visible list stayed put. The stack is cleared with the model. - m_currentMessages was assigned on every thread load and never read. Removed. - buildUi() connected sync output to m_syncLog and errors to m_statusLabel before either existed. Both are constructed before the wiring now. cidPrefix generation lives here, this being its only producer in the application, and is pinned by tests: it must never contain '!' and must be distinct per message, which are the invariants the cid: namespacing rests on. A second test holds registeredActionNames() against KeyMap::knownActions(), since those two hand-maintained lists drifting either way silently breaks a user's key binding. Mutation-verified that dropping an action fails the test by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mainwindow.h')
-rw-r--r--src/mainwindow.h148
1 files changed, 148 insertions, 0 deletions
diff --git a/src/mainwindow.h b/src/mainwindow.h
new file mode 100644
index 0000000..bbfc3ec
--- /dev/null
+++ b/src/mainwindow.h
@@ -0,0 +1,148 @@
+#pragma once
+
+#include <QHash>
+#include <QMainWindow>
+#include <QThread>
+#include <QUndoCommand>
+#include <QUndoStack>
+
+#include <functional>
+
+#include "config.h"
+#include "keymap.h"
+#include "types.h"
+
+class QLineEdit;
+class QTableView;
+class QLabel;
+class QPushButton;
+class QComboBox;
+class QPlainTextEdit;
+
+class ThreadListModel;
+class MessageView;
+class MailSync;
+class NotmuchWorker;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+public:
+ explicit MainWindow(const Config &config, QWidget *parent = nullptr);
+ ~MainWindow() override;
+
+ /// Every action name registerActions() installs. Exposed so a test can hold
+ /// it against KeyMap::knownActions(): the two lists are maintained by hand,
+ /// and a drift either way silently breaks a user's key binding.
+ static QStringList registeredActionNames();
+
+ /// The cid: namespace prefix for the nth message of a thread.
+ ///
+ /// MainWindow is the only producer of this value in the application. It
+ /// must never contain '!', which is the separator that keeps one message's
+ /// cid: references from resolving to another's.
+ static QString cidPrefixForIndex(int index);
+
+protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+private slots:
+ void runCurrentQuery();
+ void onThreadsReady(const QVector<ThreadSummary> &threads, quint64 generation);
+ void onQueryFinished(int total, quint64 generation);
+ void onThreadSelected(const QModelIndex &current, const QModelIndex &previous);
+ void onThreadLoaded(const QVector<MessageRef> &messages, quint64 generation);
+ void onWorkerError(const QString &message);
+ void onSyncFinished(bool success, int exitCode);
+
+private:
+ void buildUi();
+ void registerActions();
+ void wireWorker();
+ void showWarnings();
+
+ void tagSelected(const QStringList &add, const QStringList &remove,
+ const QString &description);
+
+ /// Sends a tag change for a set of threads without touching the undo stack.
+ /// Both tagSelected() and ThreadTagCommand route through this.
+ void sendThreadTagChange(const QStringList &threadIds,
+ const QStringList &add,
+ const QStringList &remove,
+ const QString &description);
+
+ /// Undoes the optimistic model update for a write the worker rejected.
+ void revertPendingTagChange();
+
+ friend class ThreadTagCommand;
+
+ Config m_config;
+ KeyMap m_keyMap;
+
+ QThread m_workerThread;
+ NotmuchWorker *m_worker = nullptr;
+
+ ThreadListModel *m_model = nullptr;
+ MessageView *m_messageView = nullptr;
+ MailSync *m_sync = nullptr;
+ QUndoStack m_undoStack;
+
+ QLineEdit *m_queryEdit = nullptr;
+ QTableView *m_threadView = nullptr;
+ QComboBox *m_accountBox = nullptr;
+ QPushButton *m_syncButton = nullptr;
+ QLabel *m_statusLabel = nullptr;
+ QPlainTextEdit *m_syncLog = nullptr;
+
+ QHash<QString, std::function<void()>> m_actions;
+ quint64 m_generation = 0;
+ QString m_lastQuery;
+ QString m_currentThreadId;
+
+ /// The optimistic update awaiting confirmation, kept so a worker error can
+ /// put the model back. Only the most recent one: mutations are sent from
+ /// the UI thread one user action at a time.
+ TagChange m_pendingChange;
+ QStringList m_pendingThreadIds;
+};
+
+/// Undo entry for a tag change over a set of threads.
+///
+/// Stores thread ids rather than message ids, so undo re-resolves them on the
+/// worker and stays correct even if the selection has moved on.
+class ThreadTagCommand : public QUndoCommand
+{
+public:
+ ThreadTagCommand(MainWindow *window, const QStringList &threadIds,
+ const QStringList &add, const QStringList &remove,
+ const QString &description)
+ : QUndoCommand(description), m_window(window), m_threadIds(threadIds),
+ m_add(add), m_remove(remove), m_description(description) {}
+
+ /// The stack calls redo() when the command is pushed. The change has
+ /// already been sent by that point, so the first call is skipped.
+ void redo() override
+ {
+ if (m_firstRedo) {
+ m_firstRedo = false;
+ return;
+ }
+ m_window->sendThreadTagChange(m_threadIds, m_add, m_remove,
+ m_description);
+ }
+
+ void undo() override
+ {
+ // Inverted: what was added is removed and vice versa.
+ m_window->sendThreadTagChange(m_threadIds, m_remove, m_add,
+ QStringLiteral("Undo %1").arg(m_description));
+ }
+
+private:
+ MainWindow *m_window;
+ QStringList m_threadIds;
+ QStringList m_add;
+ QStringList m_remove;
+ QString m_description;
+ bool m_firstRedo = true;
+};