diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-07 16:26:22 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-07 16:26:22 +0200 |
| commit | 39cbde74a560407e24b05e171df883421aa2153e (patch) | |
| tree | a7390b036f57507b17aa4291511ce33d3f297f81 /src/threadlistview.cpp | |
| parent | de884b036689b253d10ff48daa3a05cca20ba61d (diff) | |
| download | qtmaildir-39cbde74a560407e24b05e171df883421aa2153e.tar.gz qtmaildir-39cbde74a560407e24b05e171df883421aa2153e.zip | |
feat(ui): show each thread's tags under its row
The thread list was uniform and cramped: every row one line tall, with
nothing to say what a thread was about before opening it. Rows are now
roughly double height, carrying a strip of tag chips beneath the text,
with alternating row colours and a star column for flagged threads
beside the existing paperclip.
The strip is painted by the VIEW rather than by a delegate, which is
why ThreadListView exists. A delegate is handed one cell's rectangle
and cannot paint outside its column, so a strip drawn from the subject
column stops at that column's edge, losing the last tags of a
well-tagged thread, and starts at its left edge, putting the chips
under the subject instead of under the row.
Tags the row already shows another way are left out: inbox as
structure, unread as the dimming, flagged as the star, attachment as
the paperclip, and the account as the chip in the subject cell. Sorted,
since notmuch's order is not guaranteed stable and a row whose chips
reordered between repaints would flicker.
Six defects were introduced and fixed on the way here, all of them one
consequence: a QTableView paints per cell, and a row-wide strip is not
a cell. SubjectDelegate installed view-wide drew the account chip into
every column, since AccountLabelRole belongs to the row; it is split
into RowStyleDelegate for every column and SubjectDelegate for the
subject alone, with a Q_ASSERT guarding that. Row height returned from
sizeHint did nothing, because a table takes one height per row. The
strip painted from x=0 over the marker columns, via a protected
viewportMargins() that returns 0. Measuring the text band and the strip
with one font put the pills over the date. Alternating colours and the
selection are per-cell too, so the band showed bare viewport background
until the view filled it, honouring the model's own BackgroundRole
first so a deleted row is not cut in half. And that fill spanned the
full width, cutting the centred marker glyphs at their midpoint.
Closes item 5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/threadlistview.cpp')
| -rw-r--r-- | src/threadlistview.cpp | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/src/threadlistview.cpp b/src/threadlistview.cpp new file mode 100644 index 0000000..ef80e09 --- /dev/null +++ b/src/threadlistview.cpp @@ -0,0 +1,140 @@ +/* + * 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 "threadlistview.h" + +#include "tagchip.h" +#include "threadlistmodel.h" + +#include <QPaintEvent> +#include <QPainter> +#include <QScrollBar> + +void ThreadListView::paintEvent(QPaintEvent *event) +{ + QTableView::paintEvent(event); + + if (!model()) + return; + + QPainter painter(viewport()); + + // Two fonts, deliberately. The row's own font fixes where the text band + // ends, and the pills are drawn a size smaller: at the same size they read + // as a second row of content competing with the subject, rather than as + // annotation beneath it. + const QFontMetrics rowMetrics(font()); + const QFont pillFont = SubjectDelegate::pillFont(font()); + const QFontMetrics metrics(pillFont); + painter.setFont(pillFont); + + // Only the rows actually on screen. Walking the whole model would paint + // thousands of strips outside the viewport on a large query. + const int first = rowAt(0); + const int last = rowAt(viewport()->height() - 1); + const int lastRow = last >= 0 ? last : model()->rowCount() - 1; + + for (int row = qMax(0, first); row <= lastRow; ++row) { + const QModelIndex index = + model()->index(row, ThreadListModel::SubjectColumn); + + const int rowTop = rowViewportPosition(row); + const int height = rowHeight(row); + if (height <= 0) + continue; + + // The strip's band, filled to match the row before anything is drawn + // on it. + // + // A QTableView paints alternating colours and the selection PER CELL, + // so nothing paints the width to the right of the last column, and + // nothing paints the band at all where a column does not reach. Left + // unfilled, an alternate-coloured or selected row shows the viewport + // background in a strip across its lower half. Filled for every + // visible row, not only tagged ones, since an untagged row has the + // same band to account for. + // Starting at the date column, NOT at the viewport edge. The two + // leading columns hold the attachment and flag glyphs, centred in the + // full row height, so a band drawn over them cuts those glyphs in half. + const int bandLeft = + columnViewportPosition(ThreadListModel::DateColumn); + const QRect band(bandLeft, rowTop + SubjectDelegate::kRowPadding + + rowMetrics.height(), + viewport()->width() - bandLeft, + height - SubjectDelegate::kRowPadding + - rowMetrics.height()); + + // The model's own row colour wins where it has one: a deleted or spam + // thread fills its cells with crimson or orange, and painting the base + // colour across the band beneath them would cut the row in half. + const QVariant background = index.data(Qt::BackgroundRole); + + if (background.isValid()) + painter.fillRect(band, background.value<QBrush>()); + else if (selectionModel() && selectionModel()->isRowSelected(row)) + painter.fillRect(band, palette().brush(QPalette::Highlight)); + else if (alternatingRowColors() && (row % 2)) + painter.fillRect(band, palette().brush(QPalette::AlternateBase)); + else + painter.fillRect(band, palette().brush(QPalette::Base)); + + const QStringList tags = + index.data(ThreadListModel::PillTagsRole).toStringList(); + if (tags.isEmpty()) + continue; + + const QVariantList colours = + index.data(ThreadListModel::PillColoursRole).toList(); + + // The band the cells leave free, below the text they draw in the + // upper one. Measured from SubjectDelegate by both sides, so neither + // can drift into the other's half. The row's own font metrics set the + // text band; the strip's smaller font must not be used for it, or the + // pills ride up over the date and sender. + const int top = rowTop + SubjectDelegate::kRowPadding + + rowMetrics.height() + TagChip::kSpacing; + + // Aligned with the first text column rather than the viewport edge: + // the two leading columns are narrow markers for the attachment and + // flag glyphs, and a strip starting at x=0 paints straight over them. + // Indented past the date column's own left edge rather than flush with + // it: a chip starting exactly where the column does reads as part of + // the column rather than as a strip laid under the row. + int x = columnViewportPosition(ThreadListModel::DateColumn) + + TagChip::kSpacing * 2; + const int available = viewport()->width() - TagChip::kSpacing; + + for (int i = 0; i < tags.size(); ++i) { + const QSize size = TagChip::sizeFor(metrics, tags.at(i)); + + // Stop rather than wrap or elide. A row that grew to fit its tags + // would break the uniform height the list depends on, and half a + // chip reads as a rendering fault. + if (x + size.width() > available) + break; + + const QColor colour = i < colours.size() + ? colours.at(i).value<QColor>() + : QColor(0x55, 0x55, 0x5f); + + TagChip::paint(&painter, QRect(x, top, size.width(), size.height()), + tags.at(i), colour); + x += size.width() + TagChip::kSpacing; + } + } +} |
