summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 17:00:55 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 17:00:55 +0200
commitb8de7ce746718c7193e40b0f70cd3e2178f4ed8a (patch)
treeab280755094321666d29d50694c48722809d953c /src
parent1ab12862fd47bf3816f7fad7b8a96eceb8f84272 (diff)
downloadqtmaildir-b8de7ce746718c7193e40b0f70cd3e2178f4ed8a.tar.gz
qtmaildir-b8de7ce746718c7193e40b0f70cd3e2178f4ed8a.zip
feat(rules): preview a rule's mail in the thread list
Item 77. The dialog could say how many messages a rule matched and not which ones. A Preview in list button now runs the selected rule's query in the main window; the dialog stays open, since comparing the rule against its results is the point. Two constraints from the backlog entry, both now asserted and both mutation-checked. The query runs exactly as stored, with no tag:new and no wrapping parentheses. The post-new hook adds those when it applies a rule, and a preview that copied them would match nothing outside a sync window, since tag:new is set only on mail that has just arrived. The account selector is cleared first. runQuery() wraps the bar's text in the selected account's scope, and a rule query usually names its own path already, so previewing one with an account selected would scope it twice and show an empty list, which reads as "this rule collects no mail". The second mutation only fails once the test's config has an account to select: with the default empty config the selector sits on "All accounts" anyway, and asserting that a preview leaves it there passed against the mutation. Recorded in the test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp43
-rw-r--r--src/mainwindow.h26
-rw-r--r--src/tagrulesdialog.cpp24
-rw-r--r--src/tagrulesdialog.h14
4 files changed, 107 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index e8b4dda..e2df6de 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1324,6 +1324,9 @@ void MainWindow::showTagRulesDialog()
// setFolders refills the rows that exist by then.
QMetaObject::invokeMethod(m_worker, "requestFolders", Qt::QueuedConnection);
+ connect(dialog, &TagRulesDialog::previewRequested,
+ this, &MainWindow::onRulePreviewRequested);
+
connect(dialog, &TagRulesDialog::countsRequested, this, [this, dialog]() {
QMetaObject::invokeMethod(
m_worker, "requestMessageCounts", Qt::QueuedConnection,
@@ -1570,6 +1573,46 @@ void MainWindow::onCountsReady(const QVector<int> &counts, quint64 generation)
m_messageView->showPlaceholder(placeholderHelpers());
}
+QString MainWindow::queryTextForTesting() const
+{
+ return m_queryEdit->text();
+}
+
+QString MainWindow::selectedAccountForTesting() const
+{
+ return m_accountBox->currentData().toString();
+}
+
+void MainWindow::selectAccountForTesting(const QString &key)
+{
+ const int index = m_accountBox->findData(key);
+ if (index >= 0)
+ m_accountBox->setCurrentIndex(index);
+}
+
+void MainWindow::onRulePreviewRequested(const QString &query)
+{
+ // Unscoped, deliberately. runQuery() wraps the bar's text in the selected
+ // account's scope, and a rule query usually names its own path already
+ // (path:"work/**" is what every account rule looks like), so previewing
+ // one with an account selected would scope it twice and match nothing.
+ // That reads as "this rule collects no mail", which is the opposite of
+ // what the preview is for.
+ m_accountBox->setCurrentIndex(0);
+
+ // Through the query bar, like onPlaceholderQueryRequested: the bar then
+ // shows what is on screen and the user can edit the rule's query there
+ // before deciding to change the rule itself.
+ m_queryEdit->setText(query);
+ runCurrentQuery();
+
+ // The dialog is a separate window and may be covering this one or sitting
+ // beside it. Raising makes the result visible either way, and the dialog
+ // stays open so the two can be compared.
+ raise();
+ activateWindow();
+}
+
void MainWindow::onPlaceholderQueryRequested(const QString &query)
{
// Through the query bar rather than straight to the worker, so the bar
diff --git a/src/mainwindow.h b/src/mainwindow.h
index adf63b0..18fbbb4 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -185,6 +185,27 @@ public:
/// The generation the next counts reply must carry to be accepted.
quint64 countsGenerationForTesting() const { return m_countsGeneration; }
+ /// The query bar's text, and the account the selector is scoped to
+ /// (empty for "All accounts"). Both are what a rule preview writes: the
+ /// bar so the user can see and edit what ran, and the selector because
+ /// runQuery() wraps the text in the selected account's scope, which would
+ /// double-scope a rule query that already names its own path.
+ QString queryTextForTesting() const;
+ QString selectedAccountForTesting() const;
+
+ /// Scopes the view to one account, as choosing it in the selector does.
+ /// A test for the rule preview needs this: with no account selected the
+ /// box already sits at "All accounts", so asserting that a preview leaves
+ /// it there passes whether or not the preview clears it.
+ void selectAccountForTesting(const QString &key);
+
+ /// Runs a rule preview without the dialog, which the offscreen platform
+ /// cannot click a button in.
+ void previewRuleQueryForTesting(const QString &query)
+ {
+ onRulePreviewRequested(query);
+ }
+
protected:
void closeEvent(QCloseEvent *event) override;
@@ -293,6 +314,11 @@ private slots:
/// Runs a query the user clicked on the placeholder pane.
void onPlaceholderQueryRequested(const QString &query);
+ /// Runs one tagging rule's query in the thread list, so the user can see
+ /// which mail it collects. The rules dialog stays open; the point is to
+ /// compare the rule against its results.
+ void onRulePreviewRequested(const QString &query);
+
/// Opens the auto-tagging rules editor, or raises the one already open.
void showTagRulesDialog();
diff --git a/src/tagrulesdialog.cpp b/src/tagrulesdialog.cpp
index 285043a..4fca971 100644
--- a/src/tagrulesdialog.cpp
+++ b/src/tagrulesdialog.cpp
@@ -234,6 +234,11 @@ TagRulesDialog::TagRulesDialog(QWidget *parent)
buttons->addWidget(copyButton);
buttons->addWidget(deleteButton);
buttons->addStretch();
+ m_previewButton = new QPushButton(tr("&Preview in list"), this);
+ m_previewButton->setToolTip(
+ tr("Run this rule's query in the main window, to see which mail it "
+ "collects. This does not tag anything."));
+ buttons->addWidget(m_previewButton);
buttons->addWidget(refreshButton);
layout->addLayout(buttons);
@@ -251,6 +256,8 @@ TagRulesDialog::TagRulesDialog(QWidget *parent)
this, &TagRulesDialog::onCopyRule);
connect(deleteButton, &QPushButton::clicked,
this, &TagRulesDialog::onDeleteRule);
+ connect(m_previewButton, &QPushButton::clicked,
+ this, &TagRulesDialog::previewForTest);
connect(refreshButton, &QPushButton::clicked,
this, &TagRulesDialog::countsRequested);
connect(box, &QDialogButtonBox::accepted,
@@ -369,6 +376,23 @@ int TagRulesDialog::heightDemandedBelowListForTest() const
return m_splitter->widget(1)->minimumSizeHint().height();
}
+void TagRulesDialog::previewForTest()
+{
+ // Flush any half-typed edit first, so previewing shows what the rule
+ // says NOW rather than what it said when the row was selected.
+ applyEditsToCurrentRule();
+
+ const int index = currentIndex();
+ if (index < 0 || index >= m_working.size())
+ return;
+
+ const QString query = m_working.at(index).query;
+ if (query.isEmpty())
+ return;
+
+ emit previewRequested(query);
+}
+
int TagRulesDialog::conditionAreaHeightForTest() const
{
// The CAP itself, not a qMin against the scroll area's own size hint: a
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index de35a55..e8979fe 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -109,6 +109,10 @@ public:
/// the width the user had dragged.
void reloadListForTest();
+ /// Presses Preview, which is otherwise reachable only through a click on
+ /// a button whose geometry the offscreen platform does not guarantee.
+ void previewForTest();
+
/// The height the condition-row editor asks for. This is what squeezed
/// the rule list: a stretch factor only shares out space ABOVE each
/// widget's minimum, so every row added here came out of the list.
@@ -133,6 +137,15 @@ signals:
/// Asks the owner to run countQueries() through the worker.
void countsRequested();
+ /// Asks the owner to run one rule's query in the main window, so the user
+ /// can see WHICH mail a rule collects rather than how much.
+ ///
+ /// The query goes out exactly as stored: no `tag:new`, and no wrapping
+ /// parentheses. The post-new hook adds both when it applies a rule, and a
+ /// preview that copied them would match nothing outside a sync window,
+ /// since `tag:new` is set only on mail that has just arrived.
+ void previewRequested(const QString &query);
+
public slots:
/// Corpus counts, positionally paired with countQueries().
void setCounts(const QVector<int> &counts);
@@ -241,6 +254,7 @@ private:
/// still squeezed, because a stretch factor only shares out space above
/// each widget's minimum and the form's grew with every condition row.
QSplitter *m_splitter = nullptr;
+ QPushButton *m_previewButton = nullptr;
QVBoxLayout *m_rowsLayout = nullptr;
QVBoxLayout *m_exclusionsLayout = nullptr;
QLabel *m_exclusionsHeader = nullptr;