268 lines
9.5 KiB
QML
268 lines
9.5 KiB
QML
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
|
|
}
|
|
}
|