// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import "../.." import "Cal.js" as Cal // The event form. The month grid beside it is its date picker: the panel // forwards grid clicks to pick(). Text fields are filled imperatively by // load() and write back on edit, so typing never fights a binding. Column { id: ed property var draft: Cal.newDraft(Cal.today(new Date()), new Date(), "") property bool pickUntil: false property bool confirmDelete: false property bool busy: false property string err: "" signal done() spacing: 8 function load(d) { ed.draft = d; ed.err = ""; ed.pickUntil = false; ed.confirmDelete = false; ed.busy = false; title.input.text = d.title; startTime.input.text = d.startTime; endTime.input.text = d.endTime; location.input.text = d.location; notes.text = d.notes; title.input.forceActiveFocus(); } function set(key, value) { const d = Object.assign({}, ed.draft); d[key] = value; ed.draft = d; } function pick(day, shift) { if (ed.pickUntil) { ed.set("until", day); ed.pickUntil = false; } else { ed.draft = Cal.pick(ed.draft, day, shift); } } function finish(r) { ed.busy = false; if (r.ok) ed.done(); else ed.err = r.error; } function save() { if (ed.busy) return; const r = Cal.toPayload(ed.draft); if (r.error) { ed.err = r.error; return; } ed.busy = true; if (ed.draft.uid) Calendar.update(ed.draft.uid, ed.draft.source, r.payload, ed.finish); else Calendar.create(r.payload, ed.finish); } function remove() { if (ed.busy) return; if (!ed.confirmDelete) { ed.confirmDelete = true; return; } ed.busy = true; Calendar.remove(ed.draft.uid, ed.draft.source, ed.finish); } function dayLabel(day) { const d = new Date(+day.slice(0, 4), +day.slice(5, 7) - 1, +day.slice(8, 10)); return Qt.formatDate(d, "ddd d MMM"); } // An unhandled Escape from any field lands here: leave the editor, not // the panel. Keys.onEscapePressed: ed.done() component Field: Rectangle { property alias input: ti property string placeholder: "" height: 30 radius: 8 color: Theme.surface TextInput { activeFocusOnTab: true id: ti anchors { fill: parent; leftMargin: 8; rightMargin: 8 } verticalAlignment: TextInput.AlignVCenter clip: true selectByMouse: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Text { anchors { fill: parent; leftMargin: 8 } verticalAlignment: Text.AlignVCenter visible: ti.text === "" && !ti.activeFocus text: parent.placeholder color: Theme.overlay font: ti.font } } component Chip: Rectangle { property string label: "" property bool on: false property color tint: Theme.accent signal clicked() width: chipText.implicitWidth + 16 height: 26 radius: 13 color: on ? Qt.alpha(tint, 0.35) : Theme.surface Text { id: chipText anchors.centerIn: parent text: parent.label color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } MouseArea { anchors.fill: parent; cursorShape: Qt.PointingHandCursor; onClicked: parent.clicked() } } component Label: Text { width: 58 anchors.verticalCenter: parent ? parent.verticalCenter : undefined color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Field { id: title width: ed.width placeholder: "Title" input.onTextEdited: ed.set("title", input.text) input.onAccepted: ed.save() } // When: the dates come from the grid. Row { spacing: 6 Label { text: "When" } Text { anchors.verticalCenter: parent.verticalCenter text: ed.draft.startDay === ed.draft.endDay ? ed.dayLabel(ed.draft.startDay) : ed.dayLabel(ed.draft.startDay) + " – " + ed.dayLabel(ed.draft.endDay) color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } } Text { anchors.verticalCenter: parent.verticalCenter text: "click the month, shift for the end" color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 6 } } } Row { spacing: 6 Label { text: "Time" } Field { id: startTime width: 62 visible: !ed.draft.allDay input.onTextEdited: ed.set("startTime", input.text) MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton property real acc: 0 onWheel: wheel => { acc += wheel.angleDelta.y; if (Math.abs(acc) < 120) return; parent.input.text = Cal.stepTime(parent.input.text, acc > 0 ? 15 : -15); ed.set("startTime", parent.input.text); acc = 0; } } } Text { anchors.verticalCenter: parent.verticalCenter visible: !ed.draft.allDay text: "–" color: Theme.subtext } Field { id: endTime width: 62 visible: !ed.draft.allDay input.onTextEdited: ed.set("endTime", input.text) MouseArea { anchors.fill: parent acceptedButtons: Qt.NoButton property real acc: 0 onWheel: wheel => { acc += wheel.angleDelta.y; if (Math.abs(acc) < 120) return; parent.input.text = Cal.stepTime(parent.input.text, acc > 0 ? 15 : -15); ed.set("endTime", parent.input.text); acc = 0; } } } Chip { anchors.verticalCenter: parent.verticalCenter label: "all day" on: ed.draft.allDay onClicked: ed.set("allDay", !ed.draft.allDay) } } Row { spacing: 6 Label { text: "Calendar" } Repeater { model: Calendar.calendars delegate: Chip { required property var modelData anchors.verticalCenter: parent.verticalCenter label: modelData.name on: ed.draft.calendar === modelData.name tint: modelData.color.startsWith("#") ? modelData.color : Theme[modelData.color] onClicked: ed.set("calendar", modelData.name) } } } Row { spacing: 4 Label { text: "Repeat" } Repeater { // custom is shown, never offered: it means a rule this form // cannot express, kept as it is. model: ["none", "daily", "weekly", "monthly", "yearly"] .concat(ed.draft.repeat === "custom" ? ["custom"] : []) delegate: Chip { required property string modelData anchors.verticalCenter: parent.verticalCenter label: ({ none: "none", daily: "day", weekly: "week", monthly: "month", yearly: "year", custom: "custom" })[modelData] on: ed.draft.repeat === modelData onClicked: if (modelData !== "custom") ed.set("repeat", modelData) } } } Row { spacing: 6 visible: ed.draft.repeat !== "none" && ed.draft.repeat !== "custom" Label { text: "Until" } Chip { anchors.verticalCenter: parent.verticalCenter label: ed.pickUntil ? "pick a day…" : (ed.draft.until ? ed.dayLabel(ed.draft.until) : "forever") on: ed.pickUntil onClicked: ed.pickUntil = !ed.pickUntil } Chip { anchors.verticalCenter: parent.verticalCenter visible: ed.draft.until !== "" label: "×" onClicked: ed.set("until", "") } } Row { spacing: 4 Label { text: "Alarm" } Repeater { model: ["none", "10m", "1h", "1d"].concat(ed.draft.alarm === "custom" ? ["custom"] : []) delegate: Chip { required property string modelData anchors.verticalCenter: parent.verticalCenter label: modelData on: ed.draft.alarm === modelData onClicked: if (modelData !== "custom") ed.set("alarm", modelData) } } } Text { visible: ed.draft.override === true width: ed.width wrapMode: Text.WordWrap text: "cal-notif has its own alarm for this event, and it wins." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } Field { id: location width: ed.width placeholder: "Location" input.onTextEdited: ed.set("location", input.text) } Rectangle { width: ed.width height: 56 radius: 8 color: Theme.surface TextEdit { id: notes anchors { fill: parent; margins: 8 } wrapMode: TextEdit.Wrap selectByMouse: true clip: true color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } onTextChanged: if (text !== ed.draft.notes) ed.set("notes", text) } Text { anchors { left: parent.left; top: parent.top; margins: 8 } visible: notes.text === "" && !notes.activeFocus text: "Notes" color: Theme.overlay font: notes.font } } Text { visible: ed.err !== "" width: ed.width wrapMode: Text.WordWrap text: ed.err color: Theme.red font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Item { width: ed.width height: 28 Chip { visible: ed.draft.uid !== "" label: ed.confirmDelete ? "Delete, really" : "Delete" tint: Theme.red on: ed.confirmDelete onClicked: if (!ed.busy) ed.remove() } Row { anchors.right: parent.right spacing: 6 Chip { label: "Cancel"; onClicked: ed.done() } Chip { label: ed.busy ? "Saving…" : "Save"; on: true; onClicked: if (!ed.busy) ed.save() } } } }