orkmode/ui/qml/DatePickerDialog.qml

293 lines
10 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}
}
}
}