From 39ab7108ff33032a3c0a24994314811434bd4349 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 15:53:06 +0200 Subject: [PATCH] Cycle checkboxes with the mouse wheel Scrolling over a checkbox glyph cycles its state unchecked -> partial -> checked (and back), respecting scroll direction: wheel up steps forward, wheel down steps in reverse. Left-click still toggles checked/unchecked. The shared rewriteCheckbox helper applies a state-mapping function to the Nth cookie; toggleCheckbox and the new cycleCheckbox are thin callers. --- ui/qml/OutlineView.qml | 12 +++++++++- ui/src/documentmodel.cpp | 47 ++++++++++++++++++++++++++++++++-------- ui/src/documentmodel.h | 13 +++++++++++ 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 383ca1e..6eb7b61 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -473,7 +473,8 @@ ListView { font.pointSize: outline.baseFontSize + 1 font.bold: cb !== "" - // Clickable only for checkbox items; toggles via the model. + // Checkbox interaction: left-click toggles, + // mouse wheel cycles ( ) -> (-) -> (X). MouseArea { anchors.fill: parent enabled: cbGlyph.cb !== "" @@ -481,6 +482,15 @@ ListView { cursorShape: Qt.PointingHandCursor onClicked: docModel.toggleCheckbox( del.index, modelData.checkboxOrdinal ?? -1) + onWheel: (wheel) => { + // Scroll up = forward, down = reverse. + let forward = wheel.angleDelta.y > 0 + docModel.cycleCheckbox( + del.index, + modelData.checkboxOrdinal ?? -1, + forward) + wheel.accepted = true + } } } diff --git a/ui/src/documentmodel.cpp b/ui/src/documentmodel.cpp index afc7e5d..32e5d80 100644 --- a/ui/src/documentmodel.cpp +++ b/ui/src/documentmodel.cpp @@ -187,7 +187,9 @@ bool DocumentModel::isLanguageSupported(const QString &lang) return m_supportedLanguages.contains(lang.trimmed().toLower()); } -bool DocumentModel::toggleCheckbox(int row, int checkboxOrdinal) +bool DocumentModel::rewriteCheckbox( + int row, int checkboxOrdinal, + const std::function &nextState) { if (!m_client || row < 0 || row >= m_visibleRows.size()) return false; @@ -210,28 +212,24 @@ bool DocumentModel::toggleCheckbox(int row, int checkboxOrdinal) out.reserve(lines.size()); int seen = -1; - bool toggled = false; + bool changed = false; for (const QString &line : lines) { QRegularExpressionMatch m = cookieRe.match(line); if (m.hasMatch()) { ++seen; if (seen == checkboxOrdinal) { - const QString state = m.captured(2); - const QString next = (state == QLatin1String("X") || - state == QLatin1String("x")) - ? QStringLiteral(" ") - : QStringLiteral("X"); + const QString next = nextState(m.captured(2)); QString updated = line; updated.replace(m.capturedStart(2), m.capturedLength(2), next); out.append(updated); - toggled = true; + changed = true; continue; } } out.append(line); } - if (!toggled) + if (!changed) return false; const QString newBody = out.join('\n'); @@ -247,6 +245,37 @@ bool DocumentModel::toggleCheckbox(int row, int checkboxOrdinal) return true; } +bool DocumentModel::toggleCheckbox(int row, int checkboxOrdinal) +{ + // Checked -> unchecked; unchecked/partial -> checked. + return rewriteCheckbox(row, checkboxOrdinal, [](const QString &state) { + const bool checked = (state == QLatin1String("X") || + state == QLatin1String("x")); + return checked ? QStringLiteral(" ") : QStringLiteral("X"); + }); +} + +bool DocumentModel::cycleCheckbox(int row, int checkboxOrdinal, bool forward) +{ + // Cycle order: unchecked ( ) -> partial (-) -> checked (X) -> wrap. + return rewriteCheckbox(row, checkboxOrdinal, [forward](const QString &state) { + // Normalize current state to an index in the cycle. + int cur; + if (state == QLatin1String("-")) + cur = 1; // partial + else if (state == QLatin1String("X") || state == QLatin1String("x")) + cur = 2; // checked + else + cur = 0; // unchecked + const int next = forward ? (cur + 1) % 3 : (cur + 2) % 3; + switch (next) { + case 1: return QStringLiteral("-"); + case 2: return QStringLiteral("X"); + default: return QStringLiteral(" "); + } + }); +} + void DocumentModel::loadDocument() { beginResetModel(); diff --git a/ui/src/documentmodel.h b/ui/src/documentmodel.h index dd56dd2..7eff43a 100644 --- a/ui/src/documentmodel.h +++ b/ui/src/documentmodel.h @@ -15,6 +15,8 @@ #include #include +#include + class OrkClient; class DocumentModel : public QAbstractListModel { @@ -76,6 +78,11 @@ public: // back via /body, and refreshes the row. Returns true on success. Q_INVOKABLE bool toggleCheckbox(int row, int checkboxOrdinal); + // Cycle the Nth checkbox through unchecked -> checked -> partial and back. + // `forward` steps unchecked->checked->partial; otherwise the reverse. Writes + // the section body back via /body and refreshes the row. Returns success. + Q_INVOKABLE bool cycleCheckbox(int row, int checkboxOrdinal, bool forward); + signals: void documentChanged(); @@ -123,6 +130,12 @@ private: // Rebuild the visible row list based on expansion state void rebuildVisibleRows(); + // Rewrite the Nth checkbox cookie (document order) in the given row's + // section body, mapping the current state char (' '/'x'/'X'/'-') to a new + // one via `nextState`. Writes back via /body and refreshes. Returns success. + bool rewriteCheckbox(int row, int checkboxOrdinal, + const std::function &nextState); + OrkClient *m_client = nullptr; QString m_document;