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.
This commit is contained in:
Levi Neely 2026-10-01 15:53:06 +02:00
parent 3d3ccd1e11
commit 39ab7108ff
3 changed files with 62 additions and 10 deletions

View File

@ -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
}
}
}

View File

@ -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<QString(const QString &)> &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();

View File

@ -15,6 +15,8 @@
#include <QVector>
#include <QSet>
#include <functional>
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<QString(const QString &)> &nextState);
OrkClient *m_client = nullptr;
QString m_document;