aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 16:15:57 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 16:15:57 +0200
commit3b771d03f2df572de29af3156817de3cb7ef6bef (patch)
treeefd17e55320e472ee27160d935034afe31add6e4 /src
parenta52e4e41c2bc8fc8eb23c4bc99f9b8724212bd23 (diff)
downloadqtmaildir-3b771d03f2df572de29af3156817de3cb7ef6bef.tar.gz
qtmaildir-3b771d03f2df572de29af3156817de3cb7ef6bef.zip
feat(rules): the rules window keeps its size and column widths
Item 75. saveGeometry() and the rule list header's saveState() go to uistate.conf under keys of their own, written on closeEvent so a size survives Cancel as well as Save. The 760x520 resize stays as the first-run fallback. The backlog's approach was wrong on one point and a test caught it. It said to drop the resizeColumnToContents calls once a saved header state exists, which fixes the restore and leaves the original defect standing: with nothing saved, a width the user had just dragged was still discarded by the next add or delete. Each column is instead auto-sized once, on its first fill, after which the width belongs to the user however it was set. Two flags, because the count column is filled later by a reply from the worker. The window stays a QDialog. Making it a top-level window needs the unsaved-edit story that being modal currently sidesteps, and that is its own decision rather than part of this item. Both tests redirect XDG_STATE_HOME as well as XDG_CONFIG_HOME, so they cannot write the real uistate.conf. The geometry is asserted on the stored value rather than the reopened frame, per item 46: the offscreen platform does not honour a resize. Also corrects setFolders' doc comment, which still described the folder list as coming from Config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src')
-rw-r--r--src/tagrulesdialog.cpp93
-rw-r--r--src/tagrulesdialog.h39
2 files changed, 126 insertions, 6 deletions
diff --git a/src/tagrulesdialog.cpp b/src/tagrulesdialog.cpp
index 75a9cbe..61f2c21 100644
--- a/src/tagrulesdialog.cpp
+++ b/src/tagrulesdialog.cpp
@@ -20,8 +20,11 @@
#include <QButtonGroup>
#include <QCheckBox>
+#include <QCloseEvent>
#include <QComboBox>
#include <QDialogButtonBox>
+#include <QDir>
+#include <QFileInfo>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QHeaderView>
@@ -31,10 +34,13 @@
#include <QPlainTextEdit>
#include <QPushButton>
#include <QRadioButton>
+#include <QSettings>
#include <QSpinBox>
#include <QTreeWidget>
#include <QVBoxLayout>
+#include "mainwindow.h"
+
namespace {
/// Columns of the rule list.
@@ -72,6 +78,9 @@ TagRulesDialog::TagRulesDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle(tr("Tagging rules"));
+ // The fallback for a first run. restoreUiState() overwrites it when a
+ // size was saved, and is called at the end of this constructor because
+ // the header state cannot be restored before the columns exist.
resize(760, 520);
m_rules.load();
@@ -244,6 +253,72 @@ TagRulesDialog::TagRulesDialog(QWidget *parent)
reloadList();
showWarnings();
+
+ // Last, and after reloadList(): a header state cannot be applied before
+ // the columns it describes exist, and reloadList is what fills them.
+ restoreUiState();
+}
+
+/// Reads the window size and the rule list's header layout back.
+///
+/// The same file MainWindow uses, under keys of its own. Machine-written
+/// state, never the hand-edited config: a column width is not something
+/// anyone edits by hand, and mixing the two puts a blob in a file the user
+/// reads.
+void TagRulesDialog::restoreUiState()
+{
+ QSettings state(MainWindow::uiStatePath(), QSettings::IniFormat);
+
+ const QByteArray geometry =
+ state.value(QStringLiteral("tagrules/geometry")).toByteArray();
+ if (!geometry.isEmpty())
+ restoreGeometry(geometry);
+
+ const QByteArray header =
+ state.value(QStringLiteral("tagrules/header")).toByteArray();
+ if (!header.isEmpty()) {
+ m_list->header()->restoreState(header);
+ // Counts as the one auto-size each column gets, so the restore is not
+ // immediately overwritten. reloadList() runs before this in the
+ // constructor and has already sized the first two; the count column
+ // has not been filled yet, and would otherwise resize over the
+ // restored width as soon as the first counts arrived.
+ m_columnsSized = true;
+ m_countColumnSized = true;
+ }
+}
+
+void TagRulesDialog::saveUiState()
+{
+ QDir().mkpath(QFileInfo(MainWindow::uiStatePath()).absolutePath());
+ QSettings state(MainWindow::uiStatePath(), QSettings::IniFormat);
+ state.setValue(QStringLiteral("tagrules/geometry"), saveGeometry());
+ state.setValue(QStringLiteral("tagrules/header"),
+ m_list->header()->saveState());
+}
+
+/// Saves on close rather than on accept, so a size the user chose is kept
+/// whether they pressed Save or Cancel. The window's shape is not part of the
+/// edit being confirmed.
+void TagRulesDialog::closeEvent(QCloseEvent *event)
+{
+ saveUiState();
+ QDialog::closeEvent(event);
+}
+
+int TagRulesDialog::columnWidthForTest(int column) const
+{
+ return m_list->columnWidth(column);
+}
+
+void TagRulesDialog::setColumnWidthForTest(int column, int width)
+{
+ m_list->setColumnWidth(column, width);
+}
+
+void TagRulesDialog::reloadListForTest()
+{
+ reloadList();
}
void TagRulesDialog::showWarnings()
@@ -286,8 +361,15 @@ void TagRulesDialog::reloadList()
auto *item = new QTreeWidgetItem(m_list);
fillItem(item, rule);
}
- m_list->resizeColumnToContents(ColumnEnabled);
- m_list->resizeColumnToContents(ColumnStage);
+ // Auto-sized on the FIRST fill only. After that the widths belong to the
+ // user, whether they came from a restored header or from a drag in this
+ // session, and resizing on every repopulate threw both away on the next
+ // add or delete.
+ if (!m_columnsSized) {
+ m_list->resizeColumnToContents(ColumnEnabled);
+ m_list->resizeColumnToContents(ColumnStage);
+ m_columnsSized = true;
+ }
m_reloading = false;
if (!m_working.isEmpty())
@@ -439,7 +521,12 @@ void TagRulesDialog::setCounts(const QVector<int> &counts)
counts.at(i) < 0 ? tr("invalid")
: QString::number(counts.at(i)));
}
- m_list->resizeColumnToContents(ColumnCount);
+ // Once, like the columns in reloadList. The counts arrive after the first
+ // fill, so this column gets its own flag rather than sharing that one.
+ if (!m_countColumnSized) {
+ m_list->resizeColumnToContents(ColumnCount);
+ m_countColumnSized = true;
+ }
}
void TagRulesDialog::onSave()
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index 9559918..f86d210 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -56,9 +56,14 @@ public:
/// handle and notmuch permits one per process.
QStringList countQueries() const;
- /// Account subdirectory names, for the Folder row's dropdown. Supplied by
- /// the caller rather than read here: Config knows them, and this dialog
- /// deliberately holds no Config of its own.
+ /// Maildir folder paths, for the Folder row's dropdown, relative to the
+ /// database root. Supplied by the caller rather than read here: they come
+ /// from a scan of the tree under notmuch's database.path, which only
+ /// NotmuchWorker can answer for, and this dialog reaches neither it nor
+ /// Config.
+ ///
+ /// Arrives AFTER the dialog is on screen, since the scan crosses to the
+ /// worker on a queued call, so this refills the rows that already exist.
void setFolders(const QStringList &folders);
/// Test seams. The builder's state is otherwise reachable only through
@@ -88,6 +93,20 @@ public:
/// when the widget is not.
bool textModeToggleIsReachableForTest() const;
+ /// Width of one rule-list column. The geometry restore is asserted on the
+ /// saved and reread VALUE rather than on the resulting frame: item 46
+ /// records that the offscreen platform does not honour a window resize,
+ /// so a test comparing frames there passes or fails for reasons that have
+ /// nothing to do with the code.
+ int columnWidthForTest(int column) const;
+ void setColumnWidthForTest(int column, int width);
+
+ /// Repopulates the rule list, as adding or deleting a rule does. Exposed
+ /// because a restored column width has to survive one of these, not only
+ /// a close and reopen: `resizeColumnToContents` on every reload discarded
+ /// the width the user had dragged.
+ void reloadListForTest();
+
signals:
/// Asks the owner to run countQueries() through the worker.
void countsRequested();
@@ -104,8 +123,13 @@ private slots:
void applyEditsToCurrentRule();
void onSave();
+protected:
+ void closeEvent(QCloseEvent *event) override;
+
private:
void reloadList();
+ void restoreUiState();
+ void saveUiState();
void showWarnings();
int currentIndex() const;
@@ -160,6 +184,15 @@ private:
/// row's text, which applyEditsToCurrentRule() has no chance to flush.
bool m_reloading = false;
+ /// Each column is auto-sized to its contents ONCE, on its first fill.
+ /// After that its width belongs to the user, whether it came from a
+ /// restored header or from a drag, and resizeColumnToContents on every
+ /// repopulate threw both away on the next add or delete. Two flags rather
+ /// than one because the count column is filled later than the rest, by a
+ /// reply from the worker.
+ bool m_columnsSized = false;
+ bool m_countColumnSized = false;
+
QTreeWidget *m_list = nullptr;
QLineEdit *m_id = nullptr;
QLineEdit *m_add = nullptr;