diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-10 08:37:22 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-10 08:37:22 +0200 |
| commit | ecd363cd439442029229f45aa6db7d760cc41e1a (patch) | |
| tree | ade7f1f1f545fdbcfe9f64976a9d4e2d31aa64ae /src/tagchip.cpp | |
| parent | ceaec34eb17d741536f91bad66876e94aad1c45d (diff) | |
| download | qtmaildir-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.
Diffstat (limited to 'src/tagchip.cpp')
| -rw-r--r-- | src/tagchip.cpp | 230 |
1 files changed, 3 insertions, 227 deletions
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; } |
