aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md11
-rw-r--r--src/config.cpp7
-rw-r--r--src/mainwindow.cpp31
-rw-r--r--tests/test_config.cpp20
-rw-r--r--tests/test_mainwindow.cpp87
5 files changed, 132 insertions, 24 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 133dd58..7b4df18 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,17 @@ point at which they are stable.
## [Unreleased]
+### Changed
+
+- The built-in filters carry icons, like the Save button at the other end of
+ the query row, with their text beside them.
+
+### Fixed
+
+- The flagged filter is labelled **Important**, matching the action of the same
+ name. It shipped in 0.21.0 as "Flagged", which put the same tag under two
+ names in one window.
+
## [0.21.0] - 2026-08-15
The query row gains four filters the application ships: Unread, Inbox, Flagged
diff --git a/src/config.cpp b/src/config.cpp
index ece1fb9..0b7fe86 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -719,7 +719,12 @@ SavedQuery Config::builtinFilter(const QString &generator)
} else if (generator == QStringLiteral("inbox")) {
filter.name = tr("Inbox");
} else if (generator == QStringLiteral("flagged")) {
- filter.name = tr("Flagged");
+ // "Important", matching the `flag` action, which item 57 renamed from
+ // "Flag" for exactly this reason. Shipping the filter as "Flagged"
+ // beside it put the same tag under two names in one window. The
+ // GENERATOR stays `flagged`: that string is stored in queries.json and
+ // matched against a closed set, so it is wire format, not a label.
+ filter.name = tr("Important");
} else if (generator == QStringLiteral("sent")) {
filter.name = tr("Sent");
// Messages rather than threads, and the only filter that sets this. A
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index bf1a33b..4343479 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1731,11 +1731,38 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
== Config::matchNothingQuery())
continue;
- auto *button = new QPushButton(filter.name, row);
+ // A QToolButton, like the Save button at the other end of the row, so
+ // the two shipped controls carry icons the same way. The user's own
+ // queries stay plain QPushButtons: they have no icon to carry and
+ // nothing to say about which is which.
+ auto *button = new QToolButton(row);
+ button->setText(filter.name);
// A stable object name per filter, so a test finds the button without
// depending on the label, which is translated.
button->setObjectName(filter.generated + QStringLiteral("Button"));
- connect(button, &QPushButton::clicked, this,
+
+ // Theme icons, not the shipped SVGs in Marks: item 70's split is that
+ // the panes are ours and the chrome is the system's, and the query row
+ // is chrome. A name the running theme lacks degrades to text on its
+ // own, which is why nothing here checks whether it resolved.
+ //
+ // mail-mark-important matches the `flag` action's own icon, since both
+ // reach the same tag: the filter finds what the action marks.
+ static const QHash<QString, QString> filterIcons = {
+ { QStringLiteral("unread"), QStringLiteral("mail-mark-unread") },
+ { QStringLiteral("inbox"), QStringLiteral("mail-inbox") },
+ { QStringLiteral("flagged"), QStringLiteral("mail-mark-important") },
+ { QStringLiteral("sent"), QStringLiteral("mail-sent") },
+ };
+ button->setIcon(
+ QIcon::fromTheme(filterIcons.value(filter.generated)));
+
+ // Icon AND text, for the reason the Save button records: this row is a
+ // row of text buttons, so an icon on its own reads as a different kind
+ // of control than it is.
+ button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+
+ connect(button, &QToolButton::clicked, this,
[this, filter]() { runFilter(filter); });
box->addWidget(button);
}
diff --git a/tests/test_config.cpp b/tests/test_config.cpp
index 6c1815c..e5487b8 100644
--- a/tests/test_config.cpp
+++ b/tests/test_config.cpp
@@ -96,6 +96,7 @@ private slots:
void allSentQuerySkipsAccountsWithoutTheKey();
void allSentQueryJoinsEveryConfiguredAccount();
void aStoredGeneratedQueryIsUnpinnedNotDropped();
+ void theFlaggedFilterIsCalledImportant();
void everyBuiltinFilterIsAKnownGenerator();
void aFilterAcrossAllAccountsIsTheUnscopedQuery();
void aTagFilterScopedToAnAccountCarriesThatAccountsPath();
@@ -1042,6 +1043,23 @@ static QString writeTwoAccounts(const QTemporaryDir &dir)
"maildir=personal\n"));
}
+void TestConfig::theFlaggedFilterIsCalledImportant()
+{
+ // Item 57 decided this and item 93 contradicted it. The `flag` ACTION has
+ // read "&Important" since 0.14.0, chosen over "Starred" partly because &I
+ // was free where &S collided with Mark spam, and the filter shipped as
+ // "Flagged" beside it: the same tag under two names in one window.
+ //
+ // The generator keeps its own name, `flagged`. That string is stored in
+ // queries.json and matched against a closed set, so it is wire format and
+ // must not follow the label.
+ const SavedQuery filter =
+ Config::builtinFilter(QStringLiteral("flagged"));
+
+ QCOMPARE(filter.name, QStringLiteral("Important"));
+ QCOMPARE(filter.generated, QStringLiteral("flagged"));
+}
+
void TestConfig::everyBuiltinFilterIsAKnownGenerator()
{
// The guard for every case below. A filter whose generator is not in the
@@ -1071,7 +1089,7 @@ void TestConfig::everyBuiltinFilterIsAKnownGenerator()
// surface for this would be built and deleted inside two items.
QCOMPARE(names, (QStringList{ QStringLiteral("Unread"),
QStringLiteral("Inbox"),
- QStringLiteral("Flagged"),
+ QStringLiteral("Important"),
QStringLiteral("Sent") }));
}
diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp
index 6a049e3..7563365 100644
--- a/tests/test_mainwindow.cpp
+++ b/tests/test_mainwindow.cpp
@@ -169,6 +169,7 @@ private slots:
void narrowingAnEmptyQueryBarIsAPlainSearch();
void aMalformedAccountIsReportedWithoutBlockingTheConstructor();
void aWorkerBackedWindowReturnsRealThreads();
+ void everyBuiltinFilterButtonCarriesAnIconAndItsText();
void aQueryInTheMenuCanActuallyBeRun();
void theFourBuiltinFiltersAreOnTheRowInOrder();
void aFilterComposesWithTheSelectedAccount();
@@ -5098,7 +5099,7 @@ void TestMainWindow::thereIsNoSentButtonWithoutASentKey()
QVERIFY(config.allSentQuery().isEmpty());
MainWindow window(config);
- QVERIFY(!window.findChild<QPushButton *>(QStringLiteral("sentButton")));
+ QVERIFY(!window.findChild<QAbstractButton *>(QStringLiteral("sentButton")));
}
void TestMainWindow::theSentButtonRunsEveryConfiguredAccount()
@@ -5116,7 +5117,7 @@ void TestMainWindow::theSentButtonRunsEveryConfiguredAccount()
}));
MainWindow window(config);
- auto *button = window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ auto *button = window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(button);
auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
@@ -5149,7 +5150,7 @@ void TestMainWindow::theSentButtonSurvivesABracketedPath()
}));
MainWindow window(config);
- auto *button = window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ auto *button = window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(button);
auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
QVERIFY(queryEdit);
@@ -5370,7 +5371,7 @@ void TestMainWindow::flatModeDoesNotSurviveTheNextQuery()
MainWindow window(config);
auto *model = window.findChild<ThreadListModel *>();
QVERIFY(model);
- auto *button = window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ auto *button = window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(button);
auto *queryEdit = window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
QVERIFY(queryEdit);
@@ -5495,7 +5496,7 @@ static void loadWithQueries(Config &config, QTemporaryDir &dir,
///
/// By object name, not by label: the labels are translated, and a user may name
/// their own query "Unread" too.
-static bool isBuiltinFilterButton(QPushButton *button)
+static bool isBuiltinFilterButton(QAbstractButton *button)
{
for (const SavedQuery &filter : Config::builtinFilters()) {
if (button->objectName() == filter.generated + QStringLiteral("Button"))
@@ -5510,9 +5511,14 @@ static QStringList savedQueryButtonLabels(MainWindow &window)
auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow"));
if (!row)
return labels;
- const QList<QPushButton *> buttons =
- row->findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
- for (QPushButton *button : buttons) {
+ // QAbstractButton, not QPushButton: the built-in filters are QToolButtons
+ // so they can carry an icon beside their text, and findChildren on the
+ // narrower type would silently skip them, leaving the filter below with
+ // nothing to filter.
+ const QList<QAbstractButton *> buttons =
+ row->findChildren<QAbstractButton *>(QString(),
+ Qt::FindDirectChildrenOnly);
+ for (QAbstractButton *button : buttons) {
// The menu button is not a saved query and must not be counted as one.
if (button->objectName() == QStringLiteral("savedQueryMenuButton"))
continue;
@@ -5920,7 +5926,7 @@ void TestMainWindow::aStoredGeneratedQueryRunsFlatAndComposed()
MainWindow window(config);
auto *button =
- window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(button);
auto *queryEdit =
@@ -5979,7 +5985,7 @@ void TestMainWindow::aRenamedSentEntryKeepsWorking()
// The built-in Sent still resolves the same query, so nothing the user
// could reach before became unreachable.
auto *button =
- window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(button);
auto *queryEdit =
window.findChild<QLineEdit *>(QStringLiteral("queryEdit"));
@@ -6014,7 +6020,7 @@ void TestMainWindow::aGeneratedQueryWithNothingToShowIsSkipped()
// missing Sent means it was skipped rather than that nothing was built.
QCOMPARE(savedQueryButtonLabels(window),
QStringList{ QStringLiteral("Inbox") });
- QVERIFY2(!window.findChild<QPushButton *>(QStringLiteral("sentButton")),
+ QVERIFY2(!window.findChild<QAbstractButton *>(QStringLiteral("sentButton")),
"a generated query with nothing to show must not get a button");
}
@@ -6100,7 +6106,7 @@ void TestMainWindow::onlyAStoredQueryOffersToBecomeATaggingRule()
// By object name, which rebuildSavedQueryRow assigns precisely so a test
// need not depend on a label the user can rename.
- auto *generated = row->findChild<QPushButton *>(
+ auto *generated = row->findChild<QAbstractButton *>(
QStringLiteral("sentButton"));
// savedQueryButton(), not a scan for the label: item 93 puts a BUILT-IN
@@ -6145,7 +6151,7 @@ void TestMainWindow::unpinningMovesAQueryToTheMenu()
auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow"));
QVERIFY(row);
QCOMPARE(savedQueryButtonLabels(window).size(), 2);
- QVERIFY(!window.findChild<QPushButton *>(
+ QVERIFY(!window.findChild<QAbstractButton *>(
QStringLiteral("savedQueryMenuButton")));
auto *button = savedQueryButton(window, QStringLiteral("Inbox"));
@@ -6350,6 +6356,45 @@ void TestMainWindow::aWorkerBackedWindowReturnsRealThreads()
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount(QModelIndex()) == 1, 15000);
}
+void TestMainWindow::everyBuiltinFilterButtonCarriesAnIconAndItsText()
+{
+ // The filters are part of the application now, so they carry icons like the
+ // Save button beside them rather than reading as bare text among the user's
+ // own queries.
+ //
+ // Text BESIDE the icon, not instead of it. The toolbar follows the
+ // desktop's own button style, but this row is a row of text buttons: an
+ // icon on its own here reads as a different kind of control than it is,
+ // which is the same argument the Save button records.
+ QTemporaryDir dir;
+ QVERIFY(dir.isValid());
+ Config config;
+ config.load(writeSentConfig(dir, {
+ {QStringLiteral("work"), QStringLiteral("Sent")},
+ }));
+
+ MainWindow window(config);
+
+ for (const SavedQuery &filter : Config::builtinFilters()) {
+ auto *button = window.findChild<QToolButton *>(
+ filter.generated + QStringLiteral("Button"));
+ QVERIFY2(button, qPrintable(QStringLiteral("no button for filter '%1'")
+ .arg(filter.generated)));
+
+ QCOMPARE(button->toolButtonStyle(), Qt::ToolButtonTextBesideIcon);
+ QCOMPARE(button->text(), filter.name);
+
+ // Whether the icon RESOLVES depends on the running icon theme, which a
+ // test cannot assume: QIcon::fromTheme returns a null icon under a
+ // platform with no theme installed, so asserting on isNull() would fail
+ // for a reason that has nothing to do with this code. What is asserted
+ // is that a name was asked for, which is the part that lives here.
+ QVERIFY2(!button->icon().name().isEmpty(),
+ qPrintable(QStringLiteral("filter '%1' was given no themed "
+ "icon name").arg(filter.generated)));
+ }
+}
+
void TestMainWindow::aQueryInTheMenuCanActuallyBeRun()
{
// An unpinned query was UNRUNNABLE. Its action carried both a triggered
@@ -6434,8 +6479,10 @@ void TestMainWindow::theFourBuiltinFiltersAreOnTheRowInOrder()
auto *row = window.findChild<QWidget *>(QStringLiteral("savedQueryRow"));
QVERIFY(row);
+ // QAbstractButton: the filters are QToolButtons, so they carry an icon
+ // beside their text like the Save button at the other end of the row.
QStringList labels;
- for (QPushButton *button : row->findChildren<QPushButton *>()) {
+ for (QAbstractButton *button : row->findChildren<QAbstractButton *>()) {
// The overflow menu is a control over the set, not a member of it.
if (button->objectName() == QStringLiteral("savedQueryMenuButton"))
continue;
@@ -6444,7 +6491,7 @@ void TestMainWindow::theFourBuiltinFiltersAreOnTheRowInOrder()
QCOMPARE(labels, (QStringList{ QStringLiteral("Unread"),
QStringLiteral("Inbox"),
- QStringLiteral("Flagged"),
+ QStringLiteral("Important"),
QStringLiteral("Sent") }));
}
@@ -6468,7 +6515,7 @@ void TestMainWindow::aFilterComposesWithTheSelectedAccount()
QCOMPARE(window.selectedAccountForTesting(), QStringLiteral("work"));
auto *unread =
- window.findChild<QPushButton *>(QStringLiteral("unreadButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("unreadButton"));
QVERIFY2(unread, "no built-in Unread button");
unread->click();
@@ -6478,7 +6525,7 @@ void TestMainWindow::aFilterComposesWithTheSelectedAccount()
// Sent under the same account is the account's OWN folder, not the union
// wrapped in a scope. See the Config test of the same name for why a row
// count cannot tell the two apart.
- auto *sent = window.findChild<QPushButton *>(QStringLiteral("sentButton"));
+ auto *sent = window.findChild<QAbstractButton *>(QStringLiteral("sentButton"));
QVERIFY(sent);
sent->click();
QCOMPARE(queryEdit->text(), QStringLiteral("path:\"work/Sent/**\""));
@@ -6503,7 +6550,7 @@ void TestMainWindow::aFilterAcrossAllAccountsIsUnscoped()
QVERIFY(window.selectedAccountForTesting().isEmpty());
auto *unread =
- window.findChild<QPushButton *>(QStringLiteral("unreadButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("unreadButton"));
QVERIFY(unread);
unread->click();
@@ -6526,7 +6573,7 @@ void TestMainWindow::aFilterDoesNotClearTheAccountSelection()
window.selectAccountForTesting(QStringLiteral("work"));
auto *unread =
- window.findChild<QPushButton *>(QStringLiteral("unreadButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("unreadButton"));
QVERIFY(unread);
unread->click();
@@ -6588,7 +6635,7 @@ void TestMainWindow::aFilterOffersNoEditOrDeleteActions()
MainWindow window(config);
auto *unread =
- window.findChild<QPushButton *>(QStringLiteral("unreadButton"));
+ window.findChild<QAbstractButton *>(QStringLiteral("unreadButton"));
QVERIFY(unread);
for (QAction *action : unread->actions()) {