From 94462ae2cc68d563f883b29f1812f93d6b5a6c06 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 24 Aug 2026 18:55:38 +0200 Subject: feat(queries): drop pinning, the menu is every saved query's home Item 94. The query row is the six built-in filters (Unread, Inbox, Important, Sent, Drafts, Trash), which compose with the account dropdown, and every saved query lives in the More queries menu. Nothing has to decide which of the user's queries get button space, which is the question item 93 would otherwise have had to answer. SavedQuery::pinned is gone from the struct, the reader, the writer, the save dialog's checkbox and the pin/unpin context action. The stored key is stripped rather than left ignored, at the user's choice. That has one non-obvious requirement: `pinned` stays named in loadSavedQueries' `known` list precisely so it is NOT collected as an unknown field, since those are preserved and written straight back. A mutation removing that name puts the key in the file for ever. Confirmed with the user before starting that the built-in set covers their use, since removing pinning removes the escape hatch this item was blocked on. Tests: four pinning tests replaced by two on the new rule, four more converted from buttons to menu entries. migrationPinsEveryEntry and aStoredGeneratedQueryIsUnpinnedNotDropped are rewritten around the property that outlived the flag rather than deleted: an entry must be KEPT, which is what both assertions were really guarding. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KEcn3u19xPqv6ggD15PG4c --- src/config.cpp | 26 +++++-------------- src/config.h | 5 ---- src/mainwindow.cpp | 66 +++++++++++++++++-------------------------------- src/mainwindow.h | 4 +-- src/savequerydialog.cpp | 8 ------ src/savequerydialog.h | 2 -- 6 files changed, 30 insertions(+), 81 deletions(-) (limited to 'src') diff --git a/src/config.cpp b/src/config.cpp index cb6168f..23c7364 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -728,10 +728,6 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) SavedQuery query; query.name = name; query.query = settings.value(name).toString(); - // Pinned, because these are buttons today. A migration that left - // them unpinned would empty the query row on the first launch - // after an upgrade, which reads as data loss. - query.pinned = true; m_savedQueries.append(query); } settings.endGroup(); @@ -804,7 +800,6 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) SavedQuery query; query.name = object.value(QStringLiteral("name")).toString(); query.query = object.value(QStringLiteral("query")).toString(); - query.pinned = object.value(QStringLiteral("pinned")).toBool(false); query.account = object.value(QStringLiteral("account")).toString(); query.generated = object.value(QStringLiteral("generated")).toString(); // A generator carries its own view mode, so "sent" is flat whether or @@ -833,19 +828,12 @@ void Config::loadSavedQueries(const QString &configPath, QSettings &settings) continue; } - // A stored entry naming a generator now duplicates a BUILT-IN filter of - // the same name, since item 93 ships all four rather than storing them. - // 0.19.0 migrated the hardcoded Sent button into exactly such an entry, - // so every existing install has one. - // - // Unpinned, never dropped: the row would otherwise carry two Sent - // buttons, one the user's to edit and one not. Deleting it would be - // data loss on a file whose readers are supposed to preserve what they - // do not own, and an unpin is reversible from the UI. - if (query.isGenerated() && isKnownGenerator(query.generated)) - query.pinned = false; - for (auto it = object.begin(); it != object.end(); ++it) { + // `pinned` is listed although nothing reads it any more (item + // 94). Dropping it from this list would make it an UNKNOWN field, + // which is preserved and written back, and the user chose to strip + // it rather than carry it forward. This is the one place a retired + // key has to stay named to be forgotten. static const QStringList known = { QStringLiteral("name"), QStringLiteral("query"), QStringLiteral("pinned"), QStringLiteral("account"), @@ -869,7 +857,7 @@ bool Config::saveSavedQueries() const // Only what carries information. The file is hand-editable, so a key // that always holds the same value, or one the generator already // implies, is just something the reader has to skip past. Same reason - // `pinned` and `account` are written only when set. + // `account` is written only when set. QJsonObject object; object.insert(QStringLiteral("name"), query.name); if (query.isGenerated()) { @@ -877,8 +865,6 @@ bool Config::saveSavedQueries() const } else { object.insert(QStringLiteral("query"), query.query); } - if (query.pinned) - object.insert(QStringLiteral("pinned"), true); if (!query.account.isEmpty()) object.insert(QStringLiteral("account"), query.account); // Skipped when the generator already implies it, which loadSavedQueries diff --git a/src/config.h b/src/config.h index 3ddd76a..4dcfbf1 100644 --- a/src/config.h +++ b/src/config.h @@ -165,11 +165,6 @@ struct SavedQuery QString name; QString query; - /// Renders as a button in the query row; otherwise it lives in the menu. - /// The two tiers are the point of the flag: a row that shows every saved - /// query does not scale past a handful. - bool pinned = false; - /// Account KEY, the INI group suffix ("work" from [account.work]), and /// empty for a query that spans every account. /// diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 42fe1bf..99eb2a3 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2782,11 +2782,10 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // from, since nothing looks them up by name. m_filterButtons.clear(); - // The built-in filters come first, in their own fixed order, and they are - // not saved queries: they are shipped, they are not in queries.json, and - // the user cannot edit or delete them (item 93). They are what the row is - // FOR; the pinned saved queries below them are the transitional half that - // item 94 removes. + // The built-in filters are the whole row: they are shipped, they are not + // in queries.json, and the user cannot edit or delete them (item 93). + // Item 94 removed the transitional half, so a saved query is never a + // button and the menu is its only home. for (const SavedQuery &filter : Config::builtinFilters()) { // Sent with no account configuring a sent folder finds nothing by // construction. Hidden rather than present and empty, which is what the @@ -2854,28 +2853,18 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) box->addWidget(button); } - // The user's own saved queries. A pinned one is still a button, beside the - // filters, until item 94 makes the menu their only home. - QList unpinned; + // The user's own saved queries, every one of them in the menu (item 94). + // The row is built-in filters only, so nothing has to decide which saved + // queries get button real estate. + QList savedQueries; for (const SavedQuery &saved : m_config.savedQueries()) { - // A generator whose accounts configure nothing produces a button that + // A generator whose accounts configure nothing produces an entry that // always finds nothing. Skipped entirely, which is what the hardcoded // Sent button did and is worth keeping. if (saved.isGenerated() && m_config.resolvedQuery(saved).isEmpty()) continue; - if (!saved.pinned) { - unpinned.append(saved); - continue; - } - auto *button = new QPushButton(saved.name, row); - // No object name here any more. "sentButton" now belongs to the BUILT-IN - // Sent filter, and a migrated Sent entry claiming it too would give two - // buttons one name, so findChild() would return whichever came first. - connect(button, &QPushButton::clicked, this, - [this, saved]() { runSavedQuery(saved); }); - addSavedQueryActions(button, saved); - box->addWidget(button); + savedQueries.append(saved); } // Everything above is left-aligned; the stretch here pushes what follows @@ -2887,16 +2876,16 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // The overflow menu, and only when something is in it: an empty menu // button is a control that always does nothing. - if (!unpinned.isEmpty()) { + if (!savedQueries.isEmpty()) { auto *menuButton = new QPushButton(tr("More queries"), row); menuButton->setObjectName(QStringLiteral("savedQueryMenuButton")); auto *menu = new QMenu(menuButton); - for (const SavedQuery &saved : unpinned) { + for (const SavedQuery &saved : savedQueries) { QAction *action = menu->addAction(saved.name); // A menu entry has no context menu of its own, so its own submenu - // carries the same actions; an unpinned query would otherwise be - // the one thing that cannot be edited or deleted. + // carries the same actions; a saved 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: @@ -2905,7 +2894,8 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // 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. + // 94 made it their only home, so this is now the ONLY way to run + // one. auto *run = new QAction(tr("Run"), entryMenu); run->setObjectName(QStringLiteral("runQuery")); connect(run, &QAction::triggered, this, @@ -2927,9 +2917,9 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout) // 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()) + // the stretch was added, since the stretch is always there: a saved query + // with no built-in filters shown still needs the row for its menu. + if (contentCount == 0 && savedQueries.isEmpty()) row->hide(); // Connected HERE rather than beside the query bar's other handlers, which @@ -2959,17 +2949,6 @@ void MainWindow::addSavedQueryActions(QWidget *target, const SavedQuery &saved) [this, saved]() { editSavedQuery(saved); }); target->addAction(edit); - auto *pin = new QAction(saved.pinned ? tr("Move to menu") - : tr("Show as a button"), - target); - pin->setObjectName(QStringLiteral("pinQuery")); - connect(pin, &QAction::triggered, this, [this, saved]() { - SavedQuery toggled = saved; - toggled.pinned = !saved.pinned; - replaceSavedQuery(saved.name, toggled); - }); - target->addAction(pin); - auto *separator = new QAction(target); separator->setSeparator(true); target->addAction(separator); @@ -3182,10 +3161,9 @@ void MainWindow::saveCurrentQuery() void MainWindow::rebuildSavedQueryRow() { - // The row is rebuilt wholesale rather than patched: a new query can be - // pinned, unpinned, or replace an existing one, and each moves a different - // widget. Deleting and rebuilding is a handful of buttons and cannot get - // the three cases wrong. + // The row is rebuilt wholesale rather than patched: a query can be added, + // deleted or replaced, and each moves a different entry. Deleting and + // rebuilding is a handful of widgets and cannot get the cases wrong. auto *old = findChild(QStringLiteral("savedQueryRow")); if (!old) return; diff --git a/src/mainwindow.h b/src/mainwindow.h index 4b1ef8f..951eaa4 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -446,8 +446,8 @@ private: /// Builds the row of saved-query buttons, the overflow menu and Sent. /// /// Its own row since item 23: an unbounded list of buttons sharing the - /// query row squeezed the field, which is the whole reason for the - /// pinned/unpinned split. + /// query row squeezed the field. Item 94 settled the split the other way, + /// so the row is built-in filters and one menu. void buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout); /// Runs a saved query, taking its account scope through the dropdown. diff --git a/src/savequerydialog.cpp b/src/savequerydialog.cpp index 2d09986..43c226b 100644 --- a/src/savequerydialog.cpp +++ b/src/savequerydialog.cpp @@ -18,7 +18,6 @@ #include "savequerydialog.h" -#include #include #include #include @@ -45,7 +44,6 @@ SaveQueryDialog::SaveQueryDialog(const Config &config, const QString &query, SavedQuery initial; initial.query = query; initial.account = accountKey; - initial.pinned = true; setWindowTitle(tr("Save query")); build(initial); } @@ -101,11 +99,6 @@ void SaveQueryDialog::build(const SavedQuery &initial) m_account->setCurrentIndex(index >= 0 ? index : 0); form->addRow(tr("Account"), m_account); - m_pinned = new QCheckBox(tr("Show as a button"), this); - m_pinned->setObjectName(QStringLiteral("saveQueryPinned")); - m_pinned->setChecked(initial.pinned); - form->addRow(QString(), m_pinned); - layout->addLayout(form); // Says what is about to happen rather than refusing the name. Overwriting @@ -162,7 +155,6 @@ SavedQuery SaveQueryDialog::savedQuery() const saved.name = m_name->text().trimmed(); saved.query = m_query->text().trimmed(); saved.account = m_account->currentData().toString(); - saved.pinned = m_pinned->isChecked(); // Carried through rather than re-derived: an edit must not turn a // generated entry into a plain one holding a snapshot of what it happened // to resolve to today. diff --git a/src/savequerydialog.h b/src/savequerydialog.h index d859254..a91654e 100644 --- a/src/savequerydialog.h +++ b/src/savequerydialog.h @@ -22,7 +22,6 @@ #include "config.h" -class QCheckBox; class QComboBox; class QLabel; class QLineEdit; @@ -73,7 +72,6 @@ private: QLineEdit *m_name = nullptr; QLineEdit *m_query = nullptr; QComboBox *m_account = nullptr; - QCheckBox *m_pinned = nullptr; QPushButton *m_ok = nullptr; QLabel *m_notice = nullptr; }; -- cgit v1.2.3