aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/messagedetailsdialog.cpp15
-rw-r--r--src/messagedetailsdialog.h5
-rw-r--r--src/messageview.cpp8
-rw-r--r--tests/test_messagedetailsdialog.cpp39
4 files changed, 67 insertions, 0 deletions
diff --git a/src/messagedetailsdialog.cpp b/src/messagedetailsdialog.cpp
index e862fce..b024989 100644
--- a/src/messagedetailsdialog.cpp
+++ b/src/messagedetailsdialog.cpp
@@ -98,6 +98,17 @@ MessageDetailsDialog::MessageDetailsDialog(const QList<ThreadRenderItem> &items,
requestSearch(
row, SearchTerm::SearchMode::Narrow);
});
+ auto *exclude =
+ menu.addAction(tr("Exclude from search"));
+ // Visible but greyed rather than hidden: someone
+ // exploring a fresh window is exactly who should see
+ // that the feature exists.
+ exclude->setEnabled(m_hasQuery);
+ connect(exclude, &QAction::triggered, this,
+ [this, row]() {
+ requestSearch(
+ row, SearchTerm::SearchMode::Exclude);
+ });
menu.exec(value->mapToGlobal(pos));
});
}
@@ -156,5 +167,9 @@ void MessageDetailsDialog::requestSearch(const HeaderRow &row,
{
if (row.query.isEmpty())
return;
+ // Nothing to exclude FROM: the entry is greyed, and this is the second
+ // layer in case it is reached another way.
+ if (mode == SearchTerm::SearchMode::Exclude && !m_hasQuery)
+ return;
emit searchRequested(row.query, mode);
}
diff --git a/src/messagedetailsdialog.h b/src/messagedetailsdialog.h
index b62618a..186006f 100644
--- a/src/messagedetailsdialog.h
+++ b/src/messagedetailsdialog.h
@@ -75,6 +75,11 @@ public:
/// The rows on display, in order. Exposed for testing without rendering.
QList<HeaderRow> rows() const { return m_rows; }
+ /// Whether "Exclude from search" is offered. False with an empty query
+ /// bar: there would be nothing to exclude FROM. Exposed for testing
+ /// without popping a context menu.
+ bool canExcludeFromSearch() const { return m_hasQuery; }
+
/// Emits searchRequested for `row`, or nothing when the row carries no
/// searchable query. The menu entries call this; a test can too, without
/// popping a menu.
diff --git a/src/messageview.cpp b/src/messageview.cpp
index 9dc5b36..bdf1b13 100644
--- a/src/messageview.cpp
+++ b/src/messageview.cpp
@@ -577,6 +577,14 @@ void MessageView::addSearchEntries(QMenu *menu, const QList<SearchOffer> &offers
connect(narrow, &QAction::triggered, this, [this, entry]() {
emit searchRequested(entry.query, SearchTerm::SearchMode::Narrow);
});
+
+ auto *exclude = sub->addAction(tr("Exclude from search"));
+ // Visible but greyed rather than hidden, as in the details dialog:
+ // there must be a query to exclude FROM.
+ exclude->setEnabled(m_hasQuery);
+ connect(exclude, &QAction::triggered, this, [this, entry]() {
+ emit searchRequested(entry.query, SearchTerm::SearchMode::Exclude);
+ });
}
}
diff --git a/tests/test_messagedetailsdialog.cpp b/tests/test_messagedetailsdialog.cpp
index 403750b..073611d 100644
--- a/tests/test_messagedetailsdialog.cpp
+++ b/tests/test_messagedetailsdialog.cpp
@@ -36,6 +36,7 @@ private slots:
void offersASearchForEachValue();
void omitsAnEmptyHeader();
void messageIdIsShownButNotSearchable();
+ void excludeIsOfferedOnlyWithAQueryToExcludeFrom();
private:
/// One message, with every header populated. The date's weekday matches
@@ -180,5 +181,43 @@ void TestMessageDetailsDialog::messageIdIsShownButNotSearchable()
QCOMPARE(spy.count(), 0);
}
+void TestMessageDetailsDialog::excludeIsOfferedOnlyWithAQueryToExcludeFrom()
+{
+ // Excluding from an empty query bar would mean the whole Maildir minus one
+ // value: a legitimate query, and an implausible thing to have meant by
+ // right-clicking a value in a fresh window.
+ MessageDetailsDialog withQuery({ oneMessage() }, true);
+ MessageDetailsDialog withoutQuery({ oneMessage() }, false);
+
+ // The menu is built inside a customContextMenuRequested lambda and cannot
+ // be popped without a real context-menu event, so assert on the property
+ // its enabled state is derived from.
+ QVERIFY(withQuery.canExcludeFromSearch());
+ QVERIFY(!withoutQuery.canExcludeFromSearch());
+
+ const QList<HeaderRow> rows = withoutQuery.rows();
+ const auto from = std::find_if(
+ rows.cbegin(), rows.cend(), [](const HeaderRow &row) {
+ return row.field == QStringLiteral("from");
+ });
+ QVERIFY2(from != rows.cend(), "no From row to search from");
+
+ // The emit refuses too, so the guard does not rest on the menu alone.
+ QSignalSpy blocked(&withoutQuery,
+ &MessageDetailsDialog::searchRequested);
+ QVERIFY(blocked.isValid());
+ withoutQuery.requestSearch(*from, SearchTerm::SearchMode::Exclude);
+ QCOMPARE(blocked.count(), 0);
+
+ // And with a query it goes through, so the guard is not simply refusing
+ // every exclude.
+ QSignalSpy allowed(&withQuery, &MessageDetailsDialog::searchRequested);
+ QVERIFY(allowed.isValid());
+ withQuery.requestSearch(*from, SearchTerm::SearchMode::Exclude);
+ QCOMPARE(allowed.count(), 1);
+ QCOMPARE(allowed.at(0).at(1).value<SearchTerm::SearchMode>(),
+ SearchTerm::SearchMode::Exclude);
+}
+
QTEST_MAIN(TestMessageDetailsDialog)
#include "test_messagedetailsdialog.moc"