// The month grid from GNOME's date menu. // // Six week rows always, so the grid height never changes as you page through // months and the panel below it never jumps. Days belonging to the neighbouring // months fill the edges, dimmed. Today wears the accent. // // Days become real navigation targets now that Evolution Data Server backs the // panel. Up to three source-coloured markers show whether a date has events. import Quickshell import QtQuick import qs.config import qs.services Item { id: root signal dateActivated(date value) signal visibleMonthChanged(int year, int month) implicitHeight: body.implicitHeight // Cells divide the available width evenly; the height is fixed so the grid // stays roughly square regardless of how wide the panel gets. readonly property real cellWidth: root.width / 7 readonly property int cellHeight: 32 // Hours precision is enough: the only thing that has to change on its own // is which cell counts as "today", and that only moves at midnight. SystemClock { id: clock precision: SystemClock.Hours } readonly property int todayYear: clock.date.getFullYear() readonly property int todayMonth: clock.date.getMonth() readonly property int todayDay: clock.date.getDate() // The month currently on screen. DateMenu calls showToday() every time the // panel opens, so it never comes back showing wherever you paged off to. property int viewYear: root.todayYear property int viewMonth: root.todayMonth property date selectedDate: CalendarAgenda.selectedDate readonly property bool onCurrentMonth: root.viewYear === root.todayYear && root.viewMonth === root.todayMonth function showToday(): void { root.viewYear = root.todayYear; root.viewMonth = root.todayMonth; } function stepMonth(delta: int): void { // Built through Date rather than by hand so December -> January rolls // the year over for us. const d = new Date(root.viewYear, root.viewMonth + delta, 1); root.viewYear = d.getFullYear(); root.viewMonth = d.getMonth(); } function activateCell(model: var): void { if (!model.current) { root.viewYear = model.date.getFullYear(); root.viewMonth = model.date.getMonth(); } root.dateActivated(model.date); } onViewYearChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth) onViewMonthChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth) readonly property var cells: { const first = new Date(root.viewYear, root.viewMonth, 1); const offset = first.getDay(); // 0 = Sunday, matching the header row const out = []; for (let i = 0; i < 42; i++) { const date = new Date(root.viewYear, root.viewMonth, i - offset + 1); out.push({ date: date, day: date.getDate(), current: date.getMonth() === root.viewMonth && date.getFullYear() === root.viewYear }); } return out; } Column { id: body anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top spacing: 6 // ── Month header ──────────────────────────────────────────────────── // Both arrows sit on the right so the left slot is free for the "Today" // affordance, which only exists while you are looking at another month. Item { width: parent.width height: 30 Rectangle { id: todayPill anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter width: todayLabel.implicitWidth + 18 height: 24 radius: Theme.pillRadius border.width: 0 visible: !root.onCurrentMonth color: todayMouse.containsMouse ? Theme.alpha(Theme.accent, 0.30) : Theme.alpha(Theme.accent, 0.16) Behavior on color { ColorAnimation { duration: todayMouse.containsMouse ? Theme.durFast : Theme.durNormal } } Text { id: todayLabel anchors.centerIn: parent text: "Today" color: Theme.accent font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium } MouseArea { id: todayMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.showToday() } } // Centred on the row rather than between the pill and the arrows, // so it does not shift sideways when the pill appears. Text { anchors.centerIn: parent text: Qt.formatDateTime(new Date(root.viewYear, root.viewMonth, 1), "MMMM yyyy") font.family: Theme.fontFamily // The year ticks in place when you page across a boundary. font.features: Theme.tabularFigures font.pixelSize: Theme.fontSizeLarge font.weight: Font.DemiBold color: Theme.fg } Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 2 MonthArrow { glyph: "\u{F0141}" // md-chevron_left onActivated: root.stepMonth(-1) } MonthArrow { glyph: "\u{F0142}" // md-chevron_right onActivated: root.stepMonth(1) } } } // ── Weekday header ────────────────────────────────────────────────── Row { width: parent.width Repeater { model: ["S", "M", "T", "W", "T", "F", "S"] delegate: Text { required property string modelData width: root.cellWidth height: 22 text: modelData horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall font.weight: Font.Medium color: Theme.fgMuted } } } // ── Day grid ──────────────────────────────────────────────────────── Grid { width: parent.width columns: 7 Repeater { model: root.cells delegate: Item { id: cell required property var modelData readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.onCurrentMonth readonly property bool isSelected: CalendarAgenda.dateKey(cell.modelData.date) === CalendarAgenda.dateKey(root.selectedDate) readonly property var markers: CalendarAgenda.markersForDate(cell.modelData.date).slice(0, 3) width: root.cellWidth height: root.cellHeight Rectangle { anchors.centerIn: parent width: root.cellHeight - 4 height: root.cellHeight - 4 radius: width / 2 border.width: 0 visible: cell.isToday || cell.isSelected || cellMouse.containsMouse color: { if (cell.isToday) return Theme.accent; if (cell.isSelected) return Theme.alpha(Theme.accent, 0.20); return Theme.alpha(Theme.fg, 0.08); } Behavior on color { ColorAnimation { duration: Theme.durFast } } } Text { anchors.centerIn: parent text: cell.modelData.day font.family: Theme.fontFamily font.features: Theme.tabularFigures font.pixelSize: Theme.fontSize font.weight: cell.isToday ? Font.DemiBold : Font.Normal color: { if (cell.isToday) return Theme.bgDark; if (cell.isSelected) return Theme.accent; return cell.modelData.current ? Theme.fg : Theme.fgMuted; } } Row { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: 1 spacing: 2 Repeater { model: cell.markers Rectangle { required property var modelData width: 3 height: 3 radius: 2 border.width: 0 color: cell.isToday ? Theme.bgDark : modelData.color } } } MouseArea { id: cellMouse anchors.fill: parent hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: root.activateCell(cell.modelData) } } } } } Component.onCompleted: root.visibleMonthChanged(root.viewYear, root.viewMonth) }