summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-15 11:09:29 +0200
committerDanilo M. <danix@danix.xyz>2026-08-15 11:09:29 +0200
commitb1a7339120385f3fd8f1f5251ec20e3f0ff94b22 (patch)
tree18c9adfc3381fdf5ebd09cb405d6ab92da270b0d
parent51f04ffc3c17da59b6072c4362029c45b780de82 (diff)
downloadqtmaildir-b1a7339120385f3fd8f1f5251ec20e3f0ff94b22.tar.gz
qtmaildir-b1a7339120385f3fd8f1f5251ec20e3f0ff94b22.zip
fix(queries): make a query in the overflow menu runnable
An unpinned query could not be run. Its menu action carried both a triggered connection and a submenu of edit actions, and Qt does not emit triggered for an action that owns a menu: clicking the entry only opened the submenu, so the connection had never fired. It shipped unnoticed because the menu was the rarely-used half while the user's queries were pinned buttons. Item 93 moved every query into the menu, which is how it surfaced, and item 94 makes the menu their only home, so this is now the path that has to work. Running is an item inside the submenu, first and above a separator, with the edit actions below it. The entry keeps its submenu because an unpinned query must still be editable and deletable. The test asserts the Run item exists and is first, then that triggering it reaches the query, then that Edit and Delete survived beside it. Restoring the old wiring fails it on the first of those, naming the Qt behaviour rather than just reporting a wrong query string.
-rw-r--r--src/mainwindow.cpp25
-rw-r--r--tests/test_mainwindow.cpp69
2 files changed, 90 insertions, 4 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 6bd91d9..bf1a33b 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1779,12 +1779,29 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
auto *menu = new QMenu(menuButton);
for (const SavedQuery &saved : unpinned) {
QAction *action = menu->addAction(saved.name);
- connect(action, &QAction::triggered, this,
- [this, saved]() { runSavedQuery(saved); });
+
// A menu entry has no context menu of its own, so its own submenu
- // carries the same three actions; an unpinned query would
- // otherwise be the one thing that cannot be edited or deleted.
+ // carries the same actions; an unpinned query would otherwise be
+ // the one thing that cannot be edited or deleted.
auto *entryMenu = new QMenu(menu);
+
+ // Running the query is an item INSIDE that submenu, and must be:
+ // Qt does not emit triggered for an action that owns a menu, so a
+ // connection on `action` itself never fires and clicking the entry
+ // only opens the submenu. That shipped, and went unnoticed while
+ // the menu was the rarely-used half and the user's queries were
+ // pinned buttons. Item 93 moved every query into the menu, and item
+ // 94 makes it their only home.
+ auto *run = new QAction(tr("Run"), entryMenu);
+ run->setObjectName(QStringLiteral("runQuery"));
+ connect(run, &QAction::triggered, this,
+ [this, saved]() { runSavedQuery(saved); });
+ entryMenu->addAction(run);
+
+ auto *runSeparator = new QAction(entryMenu);
+ runSeparator->setSeparator(true);
+ entryMenu->addAction(runSeparator);
+
addSavedQueryActions(entryMenu, saved);
action->setMenu(entryMenu);
}
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 4cc9954..6a049e3 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -169,6 +169,7 @@ private slots:
void narrowingAnEmptyQueryBarIsAPlainSearch();
void aMalformedAccountIsReportedWithoutBlockingTheConstructor();
void aWorkerBackedWindowReturnsRealThreads();
+ void aQueryInTheMenuCanActuallyBeRun();
void theFourBuiltinFiltersAreOnTheRowInOrder();
void aFilterComposesWithTheSelectedAccount();
void aFilterAcrossAllAccountsIsUnscoped();
@@ -6349,6 +6350,74 @@ void TestMainWindow::aWorkerBackedWindowReturnsRealThreads()
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
}
+void TestMainWindow::aQueryInTheMenuCanActuallyBeRun()
+{
+ // An unpinned query was UNRUNNABLE. Its action carried both a triggered
+ // connection and a submenu of edit actions, and Qt does not emit triggered
+ // for an action that owns a menu: clicking it opens the submenu and nothing
+ // else. The connection had never fired.
+ //
+ // It shipped unnoticed because the menu was the rarely-used half while the
+ // user's queries were pinned buttons. Item 93 moved every one of them into
+ // the menu, which is how it surfaced, and item 94 makes the menu their only
+ // home, so this is the path that has to work.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ Config config;
+ loadWithQueries(config, dir, QStringLiteral(R"({
+ "version": 1,
+ "queries": [
+ { "name": "Menued", "query": "tag:menued", "pinned": false }
+ ]
+ })"));
+
+ MainWindow window(config);
+ auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
+ QVERIFY(queryEdit);
+
+ auto *menuButton =
+ window.findChild<QPushButton *>(QStringLiteral("savedQueryMenuButton"));
+ QVERIFY2(menuButton, "no overflow menu for an unpinned query");
+ QVERIFY(menuButton->menu());
+
+ QAction *entry = nullptr;
+ for (QAction *action : menuButton->menu()->actions()) {
+ if (action->text() == QStringLiteral("Menued"))
+ entry = action;
+ }
+ QVERIFY2(entry, "the unpinned query is not in the menu");
+
+ // The entry keeps its submenu, because an unpinned query must still be
+ // editable and deletable. What it cannot be is the ONLY thing there: Qt
+ // does not emit triggered for an action that owns a menu, so running the
+ // query needs an item of its own.
+ QVERIFY2(entry->menu(), "the per-query actions are gone");
+
+ QAction *run = nullptr;
+ for (QAction *action : entry->menu()->actions()) {
+ if (action->objectName() == QStringLiteral("runQuery"))
+ run = action;
+ }
+ QVERIFY2(run, "no way to run the query: its submenu offers only edit "
+ "actions, and Qt never emits triggered for the parent");
+
+ // First, before the edit actions. Running is what the entry is for; editing
+ // is what one does to it occasionally.
+ QCOMPARE(entry->menu()->actions().constFirst(), run);
+
+ run->trigger();
+ QCOMPARE(queryEdit->text(), QStringLiteral("tag:menued"));
+
+ // The edit actions survived beside it.
+ QStringList names;
+ for (QAction *action : entry->menu()->actions())
+ names.append(action->objectName());
+ QVERIFY2(names.contains(QStringLiteral("editQuery")),
+ "the entry lost Edit");
+ QVERIFY2(names.contains(QStringLiteral("deleteQuery")),
+ "the entry lost Delete");
+}
+
void TestMainWindow::theFourBuiltinFiltersAreOnTheRowInOrder()
{
// Shipped, not pinned. Nothing in this config names a query, so a row with