summaryrefslogtreecommitdiffstats
path: root/tests/test_threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-07 16:26:22 +0200
committerDanilo M. <danix@danix.xyz>2026-08-07 16:26:22 +0200
commit39cbde74a560407e24b05e171df883421aa2153e (patch)
treea7390b036f57507b17aa4291511ce33d3f297f81 /tests/test_threadlistmodel.cpp
parentde884b036689b253d10ff48daa3a05cca20ba61d (diff)
downloadqtmaildir-39cbde74a560407e24b05e171df883421aa2153e.tar.gz
qtmaildir-39cbde74a560407e24b05e171df883421aa2153e.zip
feat(ui): show each thread's tags under its row
The thread list was uniform and cramped: every row one line tall, with nothing to say what a thread was about before opening it. Rows are now roughly double height, carrying a strip of tag chips beneath the text, with alternating row colours and a star column for flagged threads beside the existing paperclip. The strip is painted by the VIEW rather than by a delegate, which is why ThreadListView exists. A delegate is handed one cell's rectangle and cannot paint outside its column, so a strip drawn from the subject column stops at that column's edge, losing the last tags of a well-tagged thread, and starts at its left edge, putting the chips under the subject instead of under the row. Tags the row already shows another way are left out: inbox as structure, unread as the dimming, flagged as the star, attachment as the paperclip, and the account as the chip in the subject cell. Sorted, since notmuch's order is not guaranteed stable and a row whose chips reordered between repaints would flicker. Six defects were introduced and fixed on the way here, all of them one consequence: a QTableView paints per cell, and a row-wide strip is not a cell. SubjectDelegate installed view-wide drew the account chip into every column, since AccountLabelRole belongs to the row; it is split into RowStyleDelegate for every column and SubjectDelegate for the subject alone, with a Q_ASSERT guarding that. Row height returned from sizeHint did nothing, because a table takes one height per row. The strip painted from x=0 over the marker columns, via a protected viewportMargins() that returns 0. Measuring the text band and the strip with one font put the pills over the date. Alternating colours and the selection are per-cell too, so the band showed bare viewport background until the view filled it, honouring the model's own BackgroundRole first so a deleted row is not cut in half. And that fill spanned the full width, cutting the centred marker glyphs at their midpoint. Closes item 5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'tests/test_threadlistmodel.cpp')
-rw-r--r--tests/test_threadlistmodel.cpp118
1 files changed, 111 insertions, 7 deletions
diff --git a/tests/test_threadlistmodel.cpp b/tests/test_threadlistmodel.cpp
index b880c29..82686e5 100644
--- a/tests/test_threadlistmodel.cpp
+++ b/tests/test_threadlistmodel.cpp
@@ -20,6 +20,7 @@
#include <QSignalSpy>
#include <QtTest>
+#include "tagcolors.h"
#include "threadlistmodel.h"
class TestThreadListModel : public QObject
@@ -34,6 +35,9 @@ private slots:
void subjectShowsMessageCountOnlyForRealThreads();
void unreadThreadsRenderBold();
void readThreadsAreDimmedAndUnreadAreNot();
+ void flaggedThreadsShowAStar();
+ void pillTagsExcludeWhatTheRowAlreadyShows();
+ void theStarColumnIsNarrowAndCarriesNoText();
void theUnreadCueDoesNotDependOnFontWeight();
void aDoomedThreadKeepsItsContrastEvenWhenRead();
void tagsAreTheFirstColumnAndSubjectTheLast();
@@ -169,14 +173,15 @@ void TestThreadListModel::unreadThreadsRenderBold()
void TestThreadListModel::readThreadsAreDimmedAndUnreadAreNot()
{
- // Bold was unread's ONLY cue, and on the user's system it renders
- // identically to regular: verified with a bare QTableView and a plain
- // QStandardItemModel, so the fault is below this application, in Qt or
- // fontconfig, and no model change can reach it. Bold is kept, since it
- // works elsewhere, but the state can no longer depend on it.
+ // Bold was unread's ONLY cue, which leaves nothing to see when the
+ // desktop's own font is configured bold: every row renders bold and
+ // setBold() changes nothing. That is what the original report turned out
+ // to be, a qt6ct setting rather than a defect here, but a cue with one
+ // point of failure is worth reinforcing.
//
- // Read rows are dimmed instead, which inverts the emphasis: unread sits at
- // full contrast and the bulk of a mostly-read list recedes.
+ // Read rows are dimmed as well, which inverts the emphasis: unread sits at
+ // full contrast and the bulk of a mostly-read list recedes. Bold still
+ // applies on top.
ThreadListModel model;
ThreadSummary read = makeThread(QStringLiteral("t1"), QStringLiteral("read"));
read.tags = QStringList{ QStringLiteral("inbox") };
@@ -196,6 +201,105 @@ void TestThreadListModel::readThreadsAreDimmedAndUnreadAreNot()
"is the one that stands out");
}
+void TestThreadListModel::flaggedThreadsShowAStar()
+{
+ // "flagged" is an ordinary notmuch tag already carried in ThreadSummary,
+ // so this needs no worker query, exactly as the paperclip did not.
+ ThreadListModel model;
+ ThreadSummary plain = makeThread(QStringLiteral("t1"), QStringLiteral("plain"));
+ plain.tags = QStringList{ QStringLiteral("inbox") };
+ ThreadSummary starred = makeThread(QStringLiteral("t2"),
+ QStringLiteral("starred"));
+ starred.tags = QStringList{ QStringLiteral("inbox"),
+ QStringLiteral("flagged") };
+ model.appendBatch({ plain, starred });
+
+ const QString none =
+ model.data(model.index(0, ThreadListModel::FlagColumn),
+ Qt::DisplayRole).toString();
+ const QString star =
+ model.data(model.index(1, ThreadListModel::FlagColumn),
+ Qt::DisplayRole).toString();
+
+ QVERIFY2(none.isEmpty(), "an unflagged thread shows something in the column");
+ QVERIFY2(!star.isEmpty(), "a flagged thread shows nothing");
+ QCOMPARE(star, ThreadListModel::flagGlyph());
+}
+
+void TestThreadListModel::pillTagsExcludeWhatTheRowAlreadyShows()
+{
+ // The pills exist to say what the row does not already say. Repeating the
+ // account, the flag, the attachment or the read state as text beside the
+ // chip, the star, the paperclip and the dimming would spend the new space
+ // on things already visible.
+ ThreadListModel model;
+ ThreadSummary thread = makeThread(QStringLiteral("t1"),
+ QStringLiteral("noisy"));
+ thread.tags = QStringList{
+ QStringLiteral("inbox"), // structural, always true here
+ QStringLiteral("unread"), // shown by not being dimmed
+ QStringLiteral("flagged"), // shown by the star column
+ QStringLiteral("attachment"), // shown by the paperclip column
+ QStringLiteral("account-work"), // shown as the chip
+ QStringLiteral("SBo"), // worth showing
+ QStringLiteral("shopping/amazon"),
+ };
+ model.appendBatch({ thread });
+
+ const QStringList pills =
+ model.data(model.index(0, ThreadListModel::SubjectColumn),
+ ThreadListModel::PillTagsRole).toStringList();
+
+ QVERIFY2(pills.contains(QStringLiteral("SBo")), qPrintable(pills.join(',')));
+ QVERIFY2(pills.contains(QStringLiteral("shopping/amazon")),
+ qPrintable(pills.join(',')));
+
+ for (const QString &hidden : { QStringLiteral("inbox"),
+ QStringLiteral("unread"),
+ QStringLiteral("flagged"),
+ QStringLiteral("attachment") }) {
+ QVERIFY2(!pills.contains(hidden),
+ qPrintable(QStringLiteral("'%1' is repeated as a pill")
+ .arg(hidden)));
+ }
+
+ // The account tag is matched by shape rather than by name, since the key
+ // varies per user: whatever TagColors calls an account tag is excluded.
+ for (const QString &tag : pills) {
+ QVERIFY2(!TagColors::isAccountTag(tag),
+ qPrintable(QStringLiteral("account tag '%1' repeated as a pill")
+ .arg(tag)));
+ }
+
+ // Stable order, so a row does not reshuffle its own pills between repaints.
+ QStringList sorted = pills;
+ sorted.sort();
+ QCOMPARE(pills, sorted);
+}
+
+void TestThreadListModel::theStarColumnIsNarrowAndCarriesNoText()
+{
+ // A marker column, like the paperclip beside it: centred, and never
+ // carrying the subject or anything else that would want width.
+ ThreadListModel model;
+ ThreadSummary starred = makeThread(QStringLiteral("t1"),
+ QStringLiteral("starred"));
+ starred.tags = QStringList{ QStringLiteral("flagged") };
+ model.appendBatch({ starred });
+
+ const QModelIndex index = model.index(0, ThreadListModel::FlagColumn);
+ QCOMPARE(model.data(index, Qt::TextAlignmentRole).toInt(),
+ int(Qt::AlignCenter));
+
+ // The glyph is one character, whether it is the star or its fallback: a
+ // column sized for a marker cannot hold a word.
+ QCOMPARE(ThreadListModel::flagGlyph().size(), 1);
+
+ // And it says what it means, for anyone who cannot tell the glyph apart
+ // from the paperclip beside it.
+ QVERIFY(!model.data(index, Qt::ToolTipRole).toString().isEmpty());
+}
+
void TestThreadListModel::theUnreadCueDoesNotDependOnFontWeight()
{
// The property that matters, stated directly: strip every font from the