summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-10 08:37:22 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 08:37:22 +0200
commitecd363cd439442029229f45aa6db7d760cc41e1a (patch)
treeade7f1f1f545fdbcfe9f64976a9d4e2d31aa64ae
parentceaec34eb17d741536f91bad66876e94aad1c45d (diff)
downloadqtmaildir-ecd363cd439442029229f45aa6db7d760cc41e1a.tar.gz
qtmaildir-ecd363cd439442029229f45aa6db7d760cc41e1a.zip
feat(view): paint the whole card in one delegate
Replaces SubjectDelegate. The tag chips come home from the view: the strip was painted there only because a delegate cannot paint outside its column and the strip spanned all five, and with one column there is nothing to span. RowStyleDelegate is inherited rather than dropped. Its job survives the redesign: Qt resolves ForegroundRole into the palette's Text roles and prefers those over HighlightedText, so the read/unread dimming would win on a selected row and land as grey on the highlight. What it loses is the rest of its body, which aligned cells against a text band and centred two marker columns; both described a grid that no longer exists. A reply's Re: prefix is stripped here. Every reply repeating the thread's subject is the visual signature of a table of records, which is the thing item 53 is about. The account chip becomes a bar down the card's left edge, and the reply spines inherit its colour, so an expanded thread is bounded by one accent from its root to its last reply without a second line in the gutter. Neither uses the raw account colour: that colour is chosen to be a chip's fill with legible text on top, and the same value as a thin line has to be followable down an expansion without competing with the senders, so it is blended toward the palette's Base by the weight threadLineColour() already uses. A reply resolves its THREAD's colour by walking to the root, since AccountColourRole is empty on a message row and a neutral spine under an accented root would break the continuous edge. The build is red at this commit; the view and window still name the old delegate.
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/carddelegate.cpp208
-rw-r--r--src/carddelegate.h68
-rw-r--r--src/tagchip.cpp230
-rw-r--r--src/tagchip.h56
5 files changed, 282 insertions, 281 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5478fab..fdac2c1 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,6 +6,7 @@ add_library(qtmaildir_lib STATIC
htmlbuilder.cpp
cidschemehandler.cpp
cardlayout.cpp
+ carddelegate.cpp
notmuchworker.cpp
tagchip.cpp
tagcolors.cpp
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp
new file mode 100644
index 0000000..bc03b5a
--- /dev/null
+++ b/src/carddelegate.cpp
@@ -0,0 +1,208 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "carddelegate.h"
+
+#include "cardlayout.h"
+#include "threadlistmodel.h"
+
+#include <QApplication>
+#include <QDateTime>
+#include <QGuiApplication>
+#include <QPainter>
+#include <QRegularExpression>
+#include <QStyle>
+
+namespace {
+
+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();
+ return in;
+}
+
+} // namespace
+
+QRect CardDelegate::expanderRectFor(const QStyleOptionViewItem &option,
+ const QModelIndex &index)
+{
+ return CardLayout::compute(inputFor(index), option.rect, option.font)
+ .expanderRect;
+}
+
+QColor CardDelegate::accentLineColour(const QColor &accountColour)
+{
+ if (!accountColour.isValid())
+ return ThreadListModel::threadLineColour();
+
+ // The same 0.35 weight threadLineColour() uses, toward Base rather than
+ // toward Text, so the two kinds of line sit at the same visual strength.
+ const QColor base = QGuiApplication::palette().color(QPalette::Base);
+ constexpr qreal kWeight = 0.35;
+ const qreal inverse = 1.0 - kWeight;
+ return QColor::fromRgbF(
+ accountColour.redF() * kWeight + base.redF() * inverse,
+ accountColour.greenF() * kWeight + base.greenF() * inverse,
+ accountColour.blueF() * kWeight + base.blueF() * inverse);
+}
+
+QSize CardDelegate::sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ Q_UNUSED(index);
+ // One height for every row, thread and reply alike. Asserted directly in
+ // test_cardlayout rather than left to two cards happening to agree.
+ return QSize(option.rect.width(), CardLayout::heightFor(option.font));
+}
+
+void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const
+{
+ // Background, selection and any model fill first, through the style, so a
+ // selected or doomed card looks right before anything is drawn on top.
+ QStyleOptionViewItem chrome = option;
+ initStyleOption(&chrome, index);
+ chrome.text.clear();
+ const QWidget *widget = option.widget;
+ QStyle *style = widget ? widget->style() : QApplication::style();
+ style->drawControl(QStyle::CE_ItemViewItem, &chrome, painter, widget);
+
+ const CardLayout card =
+ CardLayout::compute(inputFor(index), option.rect, option.font);
+
+ painter->save();
+
+ // The account's colour, for both the accent bar and the spines.
+ //
+ // A reply must resolve its THREAD's colour, not its own: AccountColourRole
+ // is empty on a message row, and a spine that fell back to the neutral
+ // line under an accented root would break the one continuous edge this
+ // design is built on. index.parent() is the thread for a depth-1 reply and
+ // the containing subtree for a deeper one, so walk to the root.
+ QModelIndex root = index;
+ while (root.parent().isValid())
+ root = root.parent();
+ const QColor accountColour =
+ root.data(ThreadListModel::AccountColourRole).value<QColor>();
+ const QColor lineColour = accentLineColour(accountColour);
+
+ // The accent bar, thread cards only. Drawn after the chrome so the
+ // selection highlight cannot cover it: which account a card belongs to
+ // must stay readable on the row the user is looking at.
+ if (!card.accentRect.isEmpty())
+ painter->fillRect(card.accentRect, lineColour);
+
+ // Spines, under everything else, in the same accent so an expanded thread
+ // is bounded by one colour from its root to its last reply.
+ for (const QRect &spine : card.spines)
+ painter->fillRect(spine, lineColour);
+
+ // Selection outranks the model's foreground, and the order matters: a read
+ // card carries a dimmed colour blended against the UNSELECTED background,
+ // so over the highlight it lands grey-on-highlight and close to unreadable.
+ const QVariant foreground = index.data(Qt::ForegroundRole);
+ if (option.state & QStyle::State_Selected)
+ painter->setPen(option.palette.highlightedText().color());
+ else if (foreground.isValid())
+ painter->setPen(foreground.value<QBrush>().color());
+ else
+ painter->setPen(option.palette.text().color());
+
+ // The model's font carries bold for unread and strike-out for deleted;
+ // initStyleOption resolved it into chrome.font.
+ painter->setFont(chrome.font);
+ const QFontMetrics metrics(chrome.font);
+
+ // Line 1: sender, then the date flush right.
+ painter->drawText(card.senderRect, Qt::AlignVCenter | Qt::AlignLeft,
+ metrics.elidedText(
+ index.data(ThreadListModel::SendersRole).toString(),
+ Qt::ElideRight, card.senderRect.width()));
+ const QDateTime date =
+ index.data(ThreadListModel::DateRole).toDateTime();
+ painter->drawText(card.dateRect, Qt::AlignVCenter | Qt::AlignRight,
+ date.toString(QStringLiteral("yyyy-MM-dd hh:mm")));
+
+ // Line 2: the flag mark, the subject, the attachment mark.
+ QString subject = index.data(ThreadListModel::SubjectRole).toString();
+ if (index.data(ThreadListModel::IsMessageRole).toBool()) {
+ // Every reply repeating "Re: <the thread's subject>" is the visual
+ // signature of a table of records, which is what item 53 is about.
+ static const QRegularExpression re(
+ QStringLiteral("^\\s*(?:[Rr][Ee]\\s*:\\s*)+"));
+ subject.remove(re);
+ }
+ QString line2;
+ if (index.data(ThreadListModel::IsFlaggedRole).toBool())
+ line2 += ThreadListModel::flagGlyph() + QLatin1Char(' ');
+ line2 += subject;
+ if (index.data(ThreadListModel::HasAttachmentRole).toBool())
+ line2 += QLatin1Char(' ') + ThreadListModel::attachmentGlyph();
+ painter->drawText(card.subjectRect, Qt::AlignVCenter | Qt::AlignLeft,
+ metrics.elidedText(line2, Qt::ElideRight,
+ card.subjectRect.width()));
+
+ // The reply count, which is also the expander.
+ if (!card.expanderRect.isEmpty()) {
+ painter->setFont(CardLayout::smallFont(chrome.font));
+ const int count = index.data(ThreadListModel::ReplyCountRole).toInt();
+ const QString glyph = (option.state & QStyle::State_Open)
+ ? QStringLiteral("▾")
+ : QStringLiteral("▸");
+ painter->drawText(card.expanderRect, Qt::AlignVCenter | Qt::AlignRight,
+ QStringLiteral("%1 %2").arg(glyph).arg(count));
+ painter->setFont(chrome.font);
+ }
+
+ painter->restore();
+
+ // Line 3: the chips. A thread card draws its own tags; a reply draws only
+ // the tags its thread does not already carry, so the thread's chips are
+ // not repeated down the whole expansion.
+ const bool isMessage =
+ index.data(ThreadListModel::IsMessageRole).toBool();
+ const QStringList tags =
+ index.data(isMessage ? ThreadListModel::MessageOwnTagsRole
+ : ThreadListModel::PillTagsRole)
+ .toStringList();
+ const QVariantList colours =
+ index.data(isMessage ? ThreadListModel::MessageOwnColoursRole
+ : ThreadListModel::PillColoursRole)
+ .toList();
+
+ const QFont chipFont = CardLayout::smallFont(chrome.font);
+ const QFontMetrics chipMetrics(chipFont);
+ painter->save();
+ painter->setFont(chipFont);
+ int x = card.tagRect.left();
+ for (int i = 0; i < tags.size(); ++i) {
+ const QSize size = TagChip::sizeFor(chipMetrics, tags.at(i));
+ if (x + size.width() > card.tagRect.right())
+ break; // Out of room; a clipped chip reads as a rendering fault.
+ const QColor colour = i < colours.size()
+ ? colours.at(i).value<QColor>()
+ : QColor(0x55, 0x55, 0x5f);
+ TagChip::paint(painter, QRect(QPoint(x, card.tagRect.top()), size),
+ tags.at(i), colour);
+ x += size.width() + TagChip::kSpacing;
+ }
+ painter->restore();
+}
diff --git a/src/carddelegate.h b/src/carddelegate.h
new file mode 100644
index 0000000..317c78a
--- /dev/null
+++ b/src/carddelegate.h
@@ -0,0 +1,68 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include "tagchip.h"
+
+/// Paints a whole card: three lines, all of it, including the tag chips.
+///
+/// It replaces both SubjectDelegate and ThreadListView::paintEvent. The view
+/// used to paint the tag strip because a delegate cannot paint outside its
+/// column and the strip spanned all five; with one column there is nothing to
+/// span, so the strip comes home to the delegate and the view stops painting
+/// entirely. That removes the two failure modes CLAUDE.md records for the
+/// strip, a deleted row cut in half and every other row showing a bare stripe,
+/// both of which existed because the view had to re-honour alternating
+/// colours, the selection and BackgroundRole across cells it did not own.
+///
+/// Inherits RowStyleDelegate for its one job, which still matters: Qt resolves
+/// Qt::ForegroundRole into the palette's Text roles and then prefers those over
+/// HighlightedText, so the read/unread dimming would otherwise win on a
+/// selected row and land as grey on the highlight colour.
+class CardDelegate : public RowStyleDelegate
+{
+ Q_OBJECT
+public:
+ using RowStyleDelegate::RowStyleDelegate;
+
+ void paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+ QSize sizeHint(const QStyleOptionViewItem &option,
+ const QModelIndex &index) const override;
+
+ /// The expander's rect for a row, so the VIEW can hit-test a click without
+ /// duplicating the layout. The delegate draws it and the view owns the
+ /// click, because a delegate gets no click of its own without an editor.
+ static QRect expanderRectFor(const QStyleOptionViewItem &option,
+ const QModelIndex &index);
+
+ /// An account's colour as a thin LINE rather than as a chip's fill.
+ ///
+ /// Never use the raw account colour for the accent bar or the spine. That
+ /// colour is chosen to be a background with legible text drawn on top
+ /// (TagColors::textColourOn picks black or white against it). The same
+ /// colour as a few pixels of line on the pane's own background is a
+ /// different problem: it has to be followable down a long expansion
+ /// WITHOUT competing with the senders beside it, which is the constraint
+ /// threadLineColour() states and meets by blending 0.35 toward the
+ /// palette's text. This blends the account colour toward the palette's
+ /// Base by the same weight, keeping the hue that identifies the account
+ /// and dropping the saturation that would shout.
+ static QColor accentLineColour(const QColor &accountColour);
+};
diff --git a/src/tagchip.cpp b/src/tagchip.cpp
index b3e743e..d770a56 100644
--- a/src/tagchip.cpp
+++ b/src/tagchip.cpp
@@ -55,42 +55,6 @@ void paint(QPainter *painter, const QRect &rect, const QString &text,
} // namespace TagChip
-int SubjectDelegate::subjectBandHeight(const QStyleOptionViewItem &option)
-{
- return QFontMetrics(option.font).height();
-}
-
-QFont SubjectDelegate::pillFont(const QFont &rowFont)
-{
- QFont font = rowFont;
-
- // Two points down, floored. One point was measured to change nothing at a
- // 12pt desktop font: 12 and 11 both render 17px tall, so the pills came
- // out the same size as the subject and read as competing content rather
- // than as annotation.
- //
- // pointSize() is -1 when the font was specified in pixels, which
- // subtracting from would be nonsense, hence the two branches.
- if (rowFont.pointSize() > 0)
- font.setPointSize(qMax(6, rowFont.pointSize() - 2));
- else if (rowFont.pixelSize() > 0)
- font.setPixelSize(qMax(8, rowFont.pixelSize() - 3));
-
- return font;
-}
-
-int SubjectDelegate::rowHeightFor(const QFont &rowFont)
-{
- // The text band uses the ROW's font and the strip its own smaller one.
- // Measuring both with one font is what put the pills over the date text.
- const QFontMetrics rowMetrics(rowFont);
- const QFontMetrics pillMetrics(pillFont(rowFont));
-
- return rowMetrics.height()
- + TagChip::sizeFor(pillMetrics, QStringLiteral("x")).height()
- + kRowPadding * 2 + TagChip::kSpacing;
-}
-
void RowStyleDelegate::initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const
{
@@ -115,197 +79,9 @@ void RowStyleDelegate::initStyleOption(QStyleOptionViewItem *option,
option->palette.setColor(QPalette::WindowText, highlighted);
}
- // Top-aligned and on one line, matching the subject beside them.
- //
- // The row is tall enough for a pill strip under the text, and Qt centres a
- // cell's text in the whole rectangle by default: date and sender floated
- // into the middle while the subject sat at the top, so the three did not
- // share a baseline. Confining the rectangle to the text band puts them all
- // on one.
- //
- // Wrapping matters more than it looks. A long sender ran to a second line,
- // which reached down into the strip's band and collided with the pills; a
- // cell cannot know they are there, since the view paints them afterwards.
- // Eliding keeps every row's text inside its own band whatever it holds.
- // Top of the row rather than centre of it, so the alignment is expressed
- // without shrinking the rectangle: the rect is also what the background
- // and selection fill are drawn into, and clipping it to the text band
- // would leave the highlight covering only the upper part of the row.
+ // One line, elided. A card draws its own text through CardDelegate, but
+ // this still governs whatever Qt draws for the item itself, and a wrapped
+ // string would run past the card's own three lines.
option->features &= ~QStyleOptionViewItem::WrapText;
option->textElideMode = Qt::ElideRight;
-
- // The marker columns keep their centring. Their glyphs are the row's
- // symbols rather than its text, so aligning them with the subject's
- // baseline would strand them at the top of a tall row with the pill strip
- // empty beneath; centred, they read as marking the whole row.
- const bool marker = index.column() == ThreadListModel::AttachmentColumn
- || index.column() == ThreadListModel::FlagColumn;
- option->displayAlignment = marker
- ? Qt::AlignCenter
- : (Qt::AlignLeft | Qt::AlignTop);
-}
-
-void SubjectDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
- const QModelIndex &index) const
-{
- // AccountLabelRole is a property of the ROW, not of a cell, so this
- // delegate must only ever be installed on the subject column. Installed
- // view-wide it draws the account chip into every column, which is exactly
- // what happened when that was tried.
- Q_ASSERT(index.column() == ThreadListModel::SubjectColumn);
-
- const QString account =
- index.data(ThreadListModel::AccountLabelRole).toString();
-
- // The expander is drawn HERE and not in QTreeView::drawBranches, which is
- // the obvious place and does not work. drawBranches runs before the row's
- // cells, so with the expander column set to the subject the delegate's own
- // background fills straight over it: measured at 8 surviving pixels of a
- // 60-pixel triangle, which is exactly the near-invisible dot that made this
- // override necessary in the first place. The delegate owns this cell and
- // paints after the background, so it is the only place the glyph survives.
- const auto drawExpander = [&](const QRect &cell) {
- if (!index.data(ThreadListModel::HasRepliesRole).toBool())
- return;
-
- const int size = qMax(7, qMin(cell.height() / 3, 10));
- const QPoint centre(cell.left() + size,
- cell.top() + subjectBandHeight(option) / 2
- + kRowPadding);
-
- QPolygon triangle;
- if (option.state & QStyle::State_Open) {
- triangle << QPoint(centre.x() - size / 2, centre.y() - size / 4)
- << QPoint(centre.x() + size / 2, centre.y() - size / 4)
- << QPoint(centre.x(), centre.y() + size / 2);
- } else {
- triangle << QPoint(centre.x() - size / 4, centre.y() - size / 2)
- << QPoint(centre.x() + size / 2, centre.y())
- << QPoint(centre.x() - size / 4, centre.y() + size / 2);
- }
-
- painter->save();
- painter->setRenderHint(QPainter::Antialiasing, true);
- painter->setPen(Qt::NoPen);
- // From the palette, so it survives a theme change, and undimmed: this
- // is the only cue that a thread can be opened at all.
- painter->setBrush(option.palette.color(QPalette::Text));
- painter->drawPolygon(triangle);
- painter->restore();
- };
- // Room for the expander in front of whatever follows, on a thread row that
- // has one. Reserved before either branch draws, so the chip and the bare
- // subject are indented identically and a thread with replies does not sit
- // a few pixels left of one without.
- const bool hasReplies =
- index.data(ThreadListModel::HasRepliesRole).toBool();
- const int expanderWidth = hasReplies ? kExpanderWidth : 0;
-
- if (account.isEmpty()) {
- // No chip to draw, so the base class renders the text, confined to the
- // upper band: the lower one belongs to the row-wide pill strip that
- // ThreadListView paints after every cell.
- QStyleOptionViewItem chrome = option;
- initStyleOption(&chrome, index);
- chrome.rect.setHeight(subjectBandHeight(option));
- chrome.rect.setLeft(chrome.rect.left() + expanderWidth);
- QStyledItemDelegate::paint(painter, chrome, index);
-
- drawExpander(option.rect);
- return;
- }
-
- // Draw the row's own background and selection first, then the chip and the
- // subject on top, so a selected or struck-through row still looks right.
- QStyleOptionViewItem chrome = option;
- initStyleOption(&chrome, index);
- chrome.text.clear();
- const QWidget *widget = option.widget;
- QStyle *style = widget ? widget->style() : QApplication::style();
- style->drawControl(QStyle::CE_ItemViewItem, &chrome, painter, widget);
-
- const QFontMetrics metrics(option.font);
- const QSize chipSize = TagChip::sizeFor(metrics, account);
-
- // The subject and its chip occupy the upper band; ThreadListView paints
- // the pill strip across the lower one. Centring the chip in the whole row
- // would leave it floating beside that gap rather than beside its text.
- const int textBandHeight = subjectBandHeight(option);
- const int textTop = option.rect.top() + kRowPadding;
-
- const QRect chipRect(option.rect.left() + expanderWidth + TagChip::kSpacing,
- textTop + (textBandHeight - chipSize.height()) / 2,
- chipSize.width(), chipSize.height());
-
- const QColor colour =
- index.data(ThreadListModel::AccountColourRole).value<QColor>();
- TagChip::paint(painter, chipRect, account,
- colour.isValid() ? colour : QColor(0x55, 0x55, 0x5f));
-
- // The subject follows the chip, elided so a long one cannot overflow.
- QRect textRect = option.rect;
- textRect.setLeft(chipRect.right() + TagChip::kSpacing * 2);
- textRect.setTop(textTop);
- textRect.setHeight(textBandHeight);
- if (textRect.width() <= 0)
- return;
-
- painter->save();
- // Selection outranks the model's colour, and that order matters. A read
- // thread carries a dimmed foreground blended against the UNSELECTED
- // background, so painting it over the highlight leaves grey-on-purple,
- // which is close to unreadable. The highlight already carries the "this
- // row" signal, so the read/unread distinction can yield to it for as long
- // as the row is selected.
- //
- // A doomed thread is the exception that proves the rule: its white is not
- // a dimming but a contrast requirement against its own fill, and the fill
- // is drawn under the selection too.
- const QVariant foreground = index.data(Qt::ForegroundRole);
- if (option.state & QStyle::State_Selected)
- painter->setPen(option.palette.highlightedText().color());
- else if (foreground.isValid())
- painter->setPen(foreground.value<QBrush>().color());
- else
- painter->setPen(option.palette.text().color());
-
- // The model's font carries bold for unread and strike-out for deleted.
- // initStyleOption() already resolved it into chrome.font; using it rather
- // than option.font is what keeps those cues on a delegate-drawn subject.
- const QVariant fontData = index.data(Qt::FontRole);
- const QFont rowFont = fontData.isValid() ? fontData.value<QFont>()
- : chrome.font;
- painter->setFont(rowFont);
- const QFontMetrics rowMetrics(rowFont);
- painter->drawText(textRect, Qt::AlignVCenter | Qt::AlignLeft,
- rowMetrics.elidedText(index.data(Qt::DisplayRole).toString(),
- Qt::ElideRight, textRect.width()));
- painter->restore();
-
- // Last, so the chrome fill above cannot cover it. BOTH branches of this
- // function have to call it: a thread row with an account chip takes this
- // one, and that is every row in the real application, so calling it only
- // from the no-chip branch leaves the feature invisible in practice while
- // still passing any test built on an untagged thread.
- drawExpander(option.rect);
-}
-
-QSize SubjectDelegate::sizeHint(const QStyleOptionViewItem &option,
- const QModelIndex &index) const
-{
- QSize size = QStyledItemDelegate::sizeHint(option, index);
- const QString account =
- index.data(ThreadListModel::AccountLabelRole).toString();
- if (!account.isEmpty()) {
- const QFontMetrics metrics(option.font);
- size.setWidth(size.width() + TagChip::sizeFor(metrics, account).width()
- + TagChip::kSpacing * 3);
- }
-
- // Height comes from rowHeightFor(), applied by the view to every row at
- // once. A QTableView takes ONE height per row, so a hint returned here
- // would only win if the view happened to ask this column, and this
- // delegate is on the subject column alone.
- size.setHeight(rowHeightFor(option.font));
- return size;
}
diff --git a/src/tagchip.h b/src/tagchip.h
index 3145358..5514cae 100644
--- a/src/tagchip.h
+++ b/src/tagchip.h
@@ -59,8 +59,8 @@ void paint(QPainter *painter, const QRect &rect, const QString &text,
/// the selection highlight it lands as grey on the highlight colour, close to
/// unreadable.
///
-/// Applied to the columns that have no delegate of their own; SubjectDelegate
-/// inherits it for the subject column.
+/// Inherited by CardDelegate, which is the only delegate the thread list
+/// installs.
class RowStyleDelegate : public QStyledItemDelegate
{
Q_OBJECT
@@ -72,55 +72,3 @@ protected:
const QModelIndex &index) const override;
};
-/// Item delegate for the subject column: draws the account chip in front of
-/// the subject text, so which mailbox a thread came from reads at a glance
-/// without a tags column spelling it out.
-/// **Install on the subject column only.** It reads AccountLabelRole, which is
-/// a property of the row rather than of a cell, so as a view-wide delegate it
-/// draws the account chip into every column.
-class SubjectDelegate : public RowStyleDelegate
-{
- Q_OBJECT
-public:
- using RowStyleDelegate::RowStyleDelegate;
-
- void paint(QPainter *painter, const QStyleOptionViewItem &option,
- const QModelIndex &index) const override;
- QSize sizeHint(const QStyleOptionViewItem &option,
- const QModelIndex &index) const override;
-
- /// Vertical breathing room above the subject and below the pill row.
- static constexpr int kRowPadding = 4;
-
- /// How far a reply row is indented under its thread.
- ///
- /// Deliberately far wider than Qt's 20px default. A thread row carries an
- /// account chip in front of its subject and a reply row does not, so a
- /// reply's text starts about a chip's width to the LEFT of its thread's
- /// before any indent is applied. 20px does not cover that, and the replies
- /// come out looking flush or outdented; this has to beat a chip's width to
- /// read as nesting at all.
- static constexpr int kReplyIndent = 72;
-
- /// Horizontal room reserved in front of a thread's subject for the
- /// expander glyph the delegate draws.
- static constexpr int kExpanderWidth = 18;
-
- /// The font the pill strip is drawn in: a size down from the row's own.
- ///
- /// At the same size the pills read as a second row of content competing
- /// with the subject, rather than as annotation beneath it. Derived from
- /// the row font rather than fixed, so it follows the desktop's font size.
- static QFont pillFont(const QFont &rowFont);
-
- /// The height every row gets, tall enough for the subject and a pill strip
- /// beneath it. The view applies this itself: a QTableView takes one height
- /// for the whole row, so leaving it to a single column's sizeHint would
- /// let whichever column the view happens to ask decide.
- static int rowHeightFor(const QFont &rowFont);
-
-protected:
- /// The height of the band the subject text occupies. Everything below it
- /// belongs to ThreadListView's row-wide pill strip.
- static int subjectBandHeight(const QStyleOptionViewItem &option);
-};