aboutsummaryrefslogtreecommitdiffstats
path: root/src/mainwindow.h
blob: 4445894a80567dc619fcd1e36c7fc6ba395b4391 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*
 * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
 * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#pragma once

#include <QHash>
#include <QMainWindow>
#include <QThread>
#include <QUndoCommand>
#include <QUndoStack>

#include <functional>

#include "config.h"
#include "keymap.h"
#include "tagcolors.h"
#include "types.h"

class QAction;
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. Derived from the actions
    /// themselves rather than hand-maintained, so it cannot drift from what is
    /// really registered.
    QStringList registeredActionNames() const;

    /// 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);

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 buildMenus();
    void wireWorker();
    void showWarnings();
    void showShortcutReference();
    void showAbout();

    /// Creates a QAction, binds it to the sequence KeyMap holds for `name`,
    /// and registers it. `name` is the action name used in [keys].
    QAction *addAction(const QString &name, const QString &text,
                       const QString &description,
                       const std::function<void()> &handler);

    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;
    TagColors m_tagColors;

    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;

    /// Action name (as used in [keys]) to the QAction implementing it. Owned
    /// by the window through the QObject parent, not by this hash.
    QHash<QString, QAction *> m_actions;

    /// One-line description per action, for the shortcut reference. Kept
    /// beside the actions so the dialog is generated, never hand-written in
    /// parallel with them.
    QHash<QString, QString> m_actionDescriptions;

    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;
};