Make GUI checkboxes clickable
Clicking a checkbox glyph toggles its state and writes the change back. DocumentModel::toggleCheckbox(row, ordinal) finds the Nth checkbox cookie in the section body (document order), flips [ ]/[-] -> [X] and [X] -> [ ], writes the body via /body, and refreshes the row. The body parser assigns each checkbox item a stable ordinal across the whole section, and the list delegate wires a pointer-cursor MouseArea on the glyph (checkbox items only) to the model toggle.
This commit is contained in:
parent
754051b3a4
commit
55856707c7
|
|
@ -94,7 +94,9 @@ ListView {
|
||||||
|
|
||||||
// Push a run of text, splitting out any contiguous list items into
|
// Push a run of text, splitting out any contiguous list items into
|
||||||
// dedicated "list" parts so they render with bullets/checkboxes.
|
// dedicated "list" parts so they render with bullets/checkboxes.
|
||||||
function pushText(text) {
|
// `cbCounter` is a single-element array used as a mutable counter so
|
||||||
|
// checkbox ordinals are stable across the whole section body.
|
||||||
|
function pushText(text, cbCounter) {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
let lines = text.split('\n');
|
let lines = text.split('\n');
|
||||||
let buf = [];
|
let buf = [];
|
||||||
|
|
@ -116,6 +118,9 @@ ListView {
|
||||||
let item = parseListItem(ln);
|
let item = parseListItem(ln);
|
||||||
if (item) {
|
if (item) {
|
||||||
flushBuf();
|
flushBuf();
|
||||||
|
// Assign a stable ordinal to checkbox-bearing items.
|
||||||
|
item.checkboxOrdinal = (item.checkbox !== "")
|
||||||
|
? cbCounter[0]++ : -1;
|
||||||
items.push(item);
|
items.push(item);
|
||||||
} else {
|
} else {
|
||||||
flushItems();
|
flushItems();
|
||||||
|
|
@ -126,6 +131,9 @@ ListView {
|
||||||
flushBuf();
|
flushBuf();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Running checkbox ordinal across the whole section body.
|
||||||
|
let cbCounter = [0];
|
||||||
|
|
||||||
let lines = body.split('\n');
|
let lines = body.split('\n');
|
||||||
let currentText = [];
|
let currentText = [];
|
||||||
let inCode = false;
|
let inCode = false;
|
||||||
|
|
@ -143,7 +151,7 @@ ListView {
|
||||||
if (!inCode && !inTable && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) {
|
if (!inCode && !inTable && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) {
|
||||||
// Flush pending text
|
// Flush pending text
|
||||||
if (currentText.length > 0) {
|
if (currentText.length > 0) {
|
||||||
pushText(currentText.join('\n').trim());
|
pushText(currentText.join('\n').trim(), cbCounter);
|
||||||
currentText = [];
|
currentText = [];
|
||||||
}
|
}
|
||||||
inCode = true;
|
inCode = true;
|
||||||
|
|
@ -162,7 +170,7 @@ ListView {
|
||||||
else if (!inTable && trimmed.startsWith("|")) {
|
else if (!inTable && trimmed.startsWith("|")) {
|
||||||
// Flush pending text
|
// Flush pending text
|
||||||
if (currentText.length > 0) {
|
if (currentText.length > 0) {
|
||||||
pushText(currentText.join('\n').trim());
|
pushText(currentText.join('\n').trim(), cbCounter);
|
||||||
currentText = [];
|
currentText = [];
|
||||||
}
|
}
|
||||||
inTable = true;
|
inTable = true;
|
||||||
|
|
@ -212,7 +220,7 @@ ListView {
|
||||||
}
|
}
|
||||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader});
|
parts.push({type: "table", rows: rows, hasHeader: hasHeader});
|
||||||
} else if (currentText.length > 0) {
|
} else if (currentText.length > 0) {
|
||||||
pushText(currentText.join('\n').trim());
|
pushText(currentText.join('\n').trim(), cbCounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parts;
|
return parts;
|
||||||
|
|
@ -447,6 +455,7 @@ ListView {
|
||||||
|
|
||||||
// Checkbox glyph, or a plain bullet when no checkbox.
|
// Checkbox glyph, or a plain bullet when no checkbox.
|
||||||
Label {
|
Label {
|
||||||
|
id: cbGlyph
|
||||||
readonly property string cb: modelData.checkbox ?? ""
|
readonly property string cb: modelData.checkbox ?? ""
|
||||||
text: {
|
text: {
|
||||||
switch (cb) {
|
switch (cb) {
|
||||||
|
|
@ -463,6 +472,16 @@ ListView {
|
||||||
: theme.placeholderTextColor
|
: theme.placeholderTextColor
|
||||||
font.pointSize: outline.baseFontSize + 1
|
font.pointSize: outline.baseFontSize + 1
|
||||||
font.bold: cb !== ""
|
font.bold: cb !== ""
|
||||||
|
|
||||||
|
// Clickable only for checkbox items; toggles via the model.
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: cbGlyph.cb !== ""
|
||||||
|
visible: enabled
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: docModel.toggleCheckbox(
|
||||||
|
del.index, modelData.checkboxOrdinal ?? -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Item text; checked items are dimmed and struck through.
|
// Item text; checked items are dimmed and struck through.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "orkclient.h"
|
#include "orkclient.h"
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
static const QStringList DONE_KEYWORDS = {"DONE", "CANCELLED", "CANCELED"};
|
static const QStringList DONE_KEYWORDS = {"DONE", "CANCELLED", "CANCELED"};
|
||||||
|
|
||||||
|
|
@ -186,6 +187,66 @@ bool DocumentModel::isLanguageSupported(const QString &lang)
|
||||||
return m_supportedLanguages.contains(lang.trimmed().toLower());
|
return m_supportedLanguages.contains(lang.trimmed().toLower());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DocumentModel::toggleCheckbox(int row, int checkboxOrdinal)
|
||||||
|
{
|
||||||
|
if (!m_client || row < 0 || row >= m_visibleRows.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QString &id = m_visibleRows[row];
|
||||||
|
if (!m_sections.contains(id))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Section &sec = m_sections[id];
|
||||||
|
if (!sec.bodyLoaded)
|
||||||
|
loadBody(sec);
|
||||||
|
|
||||||
|
// Match a list item's checkbox cookie: leading indent, a bullet (- / +) or
|
||||||
|
// ordered marker (N. / N)), then a [ ]/[x]/[X]/[-] cookie.
|
||||||
|
static const QRegularExpression cookieRe(
|
||||||
|
QStringLiteral("^(\\s*(?:[-+]|\\d+[.)])\\s+\\[)([ xX-])(\\])"));
|
||||||
|
|
||||||
|
const QStringList lines = sec.body.split('\n');
|
||||||
|
QStringList out;
|
||||||
|
out.reserve(lines.size());
|
||||||
|
|
||||||
|
int seen = -1;
|
||||||
|
bool toggled = 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");
|
||||||
|
QString updated = line;
|
||||||
|
updated.replace(m.capturedStart(2), m.capturedLength(2), next);
|
||||||
|
out.append(updated);
|
||||||
|
toggled = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toggled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QString newBody = out.join('\n');
|
||||||
|
if (!m_client->write(sec.path + QStringLiteral("/body"), newBody))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Refresh from the server so the parsed state matches what was written.
|
||||||
|
sec.bodyLoaded = false;
|
||||||
|
loadBody(sec);
|
||||||
|
|
||||||
|
const QModelIndex idx = index(row);
|
||||||
|
emit dataChanged(idx, idx, {BodyRole});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void DocumentModel::loadDocument()
|
void DocumentModel::loadDocument()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,12 @@ public:
|
||||||
// `/ctl languages` (loaded once and cached). Case-insensitive.
|
// `/ctl languages` (loaded once and cached). Case-insensitive.
|
||||||
Q_INVOKABLE bool isLanguageSupported(const QString &lang);
|
Q_INVOKABLE bool isLanguageSupported(const QString &lang);
|
||||||
|
|
||||||
|
// Toggle the Nth checkbox (0-based, in document order, counting only list
|
||||||
|
// items that carry a `[ ]`/`[X]`/`[-]` cookie) in the section at the given
|
||||||
|
// visible row. Flips [ ]/[-] -> [X] and [X] -> [ ], writes the section body
|
||||||
|
// back via /body, and refreshes the row. Returns true on success.
|
||||||
|
Q_INVOKABLE bool toggleCheckbox(int row, int checkboxOrdinal);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void documentChanged();
|
void documentChanged();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue