From 59f5161500ef670421cbba9665bd4aadbbb9ad30 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 13 Aug 2026 19:35:15 +0200 Subject: feat(queries): right-align the More queries menu, and keep the row when nothing is pinned The saved-query buttons are the row's content and read as a set; the overflow menu is a control over that set, so it belongs apart from them rather than trailing the last button. Moving the stretch above it pushes it to the right edge. Doing that exposed a latent defect in the same function. The row hid itself when its layout held nothing but the stretch, which was written as a count of one and happened to be right only because the stretch went last. With the stretch moved the count changes, and the condition as written would have hidden a row holding only the menu: a config with saved queries but none pinned would have had no route to any of them, the menu buried along with the row. The check now counts the content added before the stretch and treats an unpinned query as content in its own right. Both are mutation-checked. Putting the stretch back at the end fails the alignment test, and restoring the old hide condition fails the new one, which asserts the row survives with nothing but unpinned queries in it. The alignment is asserted on the layout's own ordering rather than on x coordinates, since a geometry assertion would also pass for a row that merely ran out of width. Co-Authored-By: Claude Opus 5 --- src/mainwindow.cpp | 16 ++++++++--- tests/test_mainwindow.cpp | 72 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 4 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 9eff3d5..61c883e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1692,6 +1692,13 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) box->addWidget(sentButton); } + // Everything above is left-aligned; the stretch here pushes what follows + // to the right edge. The buttons are the row's content and read as a set, + // while the overflow menu is a control over that set, so it sits apart + // from them rather than trailing the last one. + const int contentCount = box->count(); + box->addStretch(1); + // The overflow menu, and only when something is in it: an empty menu // button is a control that always does nothing. if (!unpinned.isEmpty()) { @@ -1707,12 +1714,13 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) box->addWidget(menuButton); } - box->addStretch(1); layout->addWidget(row); - // Nothing saved and no sent folder leaves an empty strip of padding, so - // the row goes away rather than sitting there as a gap. - if (box->count() == 1) + // Nothing on either side of the stretch leaves an empty strip of padding, + // so the row goes away rather than sitting there as a gap. Counted before + // the stretch was added, since the stretch is always there: an unpinned + // query with no pinned ones still needs the row for its menu. + if (contentCount == 0 && unpinned.isEmpty()) row->hide(); } diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 581daae..080e4cd 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -52,6 +52,7 @@ #include #include +#include #include #include #include "tagchip.h" @@ -196,6 +197,8 @@ private slots: void anUnscopedSavedQueryClearsTheAccount(); void theSaveQueryActionIsDisabledOnAnEmptyQuery(); void thereIsASaveButtonBesideTheQueryBar(); + void theMenuIsRightAlignedAwayFromTheButtons(); + void theRowSurvivesWithNothingButUnpinnedQueries(); private: /// Owns the throwaway lock table init() points every test at. A pointer @@ -5574,4 +5577,73 @@ void TestMainWindow::thereIsASaveButtonBesideTheQueryBar() QVERIFY(button->isEnabled()); } +/// Right-aligned, meaning a stretch sits between the buttons and the menu. +/// Asserted on the layout rather than on x coordinates: the offscreen platform +/// lays out widgets, but a geometry assertion here would also pass for a row +/// that simply ran out of width. +void TestMainWindow::theMenuIsRightAlignedAwayFromTheButtons() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + loadWithQueries(config, dir, QStringLiteral(R"({ + "version": 1, + "queries": [ + { "name": "Inbox", "query": "tag:inbox", "pinned": true }, + { "name": "Buried", "query": "tag:buried" } + ] + })")); + + MainWindow window(config); + auto *row = window.findChild(QStringLiteral("savedQueryRow")); + QVERIFY(row); + auto *box = qobject_cast(row->layout()); + QVERIFY(box); + + auto *menuButton = + window.findChild(QStringLiteral("savedQueryMenuButton")); + QVERIFY(menuButton); + + int menuIndex = -1; + int stretchIndex = -1; + for (int i = 0; i < box->count(); ++i) { + QLayoutItem *item = box->itemAt(i); + if (item->widget() == menuButton) + menuIndex = i; + else if (!item->widget() && item->spacerItem()) + stretchIndex = i; + } + + QVERIFY2(stretchIndex >= 0, "the row has no stretch to align against"); + QVERIFY2(menuIndex > stretchIndex, + "the menu must come AFTER the stretch to sit at the right edge"); +} + +/// The row must not vanish when every saved query is unpinned: the menu is +/// then the only way to reach any of them, and hiding the row buries it. +void TestMainWindow::theRowSurvivesWithNothingButUnpinnedQueries() +{ + QTemporaryDir dir; + QVERIFY(dir.isValid()); + Config config; + loadWithQueries(config, dir, QStringLiteral(R"({ + "version": 1, + "queries": [ + { "name": "Buried", "query": "tag:buried" }, + { "name": "AlsoBuried", "query": "tag:also" } + ] + })")); + + MainWindow window(config); + auto *row = window.findChild(QStringLiteral("savedQueryRow")); + QVERIFY(row); + QVERIFY2(!row->isHidden(), + "the row was hidden, so the only route to these queries is gone"); + + auto *menuButton = + window.findChild(QStringLiteral("savedQueryMenuButton")); + QVERIFY(menuButton); + QCOMPARE(menuButton->menu()->actions().size(), 2); +} + #include "test_mainwindow.moc" -- cgit v1.2.3