import QtQuick import QtQuick.Controls import QtQuick.Layouts // Document outline rendered as a flat ListView with indentation. // Each section is a row; nesting is shown via left margin. // This gives us full control over layout, selection, and editing. ListView { id: outline clip: true spacing: 4 readonly property int rowPad: 6 readonly property int baseFontSize: theme.fontSize > 0 ? theme.fontSize : 12 readonly property int indentWidth: 20 // Palette for TODO keywords by state. function keywordColor(kw) { switch (kw) { case "TODO": return theme.todoColor case "NEXT": return theme.todoColor case "WAITING": return theme.waitingColor case "DONE": return theme.doneColor case "CANCELLED": return theme.cancelledColor default: return theme.linkColor } } // Priority color by level. function priorityColor(pri) { switch (pri) { case "A": return theme.priorityAColor case "B": return theme.priorityBColor case "C": return theme.priorityCColor default: return theme.textColor } } // Headline text scales down slightly with depth. function levelFontSize(level) { return Math.max(outline.baseFontSize - 1, outline.baseFontSize + 5 - level) } delegate: Rectangle { id: del width: outline.width implicitHeight: contentColumn.implicitHeight + outline.rowPad * 2 color: ListView.isCurrentItem ? Qt.rgba(theme.highlightColor.r, theme.highlightColor.g, theme.highlightColor.b, 0.2) : "transparent" radius: 4 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 MouseArea { anchors.fill: parent onClicked: outline.currentIndex = del.index } ColumnLayout { id: contentColumn anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.leftMargin: del.indent + 8 anchors.rightMargin: 8 anchors.topMargin: outline.rowPad spacing: 4 RowLayout { id: headlineRow Layout.fillWidth: true spacing: 8 // Expand/collapse indicator for sections with children Label { visible: (del.model.hasChildren ?? false) text: (del.model.expanded ?? true) ? "▼" : "▶" color: theme.placeholderTextColor font.pointSize: outline.baseFontSize - 2 MouseArea { anchors.fill: parent onClicked: { // Toggle expansion via model if (del.model.expanded !== undefined) { docModel.toggleExpanded(del.index) } } } } // TODO keyword pill Rectangle { visible: (del.model.keyword ?? "") !== "" radius: 3 color: "transparent" border.color: outline.keywordColor(del.model.keyword ?? "") border.width: 1 implicitWidth: kwLabel.implicitWidth + 10 implicitHeight: kwLabel.implicitHeight + 4 Label { id: kwLabel anchors.centerIn: parent text: del.model.keyword ?? "" color: outline.keywordColor(del.model.keyword ?? "") font.pointSize: outline.baseFontSize - 1 font.bold: true } } // Priority cookie Label { visible: (del.model.priority ?? "") !== "" text: "[#" + (del.model.priority ?? "") + "]" color: outline.priorityColor(del.model.priority ?? "") font.pointSize: outline.baseFontSize font.bold: true } // Title Label { text: del.model.title ?? "" color: (del.model.isDone ?? false) ? theme.placeholderTextColor : theme.textColor font.pointSize: outline.levelFontSize(del.sectionLevel) font.strikeout: del.model.isDone ?? false elide: Text.ElideRight Layout.fillWidth: true } // Scheduled timestamp Label { visible: (del.model.scheduled ?? "") !== "" text: "⏱ " + (del.model.scheduled ?? "") color: theme.timestampColor font.pointSize: outline.baseFontSize - 1 } // Deadline timestamp Label { visible: (del.model.deadline ?? "") !== "" text: "⚑ " + (del.model.deadline ?? "") color: theme.deadlineColor font.pointSize: outline.baseFontSize - 1 } // Tags Repeater { model: del.model.tags ?? [] delegate: Rectangle { required property string modelData radius: 3 color: theme.alternateBaseColor implicitWidth: tagLabel.implicitWidth + 8 implicitHeight: tagLabel.implicitHeight + 3 Label { id: tagLabel anchors.centerIn: parent text: parent.modelData color: theme.tagColor font.pointSize: outline.baseFontSize - 2 } } } } // 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 !== "" Layout.fillWidth: true spacing: 8 Rectangle { width: 2 Layout.fillHeight: true Layout.leftMargin: 2 color: theme.midColor } Label { Layout.fillWidth: true text: del.sectionBody color: theme.placeholderTextColor font.pointSize: outline.baseFontSize + 1 lineHeight: 1.3 wrapMode: Text.WordWrap textFormat: Text.PlainText } } } } // Empty state Label { anchors.centerIn: parent visible: outline.count === 0 text: qsTr("No sections") color: theme.placeholderTextColor font.pointSize: outline.baseFontSize } }