diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 09:15:07 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 09:15:07 +0200 |
| commit | 633ae0c09a4a3bb1cfdf15b36328d8e2ced55d30 (patch) | |
| tree | 646d082553a3010d991c4479bc7294c456b0d6a3 /src/mainwindow.h | |
| parent | e7dceee1f449cafd07a86dfd11ff77f35897bdcf (diff) | |
| download | qtmaildir-633ae0c09a4a3bb1cfdf15b36328d8e2ced55d30.tar.gz qtmaildir-633ae0c09a4a3bb1cfdf15b36328d8e2ced55d30.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.h | 148 |
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 ¤t, 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; +}; |
