diff options
| -rw-r--r-- | src/mainwindow.cpp | 18 | ||||
| -rw-r--r-- | src/mainwindow.h | 7 | ||||
| -rw-r--r-- | src/messageview.cpp | 30 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 63 | ||||
| -rw-r--r-- | tests/test_messageview.cpp | 53 |
5 files changed, 169 insertions, 2 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index bb6aa43..115646e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -55,6 +55,7 @@ #include "querycompleter.h" #include "carddelegate.h" #include "cardlayout.h" +#include "searchterm.h" #include "tagchip.h" #include "tagdialog.h" #include "savequerydialog.h" @@ -672,6 +673,8 @@ void MainWindow::buildUi() this, &MainWindow::onPlaceholderQueryRequested); connect(m_messageView, &MessageView::staleThreadRecoveryRequested, this, &MainWindow::recoverStaleThread); + connect(m_messageView, &MessageView::searchRequested, + this, &MainWindow::runSearchFromPane); m_splitter = new QSplitter(Qt::Horizontal, central); m_splitter->addWidget(m_threadView); @@ -1648,6 +1651,21 @@ void MainWindow::onPlaceholderQueryRequested(const QString &query) runCurrentQuery(); } +void MainWindow::runSearchFromPane(const QString &query, bool extend) +{ + if (query.isEmpty()) + return; + + const QString next = + extend ? SearchTerm::extend(m_queryEdit->text(), query) : query; + + // Through the query bar and the existing runner, so the account scope, the + // generation counter and the flat-mode reset all behave exactly as they do + // for a typed query. Nothing here builds a second query path. + m_queryEdit->setText(next); + runCurrentQuery(); +} + void MainWindow::showWarnings() { const QStringList warnings = m_config.warnings() + m_keyMap.warnings(); diff --git a/src/mainwindow.h b/src/mainwindow.h index a68fcd9..f54a889 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -373,6 +373,13 @@ private slots: /// Runs a query the user clicked on the placeholder pane. void onPlaceholderQueryRequested(const QString &query); + /// Runs a search asked for from the message pane. + /// + /// `extend` narrows the current query rather than replacing it. The panes + /// carry a finished query and no knowledge of the bar; the combining + /// happens here, because only the window can see what the bar holds. + void runSearchFromPane(const QString &query, bool extend); + /// 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. diff --git a/src/messageview.cpp b/src/messageview.cpp index fbd43d6..1e6c256 100644 --- a/src/messageview.cpp +++ b/src/messageview.cpp @@ -252,6 +252,20 @@ MessageView::MessageView(QWidget *parent) m_tagStrip = new TagStrip(this); m_tagStrip->hide(); + // Item 85: a tag chip is searchable. The strip reports which chip was hit + // and where; what a tag can do is decided here, beside the other menus, so + // all three surfaces offer the same pair of operations. + connect(m_tagStrip, &TagStrip::tagContextMenuRequested, this, + [this](const QString &tag, const QPoint &globalPos) { + const QString query = SearchTerm::tag(tag); + if (query.isEmpty()) + return; + + QMenu menu(this); + addSearchEntries(&menu, { { tr("tag %1").arg(tag), query } }); + menu.exec(globalPos); + }); + auto *layout = new QVBoxLayout(this); layout->addLayout(headerRow); layout->addLayout(blockedRow); @@ -616,10 +630,22 @@ void MessageView::showDetailsDialog() return; MessageDetailsDialog dialog(m_items, this); + // The dialog's searches are the pane's searches: one signal reaches the // window whichever surface the user used. - connect(&dialog, &MessageDetailsDialog::searchRequested, - this, &MessageView::searchRequested); + // + // It CLOSES on the way out, and that is not tidiness. The dialog is modal, + // so without this the query runs and the thread list repaints behind a + // window the user still has to dismiss, making the search look like it did + // nothing. The dialog is also built from m_items, which the new query is + // about to replace, so what it displays would describe a thread the pane + // has already stopped showing. + connect(&dialog, &MessageDetailsDialog::searchRequested, this, + [this, &dialog](const QString &query, bool extend) { + emit searchRequested(query, extend); + dialog.accept(); + }); + dialog.exec(); } diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 6e300bf..8b60e37 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -88,6 +88,9 @@ private slots: void markReadTimerIsNotArmedForAReadThread(); void aConfirmedEditArmsTheAutoSync(); void autoSyncDebouncesABurstOfEdits(); + void aSearchFromThePaneReplacesTheQuery(); + void aSearchFromThePaneCanNarrowTheQuery(); + void narrowingAnEmptyQueryBarIsAPlainSearch(); void autoSyncIsNotArmedWhenDisabledOrWithNothingPending(); void autoSyncSkipsWhileABackgroundSyncIsRunning(); void aSuccessfulSyncRefreshesRatherThanRerunningTheQuery(); @@ -3725,6 +3728,66 @@ void TestMainWindow::autoSyncDebouncesABurstOfEdits() 1); } +void TestMainWindow::aSearchFromThePaneReplacesTheQuery() +{ + const Config config; + MainWindow window(config); + + QLineEdit *queryEdit = + window.findChild<QLineEdit *>(QStringLiteral("queryEdit")); + QVERIFY2(queryEdit, "no query bar: the window was never built"); + + MessageView *view = window.findChild<MessageView *>(); + QVERIFY2(view, "no message view"); + + queryEdit->setText(QStringLiteral("tag:inbox")); + emit view->searchRequested(QStringLiteral("from:\"foo@example.org\""), false); + + QCOMPARE(queryEdit->text(), QStringLiteral("from:\"foo@example.org\"")); +} + +void TestMainWindow::aSearchFromThePaneCanNarrowTheQuery() +{ + // The case the feature exists for: a query returning a thousand threads is + // narrowed by adding a condition. BOTH sides are parenthesised, because + // 'a or b AND c' binds as 'a or (b AND c)', which WIDENS a search the user + // asked to narrow, and notmuch reports no error for it. + const Config config; + MainWindow window(config); + + QLineEdit *queryEdit = + window.findChild<QLineEdit *>(QStringLiteral("queryEdit")); + QVERIFY2(queryEdit, "no query bar: the window was never built"); + + MessageView *view = window.findChild<MessageView *>(); + QVERIFY2(view, "no message view"); + + queryEdit->setText(QStringLiteral("tag:inbox or tag:flagged")); + emit view->searchRequested(QStringLiteral("from:\"foo@example.org\""), true); + + QCOMPARE(queryEdit->text(), + QStringLiteral("(tag:inbox or tag:flagged) AND (from:\"foo@example.org\")")); +} + +void TestMainWindow::narrowingAnEmptyQueryBarIsAPlainSearch() +{ + // Rather than "() AND (x)", which matches nothing. + const Config config; + MainWindow window(config); + + QLineEdit *queryEdit = + window.findChild<QLineEdit *>(QStringLiteral("queryEdit")); + QVERIFY2(queryEdit, "no query bar: the window was never built"); + + MessageView *view = window.findChild<MessageView *>(); + QVERIFY2(view, "no message view"); + + queryEdit->clear(); + emit view->searchRequested(QStringLiteral("tag:inbox"), true); + + QCOMPARE(queryEdit->text(), QStringLiteral("tag:inbox")); +} + void TestMainWindow::autoSyncIsNotArmedWhenDisabledOrWithNothingPending() { // A negative delay is the switch that restores the pre-0.16.0 behaviour, so diff --git a/tests/test_messageview.cpp b/tests/test_messageview.cpp index f83a377..edcba03 100644 --- a/tests/test_messageview.cpp +++ b/tests/test_messageview.cpp @@ -24,6 +24,7 @@ #include <QtTest> #include "htmlbuilder.h" +#include "messagedetailsdialog.h" #include "messageview.h" #include "mimeparser.h" @@ -53,6 +54,7 @@ private slots: void headerOffersNoSenderForARealThread(); void headerOffersNothingForAnAbsentField(); void bodySelectionBecomesAQuotedSearch(); + void aSearchFromTheDetailsDialogClosesIt(); private: QWebEngineView *webViewOf(MessageView *view) const @@ -697,5 +699,56 @@ void TestMessageView::bodySelectionBecomesAQuotedSearch() .label.isEmpty()); } +void TestMessageView::aSearchFromTheDetailsDialogClosesIt() +{ + // The dialog is modal. Without closing it, the query runs and the thread + // list repaints BEHIND a window the user still has to dismiss, so the + // search looks like it did nothing. The dialog also describes m_items, + // which the new query is about to replace. + MessageView view; + view.showThread({ oneMessage() }); + + QSignalSpy spy(&view, &MessageView::searchRequested); + QVERIFY(spy.isValid()); + + // showDetailsDialog() blocks in exec(), so the dialog has to be driven + // from a timer once it is up. + bool foundTheDialog = false; + QTimer::singleShot(0, &view, [&view, &foundTheDialog]() { + auto *dialog = view.findChild<MessageDetailsDialog *>(); + if (!dialog) { + // Never leave exec() spinning: a missing dialog must fail the test, + // not hang the suite. + QApplication::exit(1); + return; + } + foundTheDialog = true; + + const QList<HeaderRow> rows = dialog->rows(); + const auto from = std::find_if( + rows.cbegin(), rows.cend(), [](const HeaderRow &row) { + return row.field == QStringLiteral("from"); + }); + if (from == rows.cend()) { + dialog->reject(); + return; + } + + dialog->requestSearch(*from, false); + }); + + view.showDetailsDialog(); + + QVERIFY2(foundTheDialog, "the details dialog never appeared"); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.at(0).at(0).toString(), + QStringLiteral("from:\"Sender <sender@example.org>\"")); + + // exec() returned, which is the assertion: the dialog closed on its own + // rather than waiting for the user to dismiss it. + QVERIFY(!view.findChild<MessageDetailsDialog *>() + || !view.findChild<MessageDetailsDialog *>()->isVisible()); +} + QTEST_MAIN(TestMessageView) #include "test_messageview.moc" |
