aboutsummaryrefslogtreecommitdiffstats
path: root/src/threadlistmodel.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-11 20:24:05 +0200
committerDanilo M. <danix@danix.xyz>2026-08-11 20:24:05 +0200
commit1faf94eb35e8270a659f215d260db73bcaa3f8d7 (patch)
tree6305f6532cf1abc000eb261ce85c3ce12bc0a8d3 /src/threadlistmodel.cpp
parent64d3138ba923071069da6c9bc458a25a9cc7d27f (diff)
downloadqtmaildir-1faf94eb35e8270a659f215d260db73bcaa3f8d7.tar.gz
qtmaildir-1faf94eb35e8270a659f215d260db73bcaa3f8d7.zip
feat(panes): draw the pane marks from shipped SVGs, not font glyphs
Items 70 and 69, the second folded into the first as item 70's own size note predicted it should be. The panes drew their state marks as font glyphs: U+1F4CE for an attachment and U+2605 for a flagged thread, each with a fallback for a font that cannot render it. Both fell back to "*", so on such a font a flagged thread and one carrying an attachment were indistinguishable, which is a defect the fallback introduced rather than prevented. What a mark looks like was also the desktop's decision rather than this application's, and the panes are exactly where it should not be: the user asked for the toolbar and menus to keep following their icon theme while the panes stop. Six marks now ship in assets/icons/marks/: flagged, attachment, passed, replied and the two expander triangles. QIcon::fromTheme still resolves every toolbar and menu icon and was not touched. Licensing chose the shapes. The look came from a GPL3 icon theme, and this project is GPLv2-only, which are incompatible: GPLv2's "no further restrictions" clause bars shipping GPL3 assets in a v2-only work. The six were drawn fresh in the same idiom instead, with no path data copied. The idiom is generic: solid single-path silhouettes at 16x16 with no strokes. They are compiled in as string literals rather than loaded from a .qrc. src/CMakeLists.txt already records why resources belong to the executable: a qrc in the static library registers itself from a global initialiser the linker drops. The tests link the library, so a resource-based mark would be missing exactly where it needs asserting. assets/icons/marks/ stays the editable source. One asset serves both palettes. Every payload paints with fill="currentColor", which QSvgRenderer renders black rather than resolving, so Marks::pixmap composites the wanted colour with CompositionMode_SourceIn. A mark then takes the card's own pen colour and follows selection and the read/unread dimming without a second variant to keep in step. CardLayout reserves a rect per mark and CardDelegate paints into it. The marks were glyphs inside the subject STRING, so their width came free from the text metrics; as icons the geometry has to know they are there or the subject runs underneath them. The expander pill had the same trap, its triangle being a glyph in expanderLabel(), and now reserves that width explicitly. Item 69's part: passed and replied were words in the tag strip and are marks beside the subject now. The message pane's header carries the flagged and attachment marks next to the subject, per the user's decision that the right pane needs those two and only outside the message area. A duplicate that no test caught is worth recording. Every geometry assertion passed while a card showed passed as BOTH an arrow and a green tag chip: the chip filter had no reason to know a mark had appeared. It was found by rendering real cards to an image and looking at them. isDrawnAsAMark() is now one list consulted by both PillTagsRole and MessageOwnTagsRole, since two copies drifting apart is how a tag ends up drawn twice on one row and not at all on another. Fourteen tests: nine in test_marks, four in test_cardlayout, one in test_threadlistmodel. Mutation-checked at four points, each failing a test: the subject ignoring the marks, the flag not indenting the subject, the pill forgetting the triangle's width, and the recolour composite removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/threadlistmodel.cpp')
-rw-r--r--src/threadlistmodel.cpp75
1 files changed, 40 insertions, 35 deletions
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 5874648..20d6915 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -27,25 +27,27 @@
#include <QFontDatabase>
#include <QFontMetrics>
-QString ThreadListModel::attachmentGlyph()
+namespace {
+
+/// Whether a tag is already drawn on the card as a mark, and so must not also
+/// appear as a chip.
+///
+/// One list, consulted by both PillTagsRole (a thread's chips) and
+/// MessageOwnTagsRole (a reply's own chips). Two copies drifted apart is
+/// exactly how a tag ends up drawn twice on one row and not at all on another.
+bool isDrawnAsAMark(const QString &tag)
{
- // U+1F4CE PAPERCLIP, with a fallback for a system whose default font
- // cannot draw it: an unrenderable codepoint shows as a tofu box, which
- // reads as "something is broken" rather than "this has an attachment".
- // Computed once; the font does not change under a running application.
- static const QString glyph = [] {
- const char32_t paperclip = 0x1F4CE;
- const QString preferred = QString::fromUcs4(&paperclip, 1);
- const QFontMetrics metrics{QFontDatabase::systemFont(
- QFontDatabase::GeneralFont)};
- // "*" as the fallback: ASCII, present in every practical font, and
- // unambiguous in a column that shows nothing else.
- return metrics.inFontUcs4(paperclip) ? preferred
- : QStringLiteral("*");
- }();
- return glyph;
+ static const QStringList marks = {
+ QStringLiteral("flagged"),
+ QStringLiteral("attachment"),
+ QStringLiteral("passed"),
+ QStringLiteral("replied"),
+ };
+ return marks.contains(tag);
}
+} // namespace
+
QColor ThreadListModel::deletedColour()
{
// Desaturated crimson: legible under white text on a dark theme, and calm
@@ -61,22 +63,6 @@ QColor ThreadListModel::spamColour()
return QColor(0xa8, 0x5c, 0x18);
}
-QString ThreadListModel::flagGlyph()
-{
- // U+2605 BLACK STAR, with the same fallback reasoning as the paperclip: an
- // unrenderable codepoint shows as tofu, which reads as breakage rather
- // than as "flagged". The solid star, not the outlined U+2606, since it has
- // to register at small size beside a paperclip.
- static const QString glyph = [] {
- const char32_t star = 0x2605;
- const QString preferred = QString::fromUcs4(&star, 1);
- const QFontMetrics metrics{QFontDatabase::systemFont(
- QFontDatabase::GeneralFont)};
- return metrics.inFontUcs4(star) ? preferred : QStringLiteral("*");
- }();
- return glyph;
-}
-
QColor ThreadListModel::replyBackground()
{
// Mixed from the palette rather than fixed, for the same reason as
@@ -308,6 +294,13 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
: QStringList();
QStringList own;
for (const QString &tag : node.tags) {
+ // Drawn as a mark on this row's own line two since items 69
+ // and 70, so a chip would repeat it. Filtered here as well as
+ // in PillTagsRole because "own" is a difference against the
+ // THREAD, and a reply that is flagged where its thread is not
+ // would otherwise show both the mark and the word.
+ if (isDrawnAsAMark(tag))
+ continue;
if (!threadTags.contains(tag))
own.append(tag);
}
@@ -343,6 +336,10 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
return node.hasAttachment();
case IsFlaggedRole:
return node.isFlagged();
+ case IsPassedRole:
+ return node.isPassed();
+ case IsRepliedRole:
+ return node.isReplied();
case ReplyCountRole:
// A reply never offers an expander: nesting past the first level is
// drawn from depth, not from further parent-child structure.
@@ -425,16 +422,20 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
//
// deleted and spam are kept: they repaint the whole row, so a pill is
// redundant there too, but a doomed thread is rare and worth naming.
+ // passed and replied joined this list with item 69: they are drawn
+ // marks on line two now, so a chip repeating the word is the same
+ // duplication flagged and attachment were already dropped for. This is
+ // what item 69 asked for, "tags like Passed and Replied should use
+ // icons instead", and the chip has to go or both appear at once.
static const QStringList hidden = {
QStringLiteral("inbox"),
QStringLiteral("unread"),
- QStringLiteral("flagged"),
- QStringLiteral("attachment"),
};
QStringList pills;
for (const QString &tag : thread.tags) {
- if (hidden.contains(tag) || TagColors::isAccountTag(tag))
+ if (hidden.contains(tag) || isDrawnAsAMark(tag)
+ || TagColors::isAccountTag(tag))
continue;
pills.append(tag);
}
@@ -512,6 +513,10 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
return thread.hasAttachment();
case IsFlaggedRole:
return thread.isFlagged();
+ case IsPassedRole:
+ return thread.isPassed();
+ case IsRepliedRole:
+ return thread.isReplied();
case ReplyCountRole:
// Zero in a flat list, so the card draws no expander pill. The count
// and hasChildren() must agree: a card advertising "3 replies" that