aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp19
-rw-r--r--src/threadlistmodel.cpp35
-rw-r--r--src/threadlistmodel.h10
-rw-r--r--src/types.h7
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.