aboutsummaryrefslogtreecommitdiffstats
path: root/src/tagchip.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-10 09:33:44 +0200
committerDanilo M. <danix@danix.xyz>2026-08-10 09:33:44 +0200
commit01419de209c2b5e2ae7b996e6b5ff1baa2efb3da (patch)
tree25718fd36e87eac721f41b02cca46b6fb07e94d9 /src/tagchip.cpp
parentf72dba9f6c463c6823d85701e51d8be38dd22a62 (diff)
parente1dba2987a9a1e87b92801959df9c9d4f1375d2f (diff)
downloadqtmaildir-01419de209c2b5e2ae7b996e6b5ff1baa2efb3da.tar.gz
qtmaildir-01419de209c2b5e2ae7b996e6b5ff1baa2efb3da.zip
Merge branch 'card-list': the thread pane as a list of cards
Replaces the five-column grid with a single column of three-line cards. Item 53 recorded that the columns, not the cues drawn inside them, were what made the list read as a table of records; item 20 had already shipped finished, tested and green and been rejected on sight for exactly that reason. A card is sender and date, subject with the flag, attachment and reply-count marks, and tags, at one uniform height. Replies indent under a continuous spine and show only the tags their thread does not carry. The account colour runs down the card's left edge, replacing the chip that used to eat a third of every subject line, with matching swatches in the account dropdown. Sorting newest or oldest first is new and remembered. Closes items 20, 51, 53 and 60. The four defects that mattered were all found by rendering cards to an image and looking at them, with the suite green through every one: a date clipped on unread cards because bold is wider than the font the layout measured, an accent bar painted in a colour identical to the background, an expander pill in a palette role a theme had made equal to Base, and three separate faults from trusting notmuch's reply depth to mean structure when it only means how notmuch happened to thread the mail.
Diffstat (limited to 'src/tagchip.cpp')
-rw-r--r--src/tagchip.cpp176
1 files changed, 3 insertions, 173 deletions
diff --git a/src/tagchip.cpp b/src/tagchip.cpp
index 1f4ad79..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,143 +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();
- 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));
- QStyledItemDelegate::paint(painter, chrome, index);
-
- 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() + 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();
-}
-
-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;
}