diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mainwindow.cpp | 19 | ||||
| -rw-r--r-- | src/threadlistmodel.cpp | 35 | ||||
| -rw-r--r-- | src/threadlistmodel.h | 10 | ||||
| -rw-r--r-- | src/types.h | 7 |
4 files changed, 69 insertions, 2 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2a79988..1385f01 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -100,9 +100,18 @@ void MainWindow::restoreUiState() m_splitter->restoreState(splitter); } + // A header blob saved against a different set of columns must be + // discarded, not restored. QHeaderView::restoreState() returns TRUE for a + // blob with fewer sections than the model and applies the old widths to + // the wrong columns: adding the attachment column in front shifted every + // saved width one place right, silently mangling the layout with no error + // to detect it by (verified on Qt 6.11). The column count is stored + // alongside and the blob is only used when it still matches. const QByteArray header = state.value(QStringLiteral("threadlist/header")) .toByteArray(); - if (!header.isEmpty()) { + const int savedColumns = + state.value(QStringLiteral("threadlist/columns")).toInt(); + if (!header.isEmpty() && savedColumns == ThreadListModel::ColumnCount) { m_threadView->horizontalHeader()->restoreState(header); } @@ -123,6 +132,9 @@ void MainWindow::saveUiState() const state.setValue(QStringLiteral("window/splitter"), m_splitter->saveState()); state.setValue(QStringLiteral("threadlist/header"), m_threadView->horizontalHeader()->saveState()); + // Guards the blob above: see restoreUiState(). + state.setValue(QStringLiteral("threadlist/columns"), + int(ThreadListModel::ColumnCount)); state.setValue(QStringLiteral("message/zoom"), m_messageView->zoomFactor()); } @@ -275,6 +287,11 @@ void MainWindow::buildUi() // Starting widths only; a drag overrides them, and they are what the // saved-widths item will persist. + // Without this the attachment column cannot be narrow at all: the default + // minimum section size is 58px on this platform, and setColumnWidth() + // 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::DateColumn, 130); m_threadView->setColumnWidth(ThreadListModel::AuthorsColumn, 180); m_threadView->setColumnWidth(ThreadListModel::SubjectColumn, 520); diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp index 2f2882e..2289e6c 100644 --- a/src/threadlistmodel.cpp +++ b/src/threadlistmodel.cpp @@ -20,6 +20,27 @@ #include <QBrush> #include <QFont> +#include <QFontDatabase> +#include <QFontMetrics> + +QString ThreadListModel::attachmentGlyph() +{ + // U+1F4CE PAPERCLIP, with a fallback for a system whose default font + // cannot draw it: an unrenderable codepoint shows as a tofu box, which + // reads as "something is broken" rather than "this has an attachment". + // Computed once; the font does not change under a running application. + static const QString glyph = [] { + const char32_t paperclip = 0x1F4CE; + const QString preferred = QString::fromUcs4(&paperclip, 1); + const QFontMetrics metrics{QFontDatabase::systemFont( + QFontDatabase::GeneralFont)}; + // "*" as the fallback: ASCII, present in every practical font, and + // unambiguous in a column that shows nothing else. + return metrics.inFontUcs4(paperclip) ? preferred + : QStringLiteral("*"); + }(); + return glyph; +} QColor ThreadListModel::deletedColour() { @@ -85,8 +106,19 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const return {}; } + if (role == Qt::ToolTipRole && index.column() == AttachmentColumn) + return thread.hasAttachment() ? tr("Has an attachment") : QVariant(); + + if (role == Qt::TextAlignmentRole && index.column() == AttachmentColumn) + return QVariant::fromValue(Qt::AlignCenter); + if (role == Qt::DisplayRole) { switch (index.column()) { + case AttachmentColumn: + // A glyph rather than an icon resource: no new asset to ship, and + // it inherits the row's font, so it strikes through with a doomed + // thread like every other cell. + return thread.hasAttachment() ? attachmentGlyph() : QString(); case DateColumn: return thread.date.toString(QStringLiteral("yyyy-MM-dd hh:mm")); case AuthorsColumn: @@ -141,6 +173,9 @@ QVariant ThreadListModel::headerData(int section, Qt::Orientation orientation, return {}; switch (section) { + // No label: any text would set a minimum width far wider than the icon, + // which defeats the point of a narrow column. + case AttachmentColumn: return QString(); case DateColumn: return QStringLiteral("Date"); case AuthorsColumn: return QStringLiteral("From"); case SubjectColumn: return QStringLiteral("Subject"); diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h index 7ed8fef..ab9378e 100644 --- a/src/threadlistmodel.h +++ b/src/threadlistmodel.h @@ -36,7 +36,11 @@ public: /// under the message pane, and the account tag renders as a chip in front /// of the subject. enum Column { - DateColumn = 0, + /// A paperclip when the thread has an attachment, so it is visible + /// without opening the thread. Icon only and deliberately narrow; + /// it carries no text. + AttachmentColumn = 0, + DateColumn, AuthorsColumn, SubjectColumn, ColumnCount, @@ -64,6 +68,10 @@ public: /// 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. + /// The character shown in AttachmentColumn for a thread that has one. + /// A paperclip when the system font can draw it, "*" otherwise. + static QString attachmentGlyph(); + static QColor deletedColour(); static QColor spamColour(); diff --git a/src/types.h b/src/types.h index e25c3a9..59d1d06 100644 --- a/src/types.h +++ b/src/types.h @@ -38,6 +38,13 @@ struct ThreadSummary bool isDeleted() const { return tags.contains(QStringLiteral("deleted")); } bool isSpam() const { return tags.contains(QStringLiteral("spam")); } + /// notmuch applies "attachment" itself while indexing, so this needs no + /// MIME parsing and no extra worker query: the tag is already in tags. + bool hasAttachment() const + { + return tags.contains(QStringLiteral("attachment")); + } + /// 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. |
