From 39cbde74a560407e24b05e171df883421aa2153e Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 7 Aug 2026 16:26:22 +0200 Subject: 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 --- src/mainwindow.cpp | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) (limited to 'src/mainwindow.cpp') diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 30de89c..77322bc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -55,6 +55,7 @@ #include "tagchip.h" #include "tagdialog.h" #include "threadlistmodel.h" +#include "threadlistview.h" #include "version.h" QStringList MainWindow::registeredActionNames() const @@ -500,7 +501,10 @@ void MainWindow::buildUi() // Thread list and message pane. m_model = new ThreadListModel(this); m_model->setTagColors(&m_tagColors); - m_threadView = new QTableView(central); + // ThreadListView, not a plain QTableView: it paints the row-wide tag + // strip under each row's cells, which no delegate can do because a + // delegate is confined to one column's rectangle. + m_threadView = new ThreadListView(central); m_threadView->setModel(m_model); m_threadView->setSelectionBehavior(QAbstractItemView::SelectRows); m_threadView->setSelectionMode(QAbstractItemView::ExtendedSelection); @@ -514,16 +518,32 @@ void MainWindow::buildUi() column, QHeaderView::Interactive); } - // The subject cell carries the account chip in front of its text, and - // every cell needs the delegate's selection handling: the read/unread - // dimming arrives as a Qt::ForegroundRole, which Qt's default painting - // prefers over the highlight, leaving a selected read row grey on the - // selection colour. SubjectDelegate::initStyleOption reverses that, and - // its paint() falls through to the base class wherever there is no chip, - // so the other columns keep their ordinary rendering. - m_threadView->setItemDelegate(new SubjectDelegate(this)); + // Two delegates, and the split is not cosmetic. RowStyleDelegate carries + // only the selection fix every column needs: the read/unread dimming + // arrives as a Qt::ForegroundRole, which Qt's painting prefers over the + // highlight, leaving a selected read row grey on the selection colour. + // + // SubjectDelegate adds the account chip and the tag pills, and must go on + // the subject column ALONE. It reads AccountLabelRole, a property of the + // row rather than of a cell, so installed view-wide it draws the chip into + // every column: tried once, and the list came out with a chip repeated + // four times per row. + m_threadView->setItemDelegate(new RowStyleDelegate(this)); + m_threadView->setItemDelegateForColumn(ThreadListModel::SubjectColumn, + new SubjectDelegate(this)); + + // One height for every row, set here rather than left to a column's + // sizeHint: a QTableView takes a single height per row, so a hint from the + // subject column alone would only apply if the view happened to ask it. + m_threadView->verticalHeader()->setDefaultSectionSize( + SubjectDelegate::rowHeightFor(m_threadView->font())); // Widening a column past the viewport scrolls rather than squeezing the // others. Per-pixel so the scroll does not jump a whole column at a time. + // Banding, so the eye can follow a row across four columns and a pill + // strip without losing it. The colour comes from the palette's + // AlternateBase, so it follows the desktop theme. + m_threadView->setAlternatingRowColors(true); + m_threadView->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); m_threadView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel); @@ -534,6 +554,7 @@ void MainWindow::buildUi() // clamps to it silently rather than reporting the smaller value back. m_threadView->horizontalHeader()->setMinimumSectionSize(24); m_threadView->setColumnWidth(ThreadListModel::AttachmentColumn, 28); + m_threadView->setColumnWidth(ThreadListModel::FlagColumn, 28); m_threadView->setColumnWidth(ThreadListModel::DateColumn, 130); m_threadView->setColumnWidth(ThreadListModel::AuthorsColumn, 180); m_threadView->setColumnWidth(ThreadListModel::SubjectColumn, 520); -- cgit v1.2.3