// 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 Quickshell import Quickshell.Widgets import QtQuick import "../.." import "Cal.js" as Cal // Month grid on the left, the selected day's agenda on the right. Opening // an event swaps the agenda for the editor, and the grid becomes its date // picker. Item { id: panel signal closeRequested() property int viewY: 2026 property int viewM0: 0 property string selected: "" property bool editing: false property string agendaError: "" readonly property var cells: Cal.monthCells(viewY, viewM0) readonly property real gridWidth: 252 implicitHeight: Math.max(left.implicitHeight, right.implicitHeight) SystemClock { id: clock; precision: SystemClock.Minutes } readonly property string todayIso: Cal.today(clock.date) function colorOf(c) { return c.startsWith("#") ? c : Theme[c]; } function show(y, m0) { panel.viewY = y; panel.viewM0 = m0; const cells = Cal.monthCells(y, m0); Calendar.load(cells[0].iso, Cal.addDays(cells[41].iso, 1)); } // Each time the panel opens: today, its month, no editor. function reset() { const now = new Date(); panel.editing = false; panel.agendaError = ""; panel.selected = Cal.today(now); panel.show(now.getFullYear(), now.getMonth()); } function shift(n) { const m = Cal.shiftMonth(panel.viewY, panel.viewM0, n); panel.show(m.y, m.m0); } function clickDay(day, shiftKey) { if (panel.editing) { editor.pick(day, shiftKey); return; } panel.selected = day; if (day.slice(0, 7) !== Cal.monthCells(panel.viewY, panel.viewM0)[10].iso.slice(0, 7)) panel.show(+day.slice(0, 4), +day.slice(5, 7) - 1); } function openEvent(uid) { Calendar.get(uid, r => { if (!r.ok) { panel.agendaError = r.error; return; } editor.load(Cal.fromEvent(r.data)); panel.editing = true; }); } function newEvent() { editor.load(Cal.newDraft(panel.selected, new Date(), Calendar.defaultCalendar)); panel.editing = true; } function closeEditor() { panel.editing = false; panel.forceActiveFocus(); } // Escape: the editor first, then the panel. function back() { if (panel.editing) { panel.closeEditor(); return true; } return false; } Keys.onEscapePressed: if (!panel.back()) panel.closeRequested() Component.onCompleted: panel.reset() // --- left: the month --------------------------------------------------- Column { id: left width: panel.gridWidth spacing: 6 Item { width: parent.width height: 22 Text { anchors.verticalCenter: parent.verticalCenter text: Qt.locale().standaloneMonthName(panel.viewM0) + " " + panel.viewY color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } } Row { anchors { right: parent.right; verticalCenter: parent.verticalCenter } spacing: 12 Repeater { model: [{ t: "‹", n: -1 }, { t: "›", n: 1 }] delegate: Text { required property var modelData text: modelData.t color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize + 2 } MouseArea { anchors { fill: parent; margins: -6 } cursorShape: Qt.PointingHandCursor onClicked: panel.shift(parent.modelData.n) } } } } } Grid { columns: 7 spacing: 2 Repeater { model: [0, 1, 2, 3, 4, 5, 6] delegate: Text { required property int modelData width: (panel.gridWidth - 12) / 7 horizontalAlignment: Text.AlignHCenter // Monday first: Qt.locale().dayName counts from Sunday. text: Qt.locale().dayName((modelData + 1) % 7, Locale.NarrowFormat) color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } } Repeater { model: panel.cells delegate: Rectangle { id: cell required property var modelData readonly property bool inDraft: panel.editing && modelData.iso >= editor.draft.startDay && modelData.iso <= editor.draft.endDay width: (panel.gridWidth - 12) / 7 height: 32 radius: 8 color: inDraft ? Qt.alpha(Theme.accent, 0.35) : hover.containsMouse ? Qt.alpha(Theme.surface, 0.8) : "transparent" border.width: !panel.editing && modelData.iso === panel.selected ? 1 : 0 border.color: Theme.accent Text { anchors { horizontalCenter: parent.horizontalCenter; top: parent.top; topMargin: 5 } text: cell.modelData.day color: cell.modelData.iso === panel.todayIso ? Theme.accent : cell.modelData.inMonth ? Theme.text : Theme.overlay font { family: Theme.fontFamily pixelSize: Theme.fontSize - 4 bold: cell.modelData.iso === panel.todayIso } } Row { anchors { horizontalCenter: parent.horizontalCenter; bottom: parent.bottom; bottomMargin: 4 } spacing: 2 Repeater { model: Cal.dots(Calendar.events, cell.modelData.iso, Calendar.calendars) delegate: Rectangle { required property var modelData width: 4 height: 4 radius: 2 color: panel.colorOf(modelData.color) } } } MouseArea { id: hover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: mouse => panel.clickDay(cell.modelData.iso, (mouse.modifiers & Qt.ShiftModifier) !== 0) } } } } } // --- right: agenda or editor ------------------------------------------- Item { id: right x: panel.gridWidth + 16 width: panel.width - x implicitHeight: panel.editing ? editor.implicitHeight : agenda.implicitHeight Column { id: agenda width: parent.width spacing: 6 visible: !panel.editing Text { text: panel.selected === "" ? "" : Qt.formatDate(new Date(+panel.selected.slice(0, 4), +panel.selected.slice(5, 7) - 1, +panel.selected.slice(8, 10)), "dddd d MMMM") color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } } Text { visible: text !== "" width: parent.width wrapMode: Text.WordWrap text: panel.agendaError || Calendar.error color: Theme.red font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } Repeater { id: dayList model: Cal.dayEvents(Calendar.events, panel.selected) delegate: Rectangle { id: row required property var modelData width: agenda.width height: 30 radius: 8 color: rowHover.containsMouse ? Theme.surfaceAlt : Theme.surface Rectangle { x: 6 anchors.verticalCenter: parent.verticalCenter width: 3 height: parent.height - 12 radius: 2 color: panel.colorOf(row.modelData.color) } Text { id: when x: 16 width: 82 anchors.verticalCenter: parent.verticalCenter text: Cal.timeLabel(row.modelData, panel.selected) color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } Text { anchors { left: when.right; right: bell.left; rightMargin: 6; verticalCenter: parent.verticalCenter } elide: Text.ElideRight text: row.modelData.title color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } // The event's own alarm, so a day can be checked without // opening each event. cal-notif's per-calendar defaults are // not shown: they are its config, not the event's. IconImage { id: bell anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } visible: row.modelData.alarm !== "none" implicitSize: visible ? 14 : 0 source: Quickshell.iconPath("bell", true) } MouseArea { id: rowHover anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: panel.openEvent(row.modelData.uid) } } } Text { visible: dayList.count === 0 text: "Nothing on this day" color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Rectangle { width: agenda.width height: 30 radius: 8 color: "transparent" border.width: 1 border.color: Theme.surfaceAlt Text { anchors.centerIn: parent text: "+ New event" color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: panel.newEvent() } } } EventEditor { id: editor width: parent.width visible: panel.editing onDone: panel.closeEditor() } } }