191 lines
6.8 KiB
QML
191 lines
6.8 KiB
QML
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Layouts
|
|
|
|
// A folding outline of a document's sections. Each row renders the section
|
|
// headline as distinct structural objects (keyword, priority, title, tags,
|
|
// timestamps) followed by the section body, always visible inline — the
|
|
// document is the control surface.
|
|
TreeView {
|
|
id: tree
|
|
clip: true
|
|
|
|
readonly property color textColor: "#d8dee9"
|
|
readonly property color dimColor: "#7a8290"
|
|
readonly property int rowPad: 6
|
|
|
|
// Palette for TODO keywords by state.
|
|
function keywordColor(kw) {
|
|
switch (kw) {
|
|
case "TODO": return "#bf616a"
|
|
case "NEXT": return "#d08770"
|
|
case "WAITING": return "#ebcb8b"
|
|
case "DONE": return "#a3be8c"
|
|
case "CANCELLED": return "#7a8290"
|
|
default: return "#88c0d0"
|
|
}
|
|
}
|
|
|
|
// Headline text scales down slightly with depth.
|
|
function levelFontSize(level) {
|
|
return Math.max(13, 19 - level)
|
|
}
|
|
|
|
selectionModel: ItemSelectionModel {}
|
|
|
|
delegate: TreeViewDelegate {
|
|
id: del
|
|
indentation: 18
|
|
|
|
readonly property string sectionBody: (model.body ?? "").trim()
|
|
readonly property int bodyIndent: 12
|
|
|
|
// The horizontal space available to the body text. The base delegate
|
|
// reserves (depth+1)*indentation on the left for the tree structure;
|
|
// the content region occupies the rest, minus our body indent and a
|
|
// right margin. Computing this explicitly (rather than via anchors)
|
|
// gives the body Text a concrete width, so contentHeight resolves
|
|
// deterministically and the row height below is correct.
|
|
readonly property real bodyWidth:
|
|
Math.max(1, tree.width - (depth + 1) * indentation
|
|
- bodyIndent - 12)
|
|
|
|
implicitWidth: tree.width
|
|
|
|
// Row height = headline + body (if any) + padding. bodyMeasure is a
|
|
// non-visible Text with the same font/width as the body, used purely
|
|
// to measure wrapped height without depending on the laid-out item.
|
|
implicitHeight: headlineRow.implicitHeight
|
|
+ (sectionBody !== "" ? bodyMeasure.implicitHeight + 4 : 0)
|
|
+ tree.rowPad * 2
|
|
|
|
background: Rectangle {
|
|
color: del.current ? "#2f3540" : "transparent"
|
|
radius: 4
|
|
}
|
|
|
|
// Off-screen measurement of the wrapped body height.
|
|
Text {
|
|
id: bodyMeasure
|
|
visible: false
|
|
width: del.bodyWidth
|
|
text: del.sectionBody
|
|
font.pixelSize: 12
|
|
lineHeight: 1.15
|
|
wrapMode: Text.WordWrap
|
|
textFormat: Text.PlainText
|
|
}
|
|
|
|
contentItem: Item {
|
|
RowLayout {
|
|
id: headlineRow
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
spacing: 8
|
|
|
|
// TODO keyword pill.
|
|
Rectangle {
|
|
visible: (model.keyword ?? "") !== ""
|
|
radius: 3
|
|
color: "transparent"
|
|
border.color: tree.keywordColor(model.keyword ?? "")
|
|
border.width: 1
|
|
implicitWidth: kwLabel.implicitWidth + 10
|
|
implicitHeight: kwLabel.implicitHeight + 4
|
|
Label {
|
|
id: kwLabel
|
|
anchors.centerIn: parent
|
|
text: model.keyword ?? ""
|
|
color: tree.keywordColor(model.keyword ?? "")
|
|
font.pixelSize: 11
|
|
font.bold: true
|
|
}
|
|
}
|
|
|
|
// Priority cookie.
|
|
Label {
|
|
visible: (model.priority ?? "") !== ""
|
|
text: "[#" + (model.priority ?? "") + "]"
|
|
color: "#b48ead"
|
|
font.pixelSize: 12
|
|
font.bold: true
|
|
}
|
|
|
|
// Title.
|
|
Label {
|
|
text: (model.title ?? "")
|
|
color: (model.isDone ?? false) ? tree.dimColor : tree.textColor
|
|
font.pixelSize: tree.levelFontSize(model.level ?? 1)
|
|
font.strikeout: (model.isDone ?? false)
|
|
elide: Text.ElideRight
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
// Scheduled / deadline stamps.
|
|
Label {
|
|
visible: (model.scheduled ?? "") !== ""
|
|
text: "⏱ " + (model.scheduled ?? "")
|
|
color: "#81a1c1"
|
|
font.pixelSize: 11
|
|
}
|
|
Label {
|
|
visible: (model.deadline ?? "") !== ""
|
|
text: "⚑ " + (model.deadline ?? "")
|
|
color: "#bf616a"
|
|
font.pixelSize: 11
|
|
}
|
|
|
|
// Tags.
|
|
Repeater {
|
|
model: del.model.tags ?? []
|
|
delegate: Rectangle {
|
|
required property string modelData
|
|
radius: 3
|
|
color: "#3b4252"
|
|
implicitWidth: tagLabel.implicitWidth + 8
|
|
implicitHeight: tagLabel.implicitHeight + 3
|
|
Label {
|
|
id: tagLabel
|
|
anchors.centerIn: parent
|
|
text: parent.modelData
|
|
color: "#8fbcbb"
|
|
font.pixelSize: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Left rule marking the body region.
|
|
Rectangle {
|
|
visible: bodyLabel.visible
|
|
width: 2
|
|
color: "#3b4252"
|
|
anchors.top: bodyLabel.top
|
|
anchors.bottom: bodyLabel.bottom
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: 2
|
|
}
|
|
|
|
// Section body, always visible inline beneath the headline.
|
|
// Given an explicit width (matching bodyMeasure) so wrapping and
|
|
// height are consistent with the reserved row height.
|
|
Text {
|
|
id: bodyLabel
|
|
anchors.top: headlineRow.bottom
|
|
anchors.topMargin: 4
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: del.bodyIndent
|
|
width: del.bodyWidth
|
|
visible: del.sectionBody !== ""
|
|
text: del.sectionBody
|
|
color: tree.dimColor
|
|
font.pixelSize: 12
|
|
lineHeight: 1.15
|
|
wrapMode: Text.WordWrap
|
|
textFormat: Text.PlainText
|
|
}
|
|
}
|
|
}
|
|
}
|