diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-28 20:46:44 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-28 20:46:44 +0200 |
| commit | d7c4d03f7d583767bc23406579e11bcc884dec18 (patch) | |
| tree | abe49fa75cb88d768a892cc11a8c2258869ead85 /src | |
| parent | ae2ae2df75ed780a88423b77af3b31fbe2b26389 (diff) | |
| download | qtmaildir-d7c4d03f7d583767bc23406579e11bcc884dec18.tar.gz qtmaildir-d7c4d03f7d583767bc23406579e11bcc884dec18.zip | |
feat: count a card's messages, not its repliesthread-row-identity
The expander pill read "N replies" while the row stood for the
conversation: a thread of one message and four replies said "4 replies"
over rows that listed all five messages. The user's model is messages, so
it now reads "5 messages". A thread of one still shows nothing: its row is
the message, the pill is the expander, and there is nothing to open.
ReplyCountRole becomes MessageCountRole and CardLayout::Input::replyCount
becomes messageCount, so the names stop lying about what they carry. The
label is now translated under a CardLayout context, with Italian
"messaggio"/"messaggi" shipped; %n's untranslated fallback on this Qt does
not pluralise, so the two forms are separate entries. The card's densest
geometry test needs 460px rather than 400 now that the pill is one
character wider.
Diffstat (limited to 'src')
| -rw-r--r-- | src/carddelegate.cpp | 6 | ||||
| -rw-r--r-- | src/cardlayout.cpp | 33 | ||||
| -rw-r--r-- | src/cardlayout.h | 17 | ||||
| -rw-r--r-- | src/threadlistmodel.cpp | 18 | ||||
| -rw-r--r-- | src/threadlistmodel.h | 8 | ||||
| -rw-r--r-- | src/threadlistview.cpp | 4 | ||||
| -rw-r--r-- | src/threadlistview.h | 2 | ||||
| -rw-r--r-- | src/types.h | 6 |
8 files changed, 55 insertions, 39 deletions
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp index fbdf13f..02f1df0 100644 --- a/src/carddelegate.cpp +++ b/src/carddelegate.cpp @@ -40,7 +40,7 @@ CardLayout::Input inputFor(const QModelIndex &index) CardLayout::Input in; in.isMessage = index.data(ThreadListModel::IsMessageRole).toBool(); in.depth = index.data(ThreadListModel::MessageDepthRole).toInt(); - in.replyCount = index.data(ThreadListModel::ReplyCountRole).toInt(); + in.messageCount = index.data(ThreadListModel::MessageCountRole).toInt(); in.dateFormat = index.data(ThreadListModel::DateFormatRole).toString(); // Item 70's marks. The layout reserves a rect for each, so these have to @@ -305,14 +305,14 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, drawMark(card.receivedForwardRect, Marks::Mark::ReceivedForward); drawMark(card.repliedRect, Marks::Mark::Replied); - // The reply count, which is also the expander, drawn as a PILL. + // The message count, which is also the expander, drawn as a PILL. // // A bare "3" on the card's own background read as an unexplained number // beside the subject and gave no hint that it could be clicked. The chip // shape says "this is a control", matching the tag chips on line 3, and the // word says what the number counts. if (!card.expanderRect.isEmpty()) { - const int count = index.data(ThreadListModel::ReplyCountRole).toInt(); + const int count = index.data(ThreadListModel::MessageCountRole).toInt(); const QString label = CardLayout::expanderLabel( count, option.state & QStyle::State_Open); diff --git a/src/cardlayout.cpp b/src/cardlayout.cpp index ec22635..f89d3ec 100644 --- a/src/cardlayout.cpp +++ b/src/cardlayout.cpp @@ -18,6 +18,7 @@ #include "cardlayout.h" +#include <QCoreApplication> #include <QFontMetrics> #include <QLocale> @@ -52,9 +53,9 @@ int CardLayout::markSide(const QFont &font) return qMax(8, side); } -QString CardLayout::expanderLabel(int replyCount, bool expanded) +QString CardLayout::expanderLabel(int messageCount, bool expanded) { - // "3 replies", not a bare "3". The count alone reads as an unexplained + // "5 messages", not a bare "5". The count alone reads as an unexplained // number beside the subject, and the word is what says the card opens. // // The triangle is NO LONGER part of this string. Item 70 made it a drawn @@ -65,14 +66,20 @@ QString CardLayout::expanderLabel(int replyCount, bool expanded) // when the card opens, and a caller that stopped passing the state would // hide that requirement rather than satisfy it. // - // Not translated through tr() here because CardLayout is a plain struct - // rather than a QObject; the delegate is where a translated build would - // wrap this, and the string is deliberately kept in one place so there is - // exactly one thing to change. + // CardLayout is a plain struct rather than a QObject, so this cannot use + // tr(); QCoreApplication::translate() with an explicit context is the + // equivalent, and the literal context string is what lupdate extracts + // under. The word is translated rather than the whole "%1 %2", because + // %n's untranslated fallback on this Qt does not pluralise (measured: + // "%n message(s)" stays literally "(s)"), and the two forms read cleanly + // in the .ts. Q_UNUSED(expanded); - const QString word = replyCount == 1 ? QStringLiteral("reply") - : QStringLiteral("replies"); - return QStringLiteral("%1 %2").arg(replyCount).arg(word); + const QString word = messageCount == 1 + ? QCoreApplication::translate("CardLayout", + "message") + : QCoreApplication::translate("CardLayout", + "messages"); + return QStringLiteral("%1 %2").arg(messageCount).arg(word); } QString CardLayout::widestDateSample(const QString &format) @@ -196,18 +203,18 @@ CardLayout CardLayout::compute(const Input &input, const QRect &rect, - kPaddingX), metrics.height()); - // The expander is the reply count as a PILL, on line two and on the right. + // The expander is the message count as a PILL, on line two and on the right. // // Sized from the label actually drawn rather than from a fixed sample, so // the background and the text inside it cannot disagree. Both states of the // glyph are measured because the rect must not change width when the card // is expanded: a pill that resized on click would shift the subject's // elision under the pointer. - if (input.replyCount > 0) { + if (input.messageCount > 0) { const int collapsed = smallMetrics.horizontalAdvance( - expanderLabel(input.replyCount, false)); + expanderLabel(input.messageCount, false)); const int expanded = smallMetrics.horizontalAdvance( - expanderLabel(input.replyCount, true)); + expanderLabel(input.messageCount, true)); // The triangle is a drawn mark since item 70, so the pill has to // reserve its width explicitly. It came free from the text metrics // while it was a glyph in the label, which is exactly the kind of diff --git a/src/cardlayout.h b/src/cardlayout.h index 57edca7..9620900 100644 --- a/src/cardlayout.h +++ b/src/cardlayout.h @@ -35,7 +35,7 @@ /// The card is three lines, always: /// /// sender ................................ date <- senderRect/dateRect -/// * subject @ v 3 replies <- subjectRect/expanderRect +/// * subject @ v 5 messages <- subjectRect/expanderRect /// [tag] [tag] <- tagRect struct CardLayout { @@ -45,7 +45,7 @@ struct CardLayout { bool isMessage = false; int depth = 0; ///< 0 for a thread root, 1 for a direct reply. - int replyCount = 0; ///< 0 means no expander. + int messageCount = 0; ///< Messages in the conversation; 0 means no expander. /// A QDateTime::toString() pattern from [general] date_format, or empty /// for the system's short format. @@ -105,8 +105,8 @@ struct CardLayout QRect subjectRect; QRect tagRect; - /// The reply count's rect, and the click target that toggles the thread. - /// Empty when the row has no replies. + /// The message count's rect, and the click target that toggles the thread. + /// Empty when the row has no expander. QRect expanderRect; /// The flagged mark, at the start of line two before the subject. Empty @@ -197,15 +197,18 @@ struct CardLayout /// The widest string formatDate() can return, for reserving space. static QString widestDateSample(const QString &format = QString()); - /// The expander's label: the reply count with its glyph, as drawn. + /// The expander's label: the message count with its word, as drawn. /// /// Shared with the layout for the same reason as formatDate: the rect /// reserved for the pill and the text put inside it must come from one /// place, or a count wider than the sample the layout guessed at spills /// out of its own background. /// - /// `expanded` chooses which way the triangle points. - static QString expanderLabel(int replyCount, bool expanded); + /// `expanded` chooses which way the triangle points. The word is + /// translated: a pill reading a foreign count in English next to an + /// otherwise local interface is a tiny broken window, and this is the one + /// user-facing string this plain struct owns. + static QString expanderLabel(int messageCount, bool expanded); /// Padding inside the expander pill, matching a tag chip's, so the two read /// as the same kind of object on the card. diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp index 8bb9849..5c44dd4 100644 --- a/src/threadlistmodel.cpp +++ b/src/threadlistmodel.cpp @@ -439,7 +439,7 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const m_forwardPrefixes); case IsRepliedRole: return node.isReplied(); - case ReplyCountRole: + case MessageCountRole: // A reply never offers an expander: nesting past the first level is // drawn from depth, not from further parent-child structure. return 0; @@ -714,15 +714,19 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const m_forwardPrefixes); case IsRepliedRole: return thread.isReplied(); - case ReplyCountRole: + case MessageCountRole: // Zero in a flat list, so the card draws no expander pill. The count - // and hasChildren() must agree: a card advertising "3 replies" that - // cannot be opened is the inert-glyph defect this project has already - // shipped once. + // and hasChildren() must agree: a card advertising a count for a thread + // that cannot be opened is the inert-glyph defect this project has + // already shipped once. if (m_flatMode) return 0; - // totalCount includes the root message, which is the card itself. - return qMax(0, thread.totalCount - 1); + // The number of MESSAGES in the conversation, not the replies alone: + // the row stands for the conversation now (item 177), so a thread of + // one message and four replies reads "5 messages". A thread of one + // shows nothing: its row is the message, there is nothing to expand, + // and the pill is the expander. + return thread.totalCount > 1 ? thread.totalCount : 0; case DateFormatRole: return m_dateFormat; default: diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h index 93474ac..fee4a1f 100644 --- a/src/threadlistmodel.h +++ b/src/threadlistmodel.h @@ -83,7 +83,8 @@ public: MessageIdRole, /// The message's reply depth, for the view's indentation. 1 for a - /// direct reply, since depth 0 is the root row itself. + /// direct reply; 0 is the thread's first message, which sits under the + /// conversation row unindented. MessageDepthRole, /// True when the row is a thread that has replies to show. @@ -138,7 +139,8 @@ public: /// bool; the message was replied to, from the Maildir "R" flag. IsRepliedRole, - ReplyCountRole, ///< int; 0 when a thread has no replies. + MessageCountRole, ///< int; messages in the conversation, 0 when the + ///< thread has one message and nothing to expand. /// The [general] date_format pattern, or empty for the system's short /// format. Same row value for every row. @@ -207,7 +209,7 @@ public: /// that is what keeps it from leaking: it is off by default, only the Sent /// button turns it on, and every other query turns it off again. The /// expander already comes from hasChildren() and the card's count from - /// ReplyCountRole, so flat mode is those two answering differently and + /// MessageCountRole, so flat mode is those two answering differently and /// nothing else changes. /// /// The children are not discarded, only hidden. Leaving flat mode restores diff --git a/src/threadlistview.cpp b/src/threadlistview.cpp index 5a3ce77..019d7c9 100644 --- a/src/threadlistview.cpp +++ b/src/threadlistview.cpp @@ -27,10 +27,10 @@ void ThreadListView::mousePressEvent(QMouseEvent *event) { const QModelIndex index = indexAt(event->pos()); - // The reply count IS the expander. Anything outside its rect selects the + // The message count IS the expander. Anything outside its rect selects the // card and opens it, which is what the rest of the card is for. if (event->button() == Qt::LeftButton && index.isValid() - && index.data(ThreadListModel::ReplyCountRole).toInt() > 0) { + && index.data(ThreadListModel::MessageCountRole).toInt() > 0) { QStyleOptionViewItem option; initViewItemOption(&option); diff --git a/src/threadlistview.h b/src/threadlistview.h index 3215153..a509389 100644 --- a/src/threadlistview.h +++ b/src/threadlistview.h @@ -40,7 +40,7 @@ public: using QTreeView::QTreeView; protected: - /// Toggles a thread when its reply count is clicked. + /// Toggles a thread when its message count is clicked. /// /// Being VISIBLE and being CLICKABLE are separate properties: /// setRootIsDecorated(false), needed to stop the style drawing its own diff --git a/src/types.h b/src/types.h index 8e24cd1..ee2627f 100644 --- a/src/types.h +++ b/src/types.h @@ -190,9 +190,9 @@ struct MessageNode QStringList tags; QString filePath; - /// Reply depth within the thread. 0 is the thread's first message, which - /// occupies the ROOT row rather than a child row: the user's model is - /// "N replies", so a thread of 7 shows 1 root and 6 descendants. + /// Reply depth within the thread. 0 is the thread's first message: since + /// item 177 a conversation lists it as a child row too, but it carries no + /// nesting. 1 is a direct reply. int depth = 0; bool isUnread() const { return tags.contains(QStringLiteral("unread")); } |
