From 429c896f5aa077dd56c9e1e2f5baab7b8cd23d67 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 10:00:20 +0200 Subject: [PATCH] Add collapsible properties drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PropertiesRole returns QVariantMap of key->value - loadProperties() parses KEY=value lines from /properties file - QML drawer shows ':PROPERTIES:' header with ▶/▼ toggle - Properties shown as monospace :KEY: value pairs when expanded - Drawer starts collapsed by default --- ui/qml/OutlineView.qml | 63 ++++++++++++++++++++++++++++++++++++++++ ui/src/documentmodel.cpp | 24 +++++++++++++++ ui/src/documentmodel.h | 8 +++++ 3 files changed, 95 insertions(+) diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 7ffdba8..2235815 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -51,6 +51,8 @@ ListView { required property int index required property var model + property bool drawerOpen: false + readonly property string sectionBody: (model.body ?? "").trim() readonly property int sectionLevel: model.level ?? 1 readonly property int indent: (sectionLevel - 1) * outline.indentWidth @@ -167,6 +169,67 @@ ListView { } } + // Properties drawer (collapsible) + RowLayout { + visible: Object.keys(del.model.properties ?? {}).length > 0 + Layout.fillWidth: true + spacing: 4 + + Label { + id: drawerToggle + text: del.drawerOpen ? "▼" : "▶" + color: theme.placeholderTextColor + font.pointSize: outline.baseFontSize - 2 + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: del.drawerOpen = !del.drawerOpen + } + } + + Label { + text: ":PROPERTIES:" + color: theme.placeholderTextColor + font.pointSize: outline.baseFontSize - 1 + font.family: "monospace" + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: del.drawerOpen = !del.drawerOpen + } + } + } + + // Properties content (when expanded) + ColumnLayout { + visible: del.drawerOpen && Object.keys(del.model.properties ?? {}).length > 0 + Layout.fillWidth: true + Layout.leftMargin: 16 + spacing: 2 + + Repeater { + model: Object.keys(del.model.properties ?? {}) + delegate: RowLayout { + required property string modelData + spacing: 8 + Label { + text: ":" + modelData + ":" + color: theme.linkColor + font.pointSize: outline.baseFontSize - 1 + font.family: "monospace" + } + Label { + text: del.model.properties[modelData] ?? "" + color: theme.textColor + font.pointSize: outline.baseFontSize - 1 + font.family: "monospace" + } + } + } + } + // Body text with left rule RowLayout { visible: del.sectionBody !== "" diff --git a/ui/src/documentmodel.cpp b/ui/src/documentmodel.cpp index 6c72c61..68ae0e2 100644 --- a/ui/src/documentmodel.cpp +++ b/ui/src/documentmodel.cpp @@ -74,6 +74,10 @@ QVariant DocumentModel::data(const QModelIndex &index, int role) const if (!sec.bodyLoaded) loadBody(sec); return sec.body; + case PropertiesRole: + if (!sec.propertiesLoaded) + loadProperties(sec); + return sec.properties; case HasChildrenRole: return !sec.childIds.isEmpty(); case ExpandedRole: @@ -98,6 +102,7 @@ QHash DocumentModel::roleNames() const {IsTodoRole, "isTodo"}, {IsDoneRole, "isDone"}, {BodyRole, "body"}, + {PropertiesRole, "properties"}, {HasChildrenRole, "hasChildren"}, {ExpandedRole, "expanded"}, }; @@ -224,6 +229,25 @@ void DocumentModel::loadBody(Section &sec) const sec.bodyLoaded = true; } +void DocumentModel::loadProperties(Section &sec) const +{ + if (!m_client || sec.propertiesLoaded) + return; + + QString propsStr = m_client->read(sec.path + "/properties").trimmed(); + if (!propsStr.isEmpty()) { + for (const QString &line : propsStr.split('\n')) { + int eq = line.indexOf('='); + if (eq > 0) { + QString key = line.left(eq).trimmed(); + QString value = line.mid(eq + 1).trimmed(); + sec.properties[key] = value; + } + } + } + sec.propertiesLoaded = true; +} + void DocumentModel::rebuildVisibleRows() { QStringList oldRows = m_visibleRows; diff --git a/ui/src/documentmodel.h b/ui/src/documentmodel.h index 2ae9216..731c981 100644 --- a/ui/src/documentmodel.h +++ b/ui/src/documentmodel.h @@ -35,6 +35,7 @@ public: IsTodoRole, // keyword is non-empty IsDoneRole, // keyword is a "done" state (DONE/CANCELLED) BodyRole, // section content beneath the headline (lazy) + PropertiesRole, // QVariantMap of property key -> value HasChildrenRole, // true if section has children ExpandedRole, // true if section is expanded (children visible) }; @@ -81,6 +82,10 @@ private: // Section body (lazy) QString body; bool bodyLoaded = false; + + // Properties drawer (lazy) + QVariantMap properties; + bool propertiesLoaded = false; }; // Load all sections for the current document @@ -95,6 +100,9 @@ private: // Load body for a section void loadBody(Section &sec) const; + // Load properties for a section + void loadProperties(Section &sec) const; + // Rebuild the visible row list based on expansion state void rebuildVisibleRows();