summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/threadlistmodel.cpp124
-rw-r--r--src/threadlistmodel.h50
3 files changed, 175 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 5198429..5f01128 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -6,6 +6,7 @@ add_library(qtmaildir_lib STATIC
htmlbuilder.cpp
cidschemehandler.cpp
notmuchworker.cpp
+ threadlistmodel.cpp
)
target_include_directories(qtmaildir_lib
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
new file mode 100644
index 0000000..5acfe0e
--- /dev/null
+++ b/src/threadlistmodel.cpp
@@ -0,0 +1,124 @@
+#include "threadlistmodel.h"
+
+#include <QFont>
+
+ThreadListModel::ThreadListModel(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+}
+
+int ThreadListModel::rowCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : m_threads.size();
+}
+
+int ThreadListModel::columnCount(const QModelIndex &parent) const
+{
+ return parent.isValid() ? 0 : ColumnCount;
+}
+
+QVariant ThreadListModel::data(const QModelIndex &index, int role) const
+{
+ // A stale index from a view that has not caught up with a clear() can carry
+ // any row or column, so both bounds are checked rather than trusted.
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_threads.size()
+ || index.column() < 0 || index.column() >= ColumnCount) {
+ return {};
+ }
+
+ const ThreadSummary &thread = m_threads.at(index.row());
+
+ if (role == ThreadIdRole)
+ return thread.threadId;
+
+ if (role == Qt::DisplayRole) {
+ switch (index.column()) {
+ case DateColumn:
+ return thread.date.toString(QStringLiteral("yyyy-MM-dd hh:mm"));
+ case AuthorsColumn:
+ return thread.authors;
+ case SubjectColumn:
+ return thread.totalCount > 1
+ ? QStringLiteral("%1 (%2)").arg(thread.subject)
+ .arg(thread.totalCount)
+ : thread.subject;
+ case TagsColumn:
+ return thread.tags.join(QLatin1Char(' '));
+ default:
+ return {};
+ }
+ }
+
+ if (role == Qt::FontRole && thread.isUnread()) {
+ QFont font;
+ font.setBold(true);
+ return font;
+ }
+
+ return {};
+}
+
+QVariant ThreadListModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
+ return {};
+
+ switch (section) {
+ case DateColumn: return QStringLiteral("Date");
+ case AuthorsColumn: return QStringLiteral("From");
+ case SubjectColumn: return QStringLiteral("Subject");
+ case TagsColumn: return QStringLiteral("Tags");
+ default: return {};
+ }
+}
+
+void ThreadListModel::appendBatch(const QVector<ThreadSummary> &batch)
+{
+ // beginInsertRows with an empty range violates Qt's contract, so the guard
+ // has to come before the signal, not inside it.
+ if (batch.isEmpty())
+ return;
+
+ const int first = m_threads.size();
+ beginInsertRows({}, first, first + batch.size() - 1);
+ m_threads.append(batch);
+ endInsertRows();
+}
+
+void ThreadListModel::clear()
+{
+ beginResetModel();
+ m_threads.clear();
+ endResetModel();
+}
+
+ThreadSummary ThreadListModel::threadAt(int row) const
+{
+ if (row < 0 || row >= m_threads.size())
+ return {};
+ return m_threads.at(row);
+}
+
+void ThreadListModel::applyTagChange(const QString &threadId,
+ const QStringList &added,
+ const QStringList &removed)
+{
+ for (int row = 0; row < m_threads.size(); ++row) {
+ if (m_threads.at(row).threadId != threadId)
+ continue;
+
+ QStringList &tags = m_threads[row].tags;
+ for (const QString &tag : removed)
+ tags.removeAll(tag);
+ for (const QString &tag : added) {
+ if (!tags.contains(tag))
+ tags.append(tag);
+ }
+
+ // The whole row repaints: unread state drives the font of every column,
+ // not just the tags one.
+ emit dataChanged(index(row, 0), index(row, ColumnCount - 1));
+ return;
+ }
+}
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
new file mode 100644
index 0000000..2cf8d2e
--- /dev/null
+++ b/src/threadlistmodel.h
@@ -0,0 +1,50 @@
+#pragma once
+
+#include <QAbstractTableModel>
+#include <QVector>
+
+#include "types.h"
+
+/// Table model over query results, filled in batches so a large query paints
+/// its first screenful immediately.
+class ThreadListModel : public QAbstractTableModel
+{
+ Q_OBJECT
+public:
+ enum Column {
+ DateColumn = 0,
+ AuthorsColumn,
+ SubjectColumn,
+ TagsColumn,
+ ColumnCount,
+ };
+
+ enum Role {
+ /// The thread id behind a row. Views hand out QModelIndexes, but the
+ /// worker speaks thread ids, so the mapping belongs on the model
+ /// rather than in every caller.
+ ThreadIdRole = Qt::UserRole + 1,
+ };
+
+ explicit ThreadListModel(QObject *parent = nullptr);
+
+ int rowCount(const QModelIndex &parent = {}) const override;
+ int columnCount(const QModelIndex &parent = {}) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role) const override;
+
+ void appendBatch(const QVector<ThreadSummary> &batch);
+ void clear();
+
+ ThreadSummary threadAt(int row) const;
+
+ /// Applies a tag change locally so the UI updates before the worker
+ /// confirms. To revert a failed write, call again with added and removed
+ /// swapped.
+ void applyTagChange(const QString &threadId, const QStringList &added,
+ const QStringList &removed);
+
+private:
+ QVector<ThreadSummary> m_threads;
+};