aboutsummaryrefslogtreecommitdiffstats
path: root/src/savequerydialog.cpp
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 20:16:43 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 20:16:43 +0200
commitc9eb6fc86c947aea0075c1e9d279695cd9f9b7ee (patch)
tree7e5339fbb7e71d29498405f9d4b1c4188f5ab4c3 /src/savequerydialog.cpp
parentf389db3aad498d46c95c2a95b4280ffb541043b2 (diff)
parent9be1b13b91188cf44a40c6786a83de034988cdbd (diff)
downloadqtmaildir-c9eb6fc86c947aea0075c1e9d279695cd9f9b7ee.tar.gz
qtmaildir-c9eb6fc86c947aea0075c1e9d279695cd9f9b7ee.zip
Merge branch 'saved-queries': saved queries in a file of their own
Items 23 and 82. Saved queries move out of the [queries] INI section into ~/.config/qtmaildir/queries.json, gaining the three things the INI could not express: an order, a pinned flag, and a per-query account scope. They can now be created, edited, reordered, unpinned and deleted from the UI rather than only by hand-editing a config file. The INI could not carry order at all: QSettings reads a section through childKeys(), which sorts alphabetically and never follows the file, so the buttons could not be arranged. Migration reads the old section once, marks every entry pinned so nothing moves on the first launch, and leaves the config file byte-identical, since rewriting it with QSettings would drop the user's comments and key order. Sent stops being a hardcoded button and becomes an ordinary entry carrying "generated": "sent". Its query is still composed from the accounts every time it runs, so correcting a folder name still needs no edit here, but the row now follows one rule instead of having one member the user could not move, rename or remove. Item 82 was found by hand-testing item 23 and fixed on the same branch: saving worked and nothing else did, so a saved query could be created and never changed. It also uncovered a defect that predated it, where rebuilding the row with deleteLater() left the stale row answering findChild(), which was already reachable from the save path. Hand-tested throughout: the migration against the real config, saving, unpinning, deleting, reordering, the read-only query field on a generated entry, and an unpin surviving a restart.
Diffstat (limited to 'src/savequerydialog.cpp')
-rw-r--r--src/savequerydialog.cpp174
1 files changed, 174 insertions, 0 deletions
diff --git a/src/savequerydialog.cpp b/src/savequerydialog.cpp
new file mode 100644
index 0000000..2d09986
--- /dev/null
+++ b/src/savequerydialog.cpp
@@ -0,0 +1,174 @@
+/*
+ * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "savequerydialog.h"
+
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QFormLayout>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QVBoxLayout>
+
+bool SaveQueryDialog::namesAnExistingQuery(const Config &config,
+ const QString &name)
+{
+ for (const SavedQuery &saved : config.savedQueries()) {
+ if (saved.name.compare(name.trimmed(), Qt::CaseInsensitive) == 0)
+ return true;
+ }
+ return false;
+}
+
+SaveQueryDialog::SaveQueryDialog(const Config &config, const QString &query,
+ const QString &accountKey, QWidget *parent)
+ : QDialog(parent)
+ , m_config(config)
+{
+ SavedQuery initial;
+ initial.query = query;
+ initial.account = accountKey;
+ initial.pinned = true;
+ setWindowTitle(tr("Save query"));
+ build(initial);
+}
+
+SaveQueryDialog::SaveQueryDialog(const Config &config,
+ const SavedQuery &existing, QWidget *parent)
+ : QDialog(parent)
+ , m_config(config)
+ , m_originalName(existing.name)
+ , m_generated(existing.generated)
+ , m_flat(existing.flat)
+{
+ setWindowTitle(tr("Edit saved query"));
+ build(existing);
+}
+
+void SaveQueryDialog::build(const SavedQuery &initial)
+{
+ auto *layout = new QVBoxLayout(this);
+ auto *form = new QFormLayout;
+
+ m_name = new QLineEdit(initial.name, this);
+ m_name->setObjectName(QStringLiteral("saveQueryName"));
+ m_name->setPlaceholderText(tr("A name for this query"));
+ form->addRow(tr("Name"), m_name);
+
+ m_query = new QLineEdit(initial.query, this);
+ m_query->setObjectName(QStringLiteral("saveQueryQuery"));
+ if (initial.isGenerated()) {
+ // A generated entry has no stored query: it is composed from the
+ // accounts every time it runs. Shown, so the user can see what it will
+ // do, but read-only, since editing it would change nothing.
+ m_query->setText(m_config.resolvedQuery(initial));
+ m_query->setReadOnly(true);
+ m_query->setToolTip(tr("Built from your accounts and not editable. "
+ "It follows the sent folder each account "
+ "configures."));
+ form->addRow(tr("Query"), m_query);
+ } else {
+ form->addRow(tr("Query"), m_query);
+ }
+
+ // The scope is stored as an account KEY, so the entries carry the key as
+ // data exactly as the main window's dropdown does. "All accounts" is the
+ // empty key, not a missing entry, so an unscoped query is a real choice
+ // rather than the absence of one.
+ m_account = new QComboBox(this);
+ m_account->setObjectName(QStringLiteral("saveQueryAccount"));
+ m_account->addItem(tr("All accounts"), QString());
+ for (const Account &account : m_config.accounts())
+ m_account->addItem(account.key, account.key);
+ const int index = m_account->findData(initial.account);
+ m_account->setCurrentIndex(index >= 0 ? index : 0);
+ form->addRow(tr("Account"), m_account);
+
+ m_pinned = new QCheckBox(tr("Show as a button"), this);
+ m_pinned->setObjectName(QStringLiteral("saveQueryPinned"));
+ m_pinned->setChecked(initial.pinned);
+ form->addRow(QString(), m_pinned);
+
+ layout->addLayout(form);
+
+ // Says what is about to happen rather than refusing the name. Overwriting
+ // a saved query on purpose is a normal edit, and the only thing worth
+ // preventing is doing it without noticing.
+ m_notice = new QLabel(this);
+ m_notice->setObjectName(QStringLiteral("saveQueryNotice"));
+ m_notice->setWordWrap(true);
+ layout->addWidget(m_notice);
+
+ auto *buttons = new QDialogButtonBox(
+ QDialogButtonBox::Save | QDialogButtonBox::Cancel, this);
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ layout->addWidget(buttons);
+
+ m_ok = buttons->button(QDialogButtonBox::Save);
+ m_ok->setObjectName(QStringLiteral("saveQueryOk"));
+
+ connect(m_name, &QLineEdit::textChanged,
+ this, &SaveQueryDialog::updateOkState);
+ connect(m_query, &QLineEdit::textChanged,
+ this, &SaveQueryDialog::updateOkState);
+ updateOkState();
+
+ m_name->setFocus();
+}
+
+void SaveQueryDialog::updateOkState()
+{
+ const QString name = m_name->text().trimmed();
+ const bool usable = !name.isEmpty() && !m_query->text().trimmed().isEmpty();
+ m_ok->setEnabled(usable);
+
+ // Ignores the entry being edited: warning that "Inbox" already exists
+ // while editing Inbox is noise, and the real case worth catching is a
+ // rename onto a name something else already holds.
+ const bool isItsOwnName =
+ !m_originalName.isEmpty()
+ && name.compare(m_originalName, Qt::CaseInsensitive) == 0;
+ if (!name.isEmpty() && !isItsOwnName
+ && namesAnExistingQuery(m_config, name)) {
+ m_notice->setText(
+ tr("A saved query named '%1' already exists and will be "
+ "replaced.").arg(name));
+ } else {
+ m_notice->clear();
+ }
+}
+
+SavedQuery SaveQueryDialog::savedQuery() const
+{
+ SavedQuery saved;
+ saved.name = m_name->text().trimmed();
+ saved.query = m_query->text().trimmed();
+ saved.account = m_account->currentData().toString();
+ saved.pinned = m_pinned->isChecked();
+ // Carried through rather than re-derived: an edit must not turn a
+ // generated entry into a plain one holding a snapshot of what it happened
+ // to resolve to today.
+ saved.generated = m_generated;
+ saved.flat = m_flat;
+ if (saved.isGenerated())
+ saved.query.clear();
+ return saved;
+}