aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/mainwindow.cpp18
-rw-r--r--src/mainwindow.h7
-rw-r--r--src/messageview.cpp30
-rw-r--r--tests/test_mainwindow.cpp63
-rw-r--r--tests/test_messageview.cpp53
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"