diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-13 10:25:59 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-13 10:25:59 +0200 |
| commit | 47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee (patch) | |
| tree | 9fe90a0fe33e512ba1d2afa87bb5889b52173408 | |
| parent | 06b7d7c77c15f24a00cfabb1925a7718cd97210f (diff) | |
| download | quickshell-47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee.tar.gz quickshell-47a97f0717fc54b5eb33c266d8dbafdc8d3c15ee.zip | |
feat(calendar): month grid beside the day's agenda
Six weeks from Monday, a dot per calendar with something that day, today
and the selection marked. The agenda lists the selected day, all-day
first; a click opens the event in the editor, and while the editor is
open the grid picks its dates instead of the selection. Escape closes
the editor before the panel.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
| -rw-r--r-- | desktop/island/calendar/CalendarPanel.qml | 323 |
1 files changed, 323 insertions, 0 deletions
diff --git a/desktop/island/calendar/CalendarPanel.qml b/desktop/island/calendar/CalendarPanel.qml new file mode 100644 index 0000000..9515a9a --- /dev/null +++ b/desktop/island/calendar/CalendarPanel.qml @@ -0,0 +1,323 @@ +// Copyright (C) 2026 Danilo M. <danix@danix.xyz> +// +// 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 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 escape() { + if (panel.editing) { panel.closeEditor(); return true; } + return false; + } + + Keys.onEscapePressed: if (!panel.escape()) 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: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } + elide: Text.ElideRight + text: row.modelData.title + color: Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } + } + 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() + } + } +} |
