diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-03 14:57:11 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-03 14:57:11 +0200 |
| commit | 1371a3901857a8d2ac4fcf235fb29e7caaf63cae (patch) | |
| tree | ef8c08db29a7ce6b4052e6533c4f66b8f5a258bf /src | |
| parent | 95d0d19a4c1323a4839c1544f3bc1c0323f85dd9 (diff) | |
| download | qtmaildir-1371a3901857a8d2ac4fcf235fb29e7caaf63cae.tar.gz qtmaildir-1371a3901857a8d2ac4fcf235fb29e7caaf63cae.zip | |
feat: show that a tag action landed
Selecting a thread and hitting Delete changed nothing on screen, so
there was no way to tell the action had stuck.
The tag was always applied: applyTagChange() emitted dataChanged across
the row, and the Tags column did update. But Subject was set to stretch
while Tags came after it, so Subject took all free width and pushed Tags
out of view. The feedback lived in the one column that could not be seen.
Columns are now Tags, Date, From, Subject, with Subject stretching last
so nothing can be pushed off the right edge. A thread tagged deleted or
spam fills its whole row, muted red or orange with white struck-through
text, through the background, foreground and font roles, so no cue
depends on one column remaining visible.
Strike-through accompanies the fill on purpose: it survives a theme that
overrides backgrounds and reads without colour. Bold for unread still
composes with it.
Archive adds no tag, so an archived row is left unstyled for now.
Diffstat (limited to 'src')
| -rw-r--r-- | src/mainwindow.cpp | 8 | ||||
| -rw-r--r-- | src/threadlistmodel.cpp | 46 | ||||
| -rw-r--r-- | src/threadlistmodel.h | 15 | ||||
| -rw-r--r-- | src/types.h | 7 |
4 files changed, 71 insertions, 5 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index eaf98b8..915c1ae 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -169,8 +169,16 @@ void MainWindow::buildUi() m_threadView->setSelectionMode(QAbstractItemView::ExtendedSelection); m_threadView->verticalHeader()->hide(); m_threadView->horizontalHeader()->setStretchLastSection(false); + // Subject is the last column and takes the leftover width; the three + // fixed-width ones size to their contents. Nothing sits to the right of + // the stretching column, so no column can be pushed out of view. m_threadView->horizontalHeader()->setSectionResizeMode( ThreadListModel::SubjectColumn, QHeaderView::Stretch); + for (int column : { ThreadListModel::TagsColumn, ThreadListModel::DateColumn, + ThreadListModel::AuthorsColumn }) { + m_threadView->horizontalHeader()->setSectionResizeMode( + column, QHeaderView::ResizeToContents); + } connect(m_threadView->selectionModel(), &QItemSelectionModel::currentRowChanged, diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp index c129be1..a083145 100644 --- a/src/threadlistmodel.cpp +++ b/src/threadlistmodel.cpp @@ -18,8 +18,24 @@ #include "threadlistmodel.h" +#include <QBrush> #include <QFont> +QColor ThreadListModel::deletedColour() +{ + // Desaturated crimson: legible under white text on a dark theme, and calm + // enough that deleting fifty threads does not repaint the list as a + // warning banner. + return QColor(0x8b, 0x2c, 0x2c); +} + +QColor ThreadListModel::spamColour() +{ + // Distinct hue rather than a lighter red, so spam and deleted are told + // apart by colour and not by shade. + return QColor(0xa8, 0x5c, 0x18); +} + ThreadListModel::ThreadListModel(QObject *parent) : QAbstractTableModel(parent) { @@ -67,10 +83,34 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const } } - if (role == Qt::FontRole && thread.isUnread()) { + // A thread tagged deleted or spam is on its way out, and the user needs to + // see that the moment they act. Every one of these roles applies to the + // whole row: a cue on a single column disappears as soon as that column + // scrolls out of view, which is exactly how the tag change used to go + // unnoticed. + if (thread.isDoomed()) { + if (role == Qt::BackgroundRole) + return QBrush(thread.isDeleted() ? deletedColour() : spamColour()); + if (role == Qt::ForegroundRole) + return QBrush(QColor(Qt::white)); + } + + if (role == Qt::FontRole) { QFont font; - font.setBold(true); - return font; + bool styled = false; + if (thread.isUnread()) { + font.setBold(true); + styled = true; + } + // Struck through as well as filled, so the state survives a + // screenshot, a colourblind reader, and a theme that overrides the + // background. + if (thread.isDoomed()) { + font.setStrikeOut(true); + styled = true; + } + if (styled) + return font; } return {}; diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h index eb1a7ff..01fd241 100644 --- a/src/threadlistmodel.h +++ b/src/threadlistmodel.h @@ -19,6 +19,7 @@ #pragma once #include <QAbstractTableModel> +#include <QColor> #include <QVector> #include "types.h" @@ -29,11 +30,14 @@ class ThreadListModel : public QAbstractTableModel { Q_OBJECT public: + /// Subject stretches to fill the view, so it must come last: anything + /// after it is pushed out of sight. Tags leads, being the column that + /// changes when the user acts on a thread. enum Column { - DateColumn = 0, + TagsColumn = 0, + DateColumn, AuthorsColumn, SubjectColumn, - TagsColumn, ColumnCount, }; @@ -44,6 +48,13 @@ public: ThreadIdRole = Qt::UserRole + 1, }; + /// Row fill for a thread tagged `deleted`, and for one tagged `spam`. + /// Muted rather than saturated: a bulk delete paints every selected row, + /// and a wall of pure red is harder to read than the list it replaces. + /// Exposed so a test names the same colour the model uses. + static QColor deletedColour(); + static QColor spamColour(); + explicit ThreadListModel(QObject *parent = nullptr); int rowCount(const QModelIndex &parent = {}) const override; diff --git a/src/types.h b/src/types.h index 2de6129..e25c3a9 100644 --- a/src/types.h +++ b/src/types.h @@ -35,6 +35,13 @@ struct ThreadSummary bool isUnread() const { return tags.contains(QStringLiteral("unread")); } bool isFlagged() const { return tags.contains(QStringLiteral("flagged")); } + bool isDeleted() const { return tags.contains(QStringLiteral("deleted")); } + bool isSpam() const { return tags.contains(QStringLiteral("spam")); } + + /// True while the thread is tagged for removal. notmuch deletes nothing + /// itself: the tag marks the thread for whatever the user's sync script + /// does next, so the row has to show it is on its way out. + bool isDoomed() const { return isDeleted() || isSpam(); } }; struct MessageRef |
