import QtQuick import QtQuick.Controls import QtQuick.Layouts // A compact date picker for a section's SCHEDULED/DEADLINE planning // timestamp. Supports the common point-timestamp forms the parser handles: // a date, an optional time (HH:MM), and an optional repeater (e.g. +1w). // Planning timestamps are active by org convention, so the server wraps the // value in <...>. Set writes the field, Clear removes it. // // Open with open(row, field, currentValue), where field is "SCHEDULED" or // "DEADLINE" and currentValue is the field's current <...> string (may be // empty). The dialog parses currentValue to prefill its controls. Dialog { id: dpd title: qsTr("Set Date") modal: true standardButtons: Dialog.NoButton width: 340 x: (parent.width - width) / 2 y: (parent.height - height) / 2 // Target section row and planning field. property int targetRow: -1 property string field: "SCHEDULED" // Mode: "planning" writes a SCHEDULED/DEADLINE field; "insert" emits the // built timestamp for a caller to insert (e.g. into a body editor at the // cursor). In insert mode `insertActive` chooses <...> vs [...]. property string mode: "planning" property bool insertActive: true signal timestampAccepted(string ts) // Calendar state. property int shownMonth: (new Date()).getMonth() // 0-11 property int shownYear: (new Date()).getFullYear() // Selected date components (-1 day = none selected yet). property int selYear: -1 property int selMonth: -1 property int selDay: -1 readonly property var monthNames: [ qsTr("January"), qsTr("February"), qsTr("March"), qsTr("April"), qsTr("May"), qsTr("June"), qsTr("July"), qsTr("August"), qsTr("September"), qsTr("October"), qsTr("November"), qsTr("December") ] readonly property var dayNames: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] background: Rectangle { color: theme.windowColor border.color: theme.midColor border.width: 1 radius: 6 } // Open the picker for a given row/field, prefilled from currentValue. function open2(row, fieldName, currentValue) { mode = "planning" targetRow = row field = fieldName parseValue(currentValue) open() } // Open the picker in insert mode. On Set it emits timestampAccepted with // the built <...> (or [...]) timestamp instead of writing a field. function openInsert() { mode = "insert" parseValue("") open() } // Parse an org timestamp like "<2025-01-15 Wed 10:30 +1w>" into the // dialog's controls. An empty/unparseable value leaves today selected and // the time/repeater empty. function parseValue(v) { timeField.text = "" repeaterField.text = "" var now = new Date() selYear = now.getFullYear(); selMonth = now.getMonth(); selDay = now.getDate() if (v && v.length > 0) { // Strip surrounding < > or [ ]. var inner = v.replace(/^[<\[]/, "").replace(/[>\]]$/, "").trim() var dm = inner.match(/^(\d{4})-(\d{2})-(\d{2})/) if (dm) { selYear = parseInt(dm[1]); selMonth = parseInt(dm[2]) - 1; selDay = parseInt(dm[3]) } var tm = inner.match(/\b(\d{1,2}:\d{2})\b/) if (tm) timeField.text = tm[1] var rm = inner.match(/((?:\+\+|\.\+|\+)\d+[hdwmy])/) if (rm) repeaterField.text = rm[1] } shownMonth = selMonth shownYear = selYear } function prevMonth() { if (shownMonth === 0) { shownMonth = 11; shownYear-- } else shownMonth-- } function nextMonth() { if (shownMonth === 11) { shownMonth = 0; shownYear++ } else shownMonth++ } // Build the inner timestamp string "YYYY-MM-DD Day [HH:MM] [repeater]". function buildTimestamp() { if (selYear < 0) return "" var mm = (selMonth + 1 < 10 ? "0" : "") + (selMonth + 1) var dd = (selDay < 10 ? "0" : "") + selDay var dow = dayNames[(new Date(selYear, selMonth, selDay)).getDay()] var s = selYear + "-" + mm + "-" + dd + " " + dow var t = timeField.text.trim() if (/^\d{1,2}:\d{2}$/.test(t)) s += " " + t var r = repeaterField.text.trim() if (/^(?:\+\+|\.\+|\+)\d+[hdwmy]$/.test(r)) s += " " + r return s } function applySet() { var ts = buildTimestamp() if (ts.length === 0) { close(); return } if (mode === "insert") { var wrapped = insertActive ? "<" + ts + ">" : "[" + ts + "]" timestampAccepted(wrapped) close() return } if (field === "DEADLINE") docModel.setDeadline(targetRow, ts) else docModel.setScheduled(targetRow, ts) close() } function applyClear() { if (field === "DEADLINE") docModel.setDeadline(targetRow, "") else docModel.setScheduled(targetRow, "") close() } contentItem: ColumnLayout { spacing: 10 // Mode-dependent selector: Scheduled/Deadline (planning) or // Active/Inactive (insert). RowLayout { Layout.fillWidth: true spacing: 6 visible: dpd.mode === "planning" RadioButton { text: qsTr("Scheduled") checked: dpd.field === "SCHEDULED" onClicked: dpd.field = "SCHEDULED" } RadioButton { text: qsTr("Deadline") checked: dpd.field === "DEADLINE" onClicked: dpd.field = "DEADLINE" } } // Insert-mode selector: active <...> vs inactive [...]. RowLayout { Layout.fillWidth: true spacing: 6 visible: dpd.mode === "insert" RadioButton { text: qsTr("Active <>") checked: dpd.insertActive onClicked: dpd.insertActive = true } RadioButton { text: qsTr("Inactive []") checked: !dpd.insertActive onClicked: dpd.insertActive = false } } // Month/year navigation. RowLayout { Layout.fillWidth: true Button { text: "‹"; onClicked: dpd.prevMonth() } Label { Layout.fillWidth: true horizontalAlignment: Text.AlignHCenter text: dpd.monthNames[dpd.shownMonth] + " " + dpd.shownYear color: theme.textColor font.bold: true } Button { text: "›"; onClicked: dpd.nextMonth() } } DayOfWeekRow { Layout.fillWidth: true locale: grid.locale delegate: Label { text: model.shortName color: theme.placeholderTextColor font.pointSize: Math.max(8, (theme.fontSize > 0 ? theme.fontSize : 11) - 2) horizontalAlignment: Text.AlignHCenter } } MonthGrid { id: grid Layout.fillWidth: true month: dpd.shownMonth year: dpd.shownYear delegate: Rectangle { required property var model readonly property bool inMonth: model.month === dpd.shownMonth readonly property bool isSelected: model.year === dpd.selYear && model.month === dpd.selMonth && model.day === dpd.selDay implicitWidth: 36 implicitHeight: 28 radius: 4 color: isSelected ? theme.highlightColor : (dayHover.hovered && inMonth ? theme.alternateBaseColor : "transparent") Label { anchors.centerIn: parent text: model.day color: !inMonth ? theme.placeholderTextColor : (parent.isSelected ? theme.highlightedTextColor : theme.textColor) font.pointSize: theme.fontSize > 0 ? theme.fontSize - 1 : 10 } HoverHandler { id: dayHover } TapHandler { onTapped: { dpd.selYear = model.year dpd.selMonth = model.month dpd.selDay = model.day // Follow the click into adjacent months. dpd.shownMonth = model.month dpd.shownYear = model.year } } } } // Optional time and repeater. GridLayout { Layout.fillWidth: true columns: 2 columnSpacing: 8 rowSpacing: 6 Label { text: qsTr("Time"); color: theme.textColor } TextField { id: timeField Layout.fillWidth: true placeholderText: qsTr("HH:MM (optional)") inputMask: "" selectByMouse: true } Label { text: qsTr("Repeat"); color: theme.textColor } TextField { id: repeaterField Layout.fillWidth: true placeholderText: qsTr("+1w / ++1m / .+2d (optional)") selectByMouse: true } } // Actions. RowLayout { Layout.fillWidth: true spacing: 6 Button { text: qsTr("Clear") visible: dpd.mode === "planning" onClicked: dpd.applyClear() } Item { Layout.fillWidth: true } Button { text: qsTr("Cancel") onClicked: dpd.close() } Button { text: dpd.mode === "insert" ? qsTr("Insert") : qsTr("Set") highlighted: true onClicked: dpd.applySet() } } } }