summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/keymap.cpp6
-rw-r--r--src/mainwindow.cpp53
-rw-r--r--src/mainwindow.h31
3 files changed, 90 insertions, 0 deletions
diff --git a/src/keymap.cpp b/src/keymap.cpp
index cdd26a1..71c5355 100644
--- a/src/keymap.cpp
+++ b/src/keymap.cpp
@@ -35,6 +35,7 @@ QStringList KeyMap::knownActions()
QStringLiteral("toggle_unread"),
QStringLiteral("mark_all_read"),
QStringLiteral("edit_tags"),
+ QStringLiteral("tag_rules"),
QStringLiteral("flag"),
QStringLiteral("focus_query"),
QStringLiteral("complete_query"),
@@ -89,6 +90,11 @@ QList<QPair<QString, QString>> KeyMap::defaultBindings()
{ QStringLiteral("Ctrl+Shift+U"), QStringLiteral("mark_all_read") },
{ QStringLiteral("Ctrl+I"), QStringLiteral("flag") },
{ QStringLiteral("Ctrl+T"), QStringLiteral("edit_tags") },
+ // Shifted against Ctrl+T for the same reason Ctrl+Shift+U is shifted
+ // against Ctrl+U: this is the standing version of tagging, applied to
+ // every message that arrives rather than to the selection, so it takes
+ // the harder chord.
+ { QStringLiteral("Ctrl+Shift+T"), QStringLiteral("tag_rules") },
{ QStringLiteral("Ctrl+L"), QStringLiteral("focus_query") },
// Ctrl+Space is the completion idiom users already carry over from
// shells and editors, and it is a named key rather than a symbol, so
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index f731511..331354f 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -56,6 +56,7 @@
#include "cardlayout.h"
#include "tagchip.h"
#include "tagdialog.h"
+#include "tagrulesdialog.h"
#include "threadlistmodel.h"
#include "threadlistview.h"
#include "version.h"
@@ -831,6 +832,10 @@ void MainWindow::registerActions()
tr("Add or remove any tag on the selected threads"), [this]() {
editTagsOnSelection();
});
+ addAction(QStringLiteral("tag_rules"), tr("Tagging &rules..."),
+ tr("Edit the rules that tag mail as it arrives"), [this]() {
+ showTagRulesDialog();
+ });
addAction(QStringLiteral("toggle_html"), tr("Toggle &HTML"),
tr("Switch the thread between HTML and plain text"), [this]() {
m_messageView->toggleHtml();
@@ -989,6 +994,10 @@ void MainWindow::buildMenus()
messageMenu->addAction(m_actions.value(QStringLiteral("mark_all_read")));
messageMenu->addAction(m_actions.value(QStringLiteral("edit_tags")));
messageMenu->addAction(m_actions.value(QStringLiteral("flag")));
+ // Separated from the entries above: those act on the selection, this edits
+ // a rule store shared with mailctl and changes nothing that is on screen.
+ messageMenu->addSeparator();
+ messageMenu->addAction(m_actions.value(QStringLiteral("tag_rules")));
auto *viewMenu = menuBar()->addMenu(tr("&View"));
viewMenu->addAction(m_actions.value(QStringLiteral("prev_thread")));
@@ -1046,6 +1055,10 @@ void MainWindow::buildMenus()
{ QStringLiteral("toggle_unread"), QStringLiteral("mail-mark-unread") },
{ QStringLiteral("mark_all_read"), QStringLiteral("mail-mark-read") },
{ QStringLiteral("edit_tags"), QStringLiteral("tag") },
+ // NOT "tag", which edit_tags uses: with the toolbar icon-only the icon
+ // is the whole control, and editing the standing rules is not editing
+ // the selection's tags.
+ { QStringLiteral("tag_rules"), QStringLiteral("configure") },
{ QStringLiteral("complete_query"), QStringLiteral("edit-find-replace") },
{ QStringLiteral("select_all"), QStringLiteral("edit-select-all") },
{ QStringLiteral("clear_pane"), QStringLiteral("edit-clear") },
@@ -1287,6 +1300,44 @@ void MainWindow::onDatabaseStatsReady(const DatabaseStats &stats,
number(stats.tags)));
}
+void MainWindow::showTagRulesDialog()
+{
+ // One dialog. A second would edit a stale copy and the last Save would
+ // silently win, which is the lost-edit case the atomic write cannot help
+ // with because both writers are this process.
+ if (m_tagRulesDialog) {
+ m_tagRulesDialog->raise();
+ m_tagRulesDialog->activateWindow();
+ return;
+ }
+
+ auto *dialog = new TagRulesDialog(this);
+ dialog->setAttribute(Qt::WA_DeleteOnClose);
+ m_tagRulesDialog = dialog;
+
+ connect(dialog, &TagRulesDialog::countsRequested, this, [this, dialog]() {
+ QMetaObject::invokeMethod(
+ m_worker, "requestMessageCounts", Qt::QueuedConnection,
+ Q_ARG(QStringList, dialog->countQueries()),
+ Q_ARG(quint64, ++m_ruleCountGeneration));
+ });
+
+ dialog->show();
+}
+
+void MainWindow::onRuleCountsReady(const QVector<int> &counts,
+ quint64 generation)
+{
+ // Stale reply, or the dialog closed while the count was in flight. Both
+ // are ordinary rather than rare: counting every rule against a cold index
+ // takes seconds, which is long enough for the user to close the dialog or
+ // press the button again.
+ if (generation != m_ruleCountGeneration || !m_tagRulesDialog)
+ return;
+
+ m_tagRulesDialog->setCounts(counts);
+}
+
void MainWindow::showAbout()
{
QDialog dialog(this);
@@ -1360,6 +1411,8 @@ void MainWindow::wireWorker()
this, &MainWindow::onCountsReady);
connect(m_worker, &NotmuchWorker::databaseStatsReady,
this, &MainWindow::onDatabaseStatsReady);
+ connect(m_worker, &NotmuchWorker::messageCountsReady,
+ this, &MainWindow::onRuleCountsReady);
// A confirmed write clears the pending revert: without this, a later
// unrelated error would roll back a change that actually succeeded.
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 19635dd..adf63b0 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -55,6 +55,7 @@ class MessageView;
class MailSync;
class NotmuchWorker;
class QueryCompleter;
+class TagRulesDialog;
class MainWindow : public QMainWindow
{
@@ -292,6 +293,14 @@ private slots:
/// Runs a query the user clicked on the placeholder pane.
void onPlaceholderQueryRequested(const QString &query);
+ /// Opens the auto-tagging rules editor, or raises the one already open.
+ void showTagRulesDialog();
+
+ /// Message counts for the rules dialog's queries, in the order it asked
+ /// for them. Does nothing if the dialog has since closed, or if a newer
+ /// request has superseded this one.
+ void onRuleCountsReady(const QVector<int> &counts, quint64 generation);
+
private:
void buildUi();
@@ -632,6 +641,28 @@ private:
/// and reopened, so an old answer cannot fill in a newer dialog.
quint64 m_statsGeneration = 0;
+ /// The open rules dialog, or null. Held so a counts reply can reach it,
+ /// and cleared when it closes: a reply that arrives after the dialog is
+ /// gone must find nothing rather than a dangling pointer. A QPointer for
+ /// the same reason m_overviewCounts is one, and the window is open wider
+ /// here, since counting every rule takes seconds.
+ QPointer<TagRulesDialog> m_tagRulesDialog;
+
+ /// Generation of the rules dialog's counts request.
+ ///
+ /// Its OWN counter, deliberately not m_generation. That one is the QUERY
+ /// generation, and onThreadsReady, onQueryFinished, onThreadLoaded,
+ /// onThreadTreeLoaded and onMessageLoaded all compare against it directly:
+ /// bumping it here would discard whatever thread load was in flight when
+ /// the user pressed Count matches, blanking the message pane for a reason
+ /// that has nothing to do with the query. Not m_countsGeneration either,
+ /// though that one is closer: it belongs to the placeholder pane's THREAD
+ /// counts, and sharing it would let each cancel the other's reply. The two
+ /// arrive on different signals (countsReady against messageCountsReady),
+ /// so they can never be confused for one another and need not share a
+ /// counter.
+ quint64 m_ruleCountGeneration = 0;
+
/// The generation of a REFRESH query, run after a sync to bring the list
/// up to date without disturbing it.
///