Render list items and checkboxes in the GUI body
The body parser now recognizes org list items (- / + / ordered) and splits them out of text runs into dedicated "list" parts. Each item captures its indent, bullet, optional checkbox state, and text. A new list component renders checkbox items with ☐ / ▣ / ☑ glyphs (unchecked / partial / checked), colored by state, with checked items dimmed and struck through. Plain list items render with their bullet.
This commit is contained in:
parent
36a9607576
commit
754051b3a4
|
|
@ -63,6 +63,69 @@ ListView {
|
|||
let body = sectionBody;
|
||||
if (!body) return parts;
|
||||
|
||||
// Match an org list item, capturing indent, bullet/number, optional
|
||||
// checkbox, and the item text. Returns null for non-list lines.
|
||||
function parseListItem(line) {
|
||||
// Unordered: - / + bullet (indented * also allowed)
|
||||
let m = line.match(/^(\s*)([-+])\s+(\[([ xX-])\]\s+)?(.*)$/);
|
||||
let ordered = false;
|
||||
if (!m) {
|
||||
// Ordered: 1. or 1)
|
||||
m = line.match(/^(\s*)(\d+)[.)]\s+(\[([ xX-])\]\s+)?(.*)$/);
|
||||
if (!m) return null;
|
||||
ordered = true;
|
||||
}
|
||||
let cb = "";
|
||||
if (m[3] !== undefined) {
|
||||
switch ((m[4] || " ")) {
|
||||
case "x": case "X": cb = "checked"; break;
|
||||
case "-": cb = "partial"; break;
|
||||
default: cb = "unchecked"; break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
indent: m[1].length,
|
||||
ordered: ordered,
|
||||
bullet: m[2],
|
||||
checkbox: cb,
|
||||
text: m[5]
|
||||
};
|
||||
}
|
||||
|
||||
// Push a run of text, splitting out any contiguous list items into
|
||||
// dedicated "list" parts so they render with bullets/checkboxes.
|
||||
function pushText(text) {
|
||||
if (!text) return;
|
||||
let lines = text.split('\n');
|
||||
let buf = [];
|
||||
let items = [];
|
||||
function flushBuf() {
|
||||
if (buf.length > 0) {
|
||||
let t = buf.join('\n').trim();
|
||||
if (t) parts.push({type: "text", content: t});
|
||||
buf = [];
|
||||
}
|
||||
}
|
||||
function flushItems() {
|
||||
if (items.length > 0) {
|
||||
parts.push({type: "list", items: items});
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
for (let ln of lines) {
|
||||
let item = parseListItem(ln);
|
||||
if (item) {
|
||||
flushBuf();
|
||||
items.push(item);
|
||||
} else {
|
||||
flushItems();
|
||||
buf.push(ln);
|
||||
}
|
||||
}
|
||||
flushItems();
|
||||
flushBuf();
|
||||
}
|
||||
|
||||
let lines = body.split('\n');
|
||||
let currentText = [];
|
||||
let inCode = false;
|
||||
|
|
@ -80,8 +143,7 @@ ListView {
|
|||
if (!inCode && !inTable && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) {
|
||||
// Flush pending text
|
||||
if (currentText.length > 0) {
|
||||
let text = currentText.join('\n').trim();
|
||||
if (text) parts.push({type: "text", content: text});
|
||||
pushText(currentText.join('\n').trim());
|
||||
currentText = [];
|
||||
}
|
||||
inCode = true;
|
||||
|
|
@ -100,8 +162,7 @@ ListView {
|
|||
else if (!inTable && trimmed.startsWith("|")) {
|
||||
// Flush pending text
|
||||
if (currentText.length > 0) {
|
||||
let text = currentText.join('\n').trim();
|
||||
if (text) parts.push({type: "text", content: text});
|
||||
pushText(currentText.join('\n').trim());
|
||||
currentText = [];
|
||||
}
|
||||
inTable = true;
|
||||
|
|
@ -151,8 +212,7 @@ ListView {
|
|||
}
|
||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader});
|
||||
} else if (currentText.length > 0) {
|
||||
let text = currentText.join('\n').trim();
|
||||
if (text) parts.push({type: "text", content: text});
|
||||
pushText(currentText.join('\n').trim());
|
||||
}
|
||||
|
||||
return parts;
|
||||
|
|
@ -344,7 +404,8 @@ ListView {
|
|||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
sourceComponent: modelData.type === "code" ? codeBlockComponent :
|
||||
modelData.type === "table" ? tableComponent : textBlockComponent
|
||||
modelData.type === "table" ? tableComponent :
|
||||
modelData.type === "list" ? listComponent : textBlockComponent
|
||||
|
||||
Component {
|
||||
id: textBlockComponent
|
||||
|
|
@ -368,6 +429,58 @@ ListView {
|
|||
}
|
||||
}
|
||||
|
||||
// Plain / checkbox list rendering
|
||||
Component {
|
||||
id: listComponent
|
||||
ColumnLayout {
|
||||
spacing: outline.baseFontSize * 0.2
|
||||
|
||||
Repeater {
|
||||
model: modelData.items ?? []
|
||||
|
||||
delegate: RowLayout {
|
||||
required property var modelData
|
||||
spacing: outline.baseFontSize * 0.4
|
||||
// Indentation: 2 spaces of org indent ≈ one level.
|
||||
Layout.leftMargin: outline.baseFontSize
|
||||
+ (modelData.indent / 2) * outline.indentWidth
|
||||
|
||||
// Checkbox glyph, or a plain bullet when no checkbox.
|
||||
Label {
|
||||
readonly property string cb: modelData.checkbox ?? ""
|
||||
text: {
|
||||
switch (cb) {
|
||||
case "checked": return "☑"
|
||||
case "partial": return "▣"
|
||||
case "unchecked": return "☐"
|
||||
default:
|
||||
return (modelData.ordered ?? false) ? "•" : (modelData.bullet ?? "•")
|
||||
}
|
||||
}
|
||||
color: cb === "checked" ? theme.doneColor
|
||||
: cb === "partial" ? theme.waitingColor
|
||||
: cb === "unchecked" ? theme.todoColor
|
||||
: theme.placeholderTextColor
|
||||
font.pointSize: outline.baseFontSize + 1
|
||||
font.bold: cb !== ""
|
||||
}
|
||||
|
||||
// Item text; checked items are dimmed and struck through.
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: modelData.text ?? ""
|
||||
color: (modelData.checkbox === "checked")
|
||||
? theme.placeholderTextColor : theme.textColor
|
||||
font.pointSize: outline.baseFontSize + 1
|
||||
font.strikeout: modelData.checkbox === "checked"
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: codeBlockComponent
|
||||
Rectangle {
|
||||
|
|
|
|||
Loading…
Reference in New Issue