summaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-03 16:33:13 +0200
committerDanilo M. <danix@danix.xyz>2026-08-03 16:33:13 +0200
commitea90e69d5e974960c653e65a5bb9ca1359f2d52c (patch)
tree87410a035212f0e8e5997722662803c17a3320d8 /src/mainwindow.cpp
parentec390fb46e1a36d8406dde487228bbb0f348a20a (diff)
parent2c33529fb665cb54c31e54230fcb7b2491cf8565 (diff)
downloadqtmaildir-ea90e69d5e974960c653e65a5bb9ca1359f2d52c.tar.gz
qtmaildir-ea90e69d5e974960c653e65a5bb9ca1359f2d52c.zip
Merge branch 'feature/ui-state-persistence'
Persistence cluster from the post-0.1.0 usability backlog: window, splitter and column geometry survive restart, the message pane owns its zoom and remembers it, and the startup query is chosen by name instead of by alphabetical accident. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp122
1 files changed, 115 insertions, 7 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 48b475c..2a79988 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -19,9 +19,12 @@
#include "mainwindow.h"
#include <QAction>
+#include <QCloseEvent>
#include <QComboBox>
#include <QDialog>
#include <QDialogButtonBox>
+#include <QDir>
+#include <QFileInfo>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
@@ -33,6 +36,7 @@
#include <QPushButton>
#include <QSettings>
#include <QSplitter>
+#include <QStandardPaths>
#include <QStatusBar>
#include <QTableView>
#include <QToolBar>
@@ -61,6 +65,73 @@ QString MainWindow::cidPrefixForIndex(int index)
return QStringLiteral("m%1").arg(index);
}
+QString MainWindow::uiStatePath()
+{
+ // GenericStateLocation, not StateLocation: the latter appends both the
+ // organization and the application name, and both are "qtmaildir" here,
+ // so it yields ~/.local/state/qtmaildir/qtmaildir. Built the same way
+ // Config::defaultPath() builds its own.
+ const QString base =
+ QStandardPaths::writableLocation(QStandardPaths::GenericStateLocation);
+ return base + QStringLiteral("/qtmaildir/uistate.conf");
+}
+
+void MainWindow::restoreUiState()
+{
+ QSettings state(uiStatePath(), QSettings::IniFormat);
+
+ // Every restore is conditional: an absent or rejected blob must leave the
+ // buildUi() defaults alone rather than produce a zero-size window.
+ const QByteArray geometry = state.value(QStringLiteral("window/geometry"))
+ .toByteArray();
+ if (!geometry.isEmpty()) {
+ restoreGeometry(geometry);
+ }
+
+ const QByteArray windowState = state.value(QStringLiteral("window/state"))
+ .toByteArray();
+ if (!windowState.isEmpty()) {
+ restoreState(windowState);
+ }
+
+ const QByteArray splitter = state.value(QStringLiteral("window/splitter"))
+ .toByteArray();
+ if (!splitter.isEmpty()) {
+ m_splitter->restoreState(splitter);
+ }
+
+ const QByteArray header = state.value(QStringLiteral("threadlist/header"))
+ .toByteArray();
+ if (!header.isEmpty()) {
+ m_threadView->horizontalHeader()->restoreState(header);
+ }
+
+ // The config value is the starting point for a profile that has never
+ // zoomed; once the user does, the state file is what they last had.
+ // clampZoom() rejects the garbage a hand-edited file can hold.
+ m_messageView->setZoomFactor(
+ state.value(QStringLiteral("message/zoom"), m_config.messageZoom())
+ .toDouble());
+}
+
+void MainWindow::saveUiState() const
+{
+ QDir().mkpath(QFileInfo(uiStatePath()).absolutePath());
+ QSettings state(uiStatePath(), QSettings::IniFormat);
+ state.setValue(QStringLiteral("window/geometry"), saveGeometry());
+ state.setValue(QStringLiteral("window/state"), saveState());
+ state.setValue(QStringLiteral("window/splitter"), m_splitter->saveState());
+ state.setValue(QStringLiteral("threadlist/header"),
+ m_threadView->horizontalHeader()->saveState());
+ state.setValue(QStringLiteral("message/zoom"), m_messageView->zoomFactor());
+}
+
+void MainWindow::closeEvent(QCloseEvent *event)
+{
+ saveUiState();
+ QMainWindow::closeEvent(event);
+}
+
MainWindow::MainWindow(const Config &config, QWidget *parent)
: QMainWindow(parent), m_config(config)
{
@@ -87,6 +158,9 @@ MainWindow::MainWindow(const Config &config, QWidget *parent)
buildUi();
registerActions();
buildMenus();
+ // After buildMenus(): QMainWindow::restoreState() matches toolbars by
+ // object name, so they must already exist or their position is dropped.
+ restoreUiState();
wireWorker();
showWarnings();
@@ -97,8 +171,12 @@ MainWindow::MainWindow(const Config &config, QWidget *parent)
// modifier shortcuts such as Ctrl+Q still work there, which the old
// filter blocked.
- if (!m_config.savedQueries().isEmpty()) {
- m_queryEdit->setText(m_config.savedQueries().first().query);
+ // Not savedQueries().first(): [queries] is read through childKeys(), which
+ // sorts alphabetically, so "first" means whatever happens to sort first
+ // rather than anything the user chose. Config resolves the name.
+ const SavedQuery startup = m_config.startupSavedQuery();
+ if (!startup.query.isEmpty()) {
+ m_queryEdit->setText(startup.query);
runCurrentQuery();
}
}
@@ -210,11 +288,11 @@ void MainWindow::buildUi()
connect(m_messageView, &MessageView::statusMessage,
this, [this](const QString &text) { m_statusLabel->setText(text); });
- auto *splitter = new QSplitter(Qt::Horizontal, central);
- splitter->addWidget(m_threadView);
- splitter->addWidget(m_messageView);
- splitter->setStretchFactor(1, 2);
- layout->addWidget(splitter, 1);
+ m_splitter = new QSplitter(Qt::Horizontal, central);
+ m_splitter->addWidget(m_threadView);
+ m_splitter->addWidget(m_messageView);
+ m_splitter->setStretchFactor(1, 2);
+ layout->addWidget(m_splitter, 1);
layout->addWidget(m_syncLog);
@@ -315,6 +393,32 @@ void MainWindow::registerActions()
tr("Load remote images for the current thread"), [this]() {
m_messageView->loadRemoteContent();
});
+ addAction(QStringLiteral("zoom_in"), tr("Zoom &in"),
+ tr("Enlarge the message text"), [this]() {
+ m_messageView->zoomIn();
+ });
+ addAction(QStringLiteral("zoom_out"), tr("Zoom &out"),
+ tr("Shrink the message text"), [this]() {
+ m_messageView->zoomOut();
+ });
+ auto *zoomReset =
+ addAction(QStringLiteral("zoom_reset"), tr("&Actual size"),
+ tr("Return the message text to its default size"), [this]() {
+ m_messageView->zoomReset();
+ });
+
+ // Ctrl+= alongside the configured binding: '=' reads as "back to normal",
+ // and on a layout where '+' is Shift+'=' it is the unshifted key next to
+ // zoom in. Appended rather than assigned, so a [keys] override of
+ // zoom_reset keeps working and simply gains this as a second way in.
+ // A user who bound Ctrl+= to something else in [keys] keeps their binding.
+ const QKeySequence altReset(QStringLiteral("Ctrl+="));
+ if (m_keyMap.actionFor(altReset).isEmpty()) {
+ QList<QKeySequence> shortcuts = zoomReset->shortcuts();
+ shortcuts.append(altReset);
+ zoomReset->setShortcuts(shortcuts);
+ }
+
addAction(QStringLiteral("undo"), tr("&Undo"),
tr("Undo the last tag change"), [this]() {
if (m_undoStack.canUndo())
@@ -362,6 +466,10 @@ void MainWindow::buildMenus()
viewMenu->addSeparator();
viewMenu->addAction(m_actions.value(QStringLiteral("toggle_html")));
viewMenu->addAction(m_actions.value(QStringLiteral("load_remote")));
+ viewMenu->addSeparator();
+ viewMenu->addAction(m_actions.value(QStringLiteral("zoom_in")));
+ viewMenu->addAction(m_actions.value(QStringLiteral("zoom_out")));
+ viewMenu->addAction(m_actions.value(QStringLiteral("zoom_reset")));
auto *helpMenu = menuBar()->addMenu(tr("&Help"));
auto *shortcuts = helpMenu->addAction(tr("&Keyboard shortcuts"));