aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/carddelegate.cpp10
-rw-r--r--src/cardlayout.cpp31
-rw-r--r--src/cardlayout.h18
-rw-r--r--src/config.cpp25
-rw-r--r--src/config.h11
-rw-r--r--src/mainwindow.cpp1
-rw-r--r--src/threadlistmodel.cpp4
-rw-r--r--src/threadlistmodel.h12
8 files changed, 96 insertions, 16 deletions
diff --git a/src/carddelegate.cpp b/src/carddelegate.cpp
index bce9eec..3039853 100644
--- a/src/carddelegate.cpp
+++ b/src/carddelegate.cpp
@@ -36,6 +36,7 @@ CardLayout::Input inputFor(const QModelIndex &index)
in.isMessage = index.data(ThreadListModel::IsMessageRole).toBool();
in.depth = index.data(ThreadListModel::MessageDepthRole).toInt();
in.replyCount = index.data(ThreadListModel::ReplyCountRole).toInt();
+ in.dateFormat = index.data(ThreadListModel::DateFormatRole).toString();
return in;
}
@@ -172,8 +173,13 @@ void CardDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
Qt::ElideRight, card.senderRect.width()));
const QDateTime date =
index.data(ThreadListModel::DateRole).toDateTime();
- painter->drawText(card.dateRect, Qt::AlignVCenter | Qt::AlignRight,
- CardLayout::formatDate(date));
+ // The same format the layout reserved width from. Reading the role again
+ // rather than a second config lookup, so the drawn string and the rect it
+ // is drawn into cannot come from different patterns.
+ painter->drawText(
+ card.dateRect, Qt::AlignVCenter | Qt::AlignRight,
+ CardLayout::formatDate(
+ date, index.data(ThreadListModel::DateFormatRole).toString()));
// Line 2: the flag mark, the subject, the attachment mark.
QString subject = index.data(ThreadListModel::SubjectRole).toString();
diff --git a/src/cardlayout.cpp b/src/cardlayout.cpp
index f542df0..d60bff9 100644
--- a/src/cardlayout.cpp
+++ b/src/cardlayout.cpp
@@ -21,12 +21,16 @@
#include <QFontMetrics>
#include <QLocale>
-QString CardLayout::formatDate(const QDateTime &date)
+QString CardLayout::formatDate(const QDateTime &date, const QString &format)
{
- // The system locale's own short format, not a hardcoded pattern: an
- // Italian desktop writes 10/08/2025, not 2025-08-10, and a mail client
- // that disagrees with every other application on screen is simply wrong.
- return QLocale::system().toString(date, QLocale::ShortFormat);
+ // The system locale's own short format by default, not a hardcoded
+ // pattern: an Italian desktop writes 10/08/2025, not 2025-08-10, and a mail
+ // client that disagrees with every other application on screen is simply
+ // wrong. [general] date_format overrides it for a user who wants one
+ // specific shape regardless of the locale.
+ if (format.isEmpty())
+ return QLocale::system().toString(date, QLocale::ShortFormat);
+ return QLocale::system().toString(date, format);
}
QString CardLayout::expanderLabel(int replyCount, bool expanded)
@@ -45,17 +49,19 @@ QString CardLayout::expanderLabel(int replyCount, bool expanded)
return QStringLiteral("%1 %2 %3").arg(glyph).arg(replyCount).arg(word);
}
-QString CardLayout::widestDateSample()
+QString CardLayout::widestDateSample(const QString &format)
{
// A real date run through the same formatter, with the wide digits and a
// two-digit day and month, so the reserved width matches what is drawn
// whatever the locale's pattern turns out to be. Guessing a pattern here
// would reintroduce the clipping this exists to prevent.
- static const QString sample = [] {
- const QDateTime wide(QDate(2028, 12, 28), QTime(22, 58));
- return formatDate(wide);
- }();
- return sample;
+ //
+ // Not cached in a static any more: the sample depends on the format, and a
+ // single static computed for whichever format arrived first would reserve
+ // the system format's width for a custom pattern. The formatter is one
+ // QLocale call per row, which is the same cost the date itself already pays.
+ const QDateTime wide(QDate(2028, 12, 28), QTime(22, 58));
+ return formatDate(wide, format);
}
QFont CardLayout::smallFont(const QFont &cardFont)
@@ -145,7 +151,8 @@ CardLayout CardLayout::compute(const Input &input, const QRect &rect,
QFont dateFont = font;
dateFont.setBold(true);
const int dateWidth =
- QFontMetrics(dateFont).horizontalAdvance(widestDateSample());
+ QFontMetrics(dateFont).horizontalAdvance(
+ widestDateSample(input.dateFormat));
out.dateRect = QRect(right - dateWidth, lineOneTop, dateWidth,
metrics.height());
out.senderRect = QRect(out.contentLeft, lineOneTop,
diff --git a/src/cardlayout.h b/src/cardlayout.h
index ef6a563..1a2fd18 100644
--- a/src/cardlayout.h
+++ b/src/cardlayout.h
@@ -46,6 +46,16 @@ struct CardLayout
bool isMessage = false;
int depth = 0; ///< 0 for a thread root, 1 for a direct reply.
int replyCount = 0; ///< 0 means no expander.
+
+ /// A QDateTime::toString() pattern from [general] date_format, or empty
+ /// for the system's short format.
+ ///
+ /// It lives on the INPUT rather than being read where the date is
+ /// drawn, because the width reserved for the date is computed from the
+ /// same format inside compute(). A pattern reaching the painter but not
+ /// the geometry is exactly how a longer date gets elided into a rect
+ /// sized for a shorter one.
+ QString dateFormat;
};
/// Width of the account accent bar down a thread card's left edge.
@@ -129,10 +139,14 @@ struct CardLayout
/// drawn into it come from one place: a locale whose short format is
/// longer than the reserved rect would clip, which is exactly the fault
/// bold text produced.
- static QString formatDate(const QDateTime &date);
+ /// `format` is a QDateTime::toString() pattern, or empty for the system's
+ /// short format. Config validates it, so an unusable pattern never gets
+ /// this far.
+ static QString formatDate(const QDateTime &date,
+ const QString &format = QString());
/// The widest string formatDate() can return, for reserving space.
- static QString widestDateSample();
+ static QString widestDateSample(const QString &format = QString());
/// The expander's label: the reply count with its glyph, as drawn.
///
diff --git a/src/config.cpp b/src/config.cpp
index 4f5831c..9e223f8 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -23,7 +23,9 @@
// so this reports the same numbers rather than keeping a second copy.
#include "messageview.h"
+#include <QDateTime>
#include <QFileInfo>
+#include <QLocale>
#include <QSettings>
#include <QStandardPaths>
@@ -146,6 +148,29 @@ void Config::load(const QString &path)
}
}
+ // Absent or empty means the system locale's short format, which is what
+ // every other application on the desktop shows. Only a non-empty pattern is
+ // validated, and a rejected one falls back to that same default.
+ const QString dateFormat =
+ settings.value(QStringLiteral("date_format")).toString().trimmed();
+ if (!dateFormat.isEmpty()) {
+ // QDateTime::toString() with a pattern carrying no date or time field
+ // returns the pattern verbatim rather than failing, so "banana" would
+ // print "banana" on every card. Formatting two DIFFERENT instants and
+ // comparing is what catches that: a pattern with any real field gives
+ // two different strings, one with none gives the same string twice.
+ const QDateTime a(QDate(2028, 12, 28), QTime(22, 58));
+ const QDateTime b(QDate(2019, 1, 3), QTime(4, 5));
+ const QLocale locale = QLocale::system();
+ if (locale.toString(a, dateFormat) == locale.toString(b, dateFormat)) {
+ addProblem(QStringLiteral("Date format '%1' contains no date or "
+ "time field; using the system format.")
+ .arg(dateFormat));
+ } else {
+ m_dateFormat = dateFormat;
+ }
+ }
+
// Absent is silent, the default being 2000. Present but unparseable warns,
// for the same reason message_zoom does: the user asked for something and
// is not getting it.
diff --git a/src/config.h b/src/config.h
index 3dd9011..bcbfb34 100644
--- a/src/config.h
+++ b/src/config.h
@@ -114,6 +114,16 @@ public:
/// Optional alternate notmuch config file. Empty means "let notmuch decide".
QString notmuchConfig() const { return m_notmuchConfig; }
+ /// A QDateTime::toString() pattern for the date on a card, or empty for the
+ /// system locale's short format.
+ ///
+ /// Empty is both the default and what an unusable pattern falls back to, so
+ /// a caller never has to distinguish "unset" from "rejected": either way
+ /// the locale decides. Validated at load, because toString() with a pattern
+ /// carrying no date field returns the pattern verbatim, which would print
+ /// the same fixed string on every card rather than failing visibly.
+ QString dateFormat() const { return m_dateFormat; }
+
/// The saved query to open at startup, by name. Falls back to "Unread"
/// when unset, and to the first saved query when no query by that name
/// exists: [queries] is read through childKeys(), which sorts
@@ -185,6 +195,7 @@ private:
QString m_syncLog;
int m_toolbarIconSize = 24;
QString m_notmuchConfig;
+ QString m_dateFormat;
qreal m_messageZoom = 1.0;
bool m_completionOnFocus = false;
int m_markReadDelayMs = 2000;
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index d9eb989..374f228 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -558,6 +558,7 @@ void MainWindow::buildUi()
// Thread list and message pane.
m_model = new ThreadListModel(this);
m_model->setTagColors(&m_tagColors);
+ m_model->setDateFormat(m_config.dateFormat());
// 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.
diff --git a/src/threadlistmodel.cpp b/src/threadlistmodel.cpp
index 21a6378..33356f0 100644
--- a/src/threadlistmodel.cpp
+++ b/src/threadlistmodel.cpp
@@ -323,6 +323,8 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
// A reply never offers an expander: nesting past the first level is
// drawn from depth, not from further parent-child structure.
return 0;
+ case DateFormatRole:
+ return m_dateFormat;
case Qt::BackgroundRole:
// Tinted, so an expanded thread reads as one block rather than as
// more table rows. Applied per cell here; ThreadListView fills the
@@ -480,6 +482,8 @@ QVariant ThreadListModel::data(const QModelIndex &index, int role) const
case ReplyCountRole:
// totalCount includes the root message, which is the card itself.
return qMax(0, thread.totalCount - 1);
+ case DateFormatRole:
+ return m_dateFormat;
default:
break;
}
diff --git a/src/threadlistmodel.h b/src/threadlistmodel.h
index 01dd9d0..777841e 100644
--- a/src/threadlistmodel.h
+++ b/src/threadlistmodel.h
@@ -116,6 +116,14 @@ public:
HasAttachmentRole, ///< bool
IsFlaggedRole, ///< bool
ReplyCountRole, ///< int; 0 when a thread has no replies.
+
+ /// The [general] date_format pattern, or empty for the system's short
+ /// format. Same row value for every row.
+ ///
+ /// Supplied by the model for the same reason as PillColoursRole: it is
+ /// the one thing here that holds config, and a delegate reading config
+ /// itself would be a second source of truth.
+ DateFormatRole,
};
/// The mark drawn on a card's second line when the message has an
@@ -159,6 +167,9 @@ public:
/// Without one, chips fall back to a colour generated from the tag name.
void setTagColors(const TagColors *colours) { m_tagColors = colours; }
+ /// The pattern DateFormatRole answers with. Empty means the system format.
+ void setDateFormat(const QString &format) { m_dateFormat = format; }
+
QModelIndex index(int row, int column,
const QModelIndex &parent = {}) const override;
QModelIndex parent(const QModelIndex &child) const override;
@@ -272,4 +283,5 @@ private:
QVector<ThreadNode> m_threads;
const TagColors *m_tagColors = nullptr;
+ QString m_dateFormat;
};