diff options
| -rw-r--r-- | src/messagedetailsdialog.cpp | 15 | ||||
| -rw-r--r-- | src/messagedetailsdialog.h | 5 | ||||
| -rw-r--r-- | src/messageview.cpp | 8 | ||||
| -rw-r--r-- | tests/test_messagedetailsdialog.cpp | 39 |
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" |
