From 3164e50b369d1c0f906806019307e736c0b8355c Mon Sep 17 00:00:00 2001 From: SirBlobby Date: Thu, 24 Sep 2026 12:37:27 -0400 Subject: [PATCH] Share one calendar with day selection and a month picker --- docs/shell.md | 27 ++ shell/Ui/CalendarDays.qml | 176 ++++++++++ shell/Ui/CalendarModel.js | 180 ++++++++++ shell/Ui/CalendarMonthPicker.qml | 65 ++++ shell/Ui/CalendarView.qml | 251 ++++++++++++++ shell/Ui/qmldir | 4 + shell/plugins/panels/clock/BarWidget.qml | 2 +- shell/plugins/panels/clock/Model.js | 171 +-------- shell/plugins/panels/clock/Panel.qml | 327 +++--------------- .../panels/quick-settings/Calendar.qml | 113 ++---- 10 files changed, 773 insertions(+), 543 deletions(-) create mode 100644 shell/Ui/CalendarDays.qml create mode 100644 shell/Ui/CalendarModel.js create mode 100644 shell/Ui/CalendarMonthPicker.qml create mode 100644 shell/Ui/CalendarView.qml diff --git a/docs/shell.md b/docs/shell.md index 12792f2..9b91090 100644 --- a/docs/shell.md +++ b/docs/shell.md @@ -61,6 +61,33 @@ notifications are silenced. The centre module named by `centerAnchor` cannot be dragged out of the centre. Every other widget still reorders. +## Calendar + +The clock's calendar popup and the Quick Settings card share one component, +`CalendarView` in `shell/Ui`, with its date math in `Ui/CalendarModel.js`. + +- Today is filled with the accent color; days from the neighbouring months are + dimmed. +- Clicking a day selects it, and the line under the grid gives its weekday, + day of the year, and how far it is from today. Clicking a day from the + previous or next month moves the view there. +- Clicking the month label opens a month picker; the chevrons then step whole + years, and clicking a month jumps to it. +- The scroll wheel steps months, or years while the picker is open. + +Keys in the clock popup: + +| Key | Action | +| --- | --- | +| arrows / `hjkl` | move the selected day (a week up or down), or the month cursor in the picker | +| `[` `]` | previous or next month | +| `{` `}` | previous or next year | +| `m` | open or close the month picker | +| `t` | back to today | +| `w` | toggle the week start between Monday and Sunday | +| Enter | back to today, or pick the highlighted month | +| Esc | close the month picker, then the popup | + ## Differences from upstream This shell was forked from Omarchy 4.0.4 and is not tracked against it. The diff --git a/shell/Ui/CalendarDays.qml b/shell/Ui/CalendarDays.qml new file mode 100644 index 0000000..17ffa36 --- /dev/null +++ b/shell/Ui/CalendarDays.qml @@ -0,0 +1,176 @@ +import QtQuick +import qs.Commons +import "CalendarModel.js" as CalendarModel + +Item { + id: root + + required property var calendar + + readonly property var weeks: CalendarModel.monthGrid(calendar.viewYear, calendar.viewMonth, + calendar.weekStart, calendar.todayKey, calendar.selectedKey) + readonly property var weekdays: CalendarModel.weekdayOrder(calendar.weekStart) + readonly property color foreground: calendar.foreground + readonly property real headerHeight: Style.space(16) + + width: grid.width + height: grid.height + + Column { + id: grid + spacing: Style.space(3) + + Row { + id: headerRow + spacing: root.calendar.cellSpacing + + Rectangle { + visible: root.calendar.showWeekNumbers + width: root.calendar.weekColumnWidth + height: root.headerHeight + radius: Style.cornerRadius + color: weekHeaderMouse.containsMouse ? Style.hoverFillFor(root.foreground, Color.accent) : "transparent" + + Text { + anchors.centerIn: parent + text: "W" + color: weekHeaderMouse.containsMouse + ? Style.hoverStateColor(root.foreground, Color.accent) + : Qt.darker(root.foreground, 1.9) + font.family: root.calendar.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + font.bold: true + } + + MouseArea { + id: weekHeaderMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.calendar.weekHeaderClicked() + } + + PanelToolTip { + visible: weekHeaderMouse.containsMouse && root.calendar.weekHeaderTooltip !== "" + text: root.calendar.weekHeaderTooltip + fontFamily: root.calendar.fontFamily + } + } + + Item { + visible: root.calendar.showWeekNumbers + width: root.calendar.gutterWidth + height: root.headerHeight + } + + Repeater { + model: root.weekdays + + Text { + required property var modelData + width: root.calendar.cellWidth + height: root.headerHeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + textFormat: Text.PlainText + text: root.calendar.weekdayLabel(modelData) + color: root.calendar.mutedForeground + font.family: root.calendar.fontFamily + font.pixelSize: Style.font.caption + font.letterSpacing: 1 + font.bold: true + } + } + } + + Repeater { + model: root.weeks + + Row { + required property var modelData + spacing: root.calendar.cellSpacing + + Text { + visible: root.calendar.showWeekNumbers + width: root.calendar.weekColumnWidth + height: root.calendar.cellHeight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + textFormat: Text.PlainText + text: modelData.week + color: Qt.darker(root.foreground, 1.9) + font.family: root.calendar.fontFamily + font.pixelSize: Style.font.caption + } + + Item { + visible: root.calendar.showWeekNumbers + width: root.calendar.gutterWidth + height: root.calendar.cellHeight + } + + Repeater { + model: modelData.days + + DayCell {} + } + } + } + } + + Rectangle { + visible: root.calendar.showWeekNumbers + x: root.calendar.weekColumnWidth + root.calendar.cellSpacing + + Math.round((root.calendar.gutterWidth - width) / 2) + y: headerRow.height + grid.spacing + width: Style.spacing.hairline + height: grid.height - headerRow.height - grid.spacing + color: root.foreground + opacity: 0.1 + } + + component DayCell: Rectangle { + id: cell + + required property var modelData + + readonly property color todayFill: Style.selectedStateColor(root.foreground, Color.accent) + readonly property color dayColor: { + if (modelData.today) return Color.background + if (!modelData.inMonth) return root.foreground + return modelData.weekend ? Qt.darker(root.foreground, 1.45) : root.foreground + } + + width: root.calendar.cellWidth + height: root.calendar.cellHeight + radius: Style.cornerRadius + color: { + if (modelData.today) return todayFill + if (dayMouse.containsMouse) return Style.hoverFillFor(root.foreground, Color.accent) + if (modelData.selected) return Style.selectedFillFor(root.foreground, Color.accent) + return "transparent" + } + border.width: modelData.selected && !modelData.today ? Math.max(1, Style.spacing.hairline) : 0 + border.color: Style.selectedBorderFor(root.foreground, Color.accent) + + Text { + anchors.centerIn: parent + textFormat: Text.PlainText + text: cell.modelData.day + color: cell.dayColor + opacity: cell.modelData.inMonth || cell.modelData.today ? 1 : 0.3 + font.family: root.calendar.fontFamily + font.pixelSize: Style.font.body + font.bold: cell.modelData.today || cell.modelData.selected + } + + MouseArea { + id: dayMouse + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.calendar.selectDay(cell.modelData.year, cell.modelData.month, cell.modelData.day) + } + } +} diff --git a/shell/Ui/CalendarModel.js b/shell/Ui/CalendarModel.js new file mode 100644 index 0000000..f024701 --- /dev/null +++ b/shell/Ui/CalendarModel.js @@ -0,0 +1,180 @@ +.pragma library + +var MS_PER_DAY = 86400000 + +var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] + +function pad2(value) { + var n = Number(value) + return (n < 10 ? "0" : "") + n +} + +function dateKey(year, month, day) { + return year + "-" + pad2(Number(month) + 1) + "-" + pad2(day) +} + +function keyForDate(date) { + return dateKey(date.getFullYear(), date.getMonth(), date.getDate()) +} + +function coerceWeekStart(value) { + if (value === undefined || value === null) return null + if (typeof value === "number") + return isFinite(value) ? ((Math.round(value) % 7) + 7) % 7 : null + + var text = String(value).replace(/^\s+|\s+$/g, "").toLowerCase() + if (text === "") return null + + for (var i = 0; i < WEEKDAY_NAMES.length; i++) + if (WEEKDAY_NAMES[i] === text || WEEKDAY_NAMES[i].substr(0, 3) === text) return i + + var parsed = parseInt(text, 10) + return isFinite(parsed) ? ((parsed % 7) + 7) % 7 : null +} + +function normalizedWeekStart(value, fallback) { + var configured = coerceWeekStart(value) + if (configured !== null) return configured + var fallbackStart = coerceWeekStart(fallback) + return fallbackStart === null ? 1 : fallbackStart +} + +function weekStartSettingName(index) { + return WEEKDAY_NAMES[normalizedWeekStart(index, 1)] +} + +function toggledWeekStart(index) { + return normalizedWeekStart(index, 1) === 1 ? 0 : 1 +} + +function weekdayOrder(weekStart) { + var start = normalizedWeekStart(weekStart, 1) + var order = [] + for (var i = 0; i < 7; i++) order.push((start + i) % 7) + return order +} + +function isoWeek(year, month, day) { + var date = new Date(Date.UTC(year, month, day)) + var weekday = date.getUTCDay() || 7 + date.setUTCDate(date.getUTCDate() + 4 - weekday) + var yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) + return Math.ceil(((date.getTime() - yearStart.getTime()) / MS_PER_DAY + 1) / 7) +} + +function isoWeekLiteral(year, month, day) { + return pad2(isoWeek(year, month, day)) +} + +function dayOfYear(year, month, day) { + return Math.round((Date.UTC(year, month, day) - Date.UTC(year, 0, 1)) / MS_PER_DAY) + 1 +} + +function daysInYear(year) { + return dayOfYear(year, 11, 31) +} + +function daysInMonth(year, month) { + return new Date(year, Number(month) + 1, 0).getDate() +} + +function yearProgress(year, month, day) { + var total = daysInYear(year) + if (total <= 0) return 0 + return Math.max(0, Math.min(1, (dayOfYear(year, month, day) - 1) / total)) +} + +function yearProgressPercent(year, month, day) { + return Math.round(yearProgress(year, month, day) * 100) +} + +function stepMonth(year, month, delta) { + var target = new Date(year, Number(month) + Number(delta), 1) + return { year: target.getFullYear(), month: target.getMonth() } +} + +function stepDay(year, month, day, delta) { + var target = new Date(year, month, Number(day) + Number(delta)) + return { year: target.getFullYear(), month: target.getMonth(), day: target.getDate() } +} + +function clampDay(year, month, day) { + return Math.max(1, Math.min(Number(day), daysInMonth(year, month))) +} + +function daysBetween(fromYear, fromMonth, fromDay, toYear, toMonth, toDay) { + return Math.round((Date.UTC(toYear, toMonth, toDay) - Date.UTC(fromYear, fromMonth, fromDay)) / MS_PER_DAY) +} + +function relativeDayLabel(offset) { + if (offset === 0) return "Today" + if (offset === 1) return "Tomorrow" + if (offset === -1) return "Yesterday" + if (offset > 0) return "In " + offset + " days" + return (-offset) + " days ago" +} + +function monthGrid(year, month, weekStart, todayKey, selectedKey) { + var start = normalizedWeekStart(weekStart, 1) + var leading = (new Date(year, month, 1).getDay() - start + 7) % 7 + var cursor = new Date(year, month, 1 - leading) + var today = String(todayKey || "") + var selected = String(selectedKey || "") + var weeks = [] + + for (var row = 0; row < 6; row++) { + var days = [] + var thursday = null + for (var column = 0; column < 7; column++) { + var cellYear = cursor.getFullYear() + var cellMonth = cursor.getMonth() + var cellDay = cursor.getDate() + var weekday = cursor.getDay() + var key = dateKey(cellYear, cellMonth, cellDay) + if (weekday === 4) thursday = { year: cellYear, month: cellMonth, day: cellDay } + days.push({ + key: key, + year: cellYear, + month: cellMonth, + day: cellDay, + weekday: weekday, + inMonth: cellMonth === month && cellYear === year, + weekend: weekday === 0 || weekday === 6, + today: key === today, + selected: key === selected + }) + cursor.setDate(cursor.getDate() + 1) + } + var anchor = thursday || days[0] + weeks.push({ + week: isoWeek(anchor.year, anchor.month, anchor.day), + days: days + }) + } + return weeks +} + +if (typeof module !== "undefined") { + module.exports = { + pad2: pad2, + dateKey: dateKey, + keyForDate: keyForDate, + normalizedWeekStart: normalizedWeekStart, + weekStartSettingName: weekStartSettingName, + toggledWeekStart: toggledWeekStart, + weekdayOrder: weekdayOrder, + isoWeek: isoWeek, + isoWeekLiteral: isoWeekLiteral, + dayOfYear: dayOfYear, + daysInYear: daysInYear, + daysInMonth: daysInMonth, + yearProgress: yearProgress, + yearProgressPercent: yearProgressPercent, + stepMonth: stepMonth, + stepDay: stepDay, + clampDay: clampDay, + daysBetween: daysBetween, + relativeDayLabel: relativeDayLabel, + monthGrid: monthGrid + } +} diff --git a/shell/Ui/CalendarMonthPicker.qml b/shell/Ui/CalendarMonthPicker.qml new file mode 100644 index 0000000..b746b72 --- /dev/null +++ b/shell/Ui/CalendarMonthPicker.qml @@ -0,0 +1,65 @@ +import QtQuick +import qs.Commons + +Item { + id: root + + required property var calendar + + readonly property int columns: 4 + readonly property int rows: 3 + readonly property real gap: calendar.cellSpacing * 2 + readonly property color foreground: calendar.foreground + + Grid { + anchors.fill: parent + columns: root.columns + spacing: root.gap + + Repeater { + model: 12 + + Rectangle { + id: monthCell + + required property int index + + readonly property bool isCurrentMonth: root.calendar.today.getFullYear() === root.calendar.pickerYear + && root.calendar.today.getMonth() === index + readonly property bool isViewedMonth: root.calendar.viewYear === root.calendar.pickerYear + && root.calendar.viewMonth === index + readonly property bool isCursor: root.calendar.pickerMonth === index + + width: (root.width - root.gap * (root.columns - 1)) / root.columns + height: (root.height - root.gap * (root.rows - 1)) / root.rows + radius: Style.cornerRadius + color: { + if (isCurrentMonth) return Style.selectedStateColor(root.foreground, Color.accent) + if (isCursor) return Style.hoverFillFor(root.foreground, Color.accent) + return "transparent" + } + border.width: isViewedMonth && !isCurrentMonth ? Math.max(1, Style.spacing.hairline) : 0 + border.color: Style.selectedBorderFor(root.foreground, Color.accent) + + Text { + anchors.centerIn: parent + textFormat: Text.PlainText + text: root.calendar.monthLabel(monthCell.index) + color: monthCell.isCurrentMonth ? Color.background : root.foreground + font.family: root.calendar.fontFamily + font.pixelSize: Style.font.body + font.letterSpacing: 1 + font.bold: monthCell.isCurrentMonth || monthCell.isViewedMonth + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onContainsMouseChanged: if (containsMouse) root.calendar.pickerMonth = monthCell.index + onClicked: root.calendar.choosePickerMonth(root.calendar.pickerYear, monthCell.index) + } + } + } + } +} diff --git a/shell/Ui/CalendarView.qml b/shell/Ui/CalendarView.qml new file mode 100644 index 0000000..9eed724 --- /dev/null +++ b/shell/Ui/CalendarView.qml @@ -0,0 +1,251 @@ +import QtQuick +import qs.Commons +import "CalendarModel.js" as CalendarModel + +Column { + id: root + + property date today: new Date() + property int weekStart: CalendarModel.normalizedWeekStart(null, Qt.locale().firstDayOfWeek) + property color foreground: Color.foreground + property string fontFamily: Style.font.family + property bool showWeekNumbers: false + property bool navigationAtBottom: false + property string weekHeaderTooltip: "" + + property real cellWidth: Style.space(36) + property real cellHeight: Style.space(28) + property real cellSpacing: Style.space(2) + property real weekColumnWidth: Style.space(32) + property real gutterWidth: Style.space(14) + + property int viewYear: today.getFullYear() + property int viewMonth: today.getMonth() + property int selectedYear: today.getFullYear() + property int selectedMonth: today.getMonth() + property int selectedDay: today.getDate() + + property bool pickingMonth: false + property int pickerYear: viewYear + property int pickerMonth: viewMonth + + readonly property string todayKey: CalendarModel.keyForDate(today) + readonly property string selectedKey: CalendarModel.dateKey(selectedYear, selectedMonth, selectedDay) + readonly property bool selectionIsToday: selectedKey === todayKey + readonly property bool viewingCurrentMonth: viewYear === today.getFullYear() && viewMonth === today.getMonth() + readonly property date viewDate: new Date(viewYear, viewMonth, 1) + readonly property date selectedDate: new Date(selectedYear, selectedMonth, selectedDay) + readonly property int selectedOffset: CalendarModel.daysBetween( + today.getFullYear(), today.getMonth(), today.getDate(), selectedYear, selectedMonth, selectedDay) + readonly property var labelLocale: Qt.locale("en_US") + readonly property real gridWidth: body.width + readonly property color mutedForeground: Qt.darker(foreground, 1.5) + + signal weekHeaderClicked() + + spacing: Style.space(8) + + function weekdayLabel(weekday) { + return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase() + } + + function monthLabel(month) { + return String(labelLocale.monthName(month, Locale.ShortFormat)).toUpperCase() + } + + function selectDay(year, month, day) { + selectedYear = year + selectedMonth = month + selectedDay = day + viewYear = year + viewMonth = month + pickingMonth = false + } + + function goToToday() { + selectDay(today.getFullYear(), today.getMonth(), today.getDate()) + } + + function moveMonth(delta) { + var next = CalendarModel.stepMonth(viewYear, viewMonth, delta) + selectDay(next.year, next.month, CalendarModel.clampDay(next.year, next.month, selectedDay)) + } + + function moveYear(delta) { + if (pickingMonth) pickerYear += delta + else moveMonth(delta * 12) + } + + function moveSelection(days) { + var next = CalendarModel.stepDay(selectedYear, selectedMonth, selectedDay, days) + selectDay(next.year, next.month, next.day) + } + + function movePickerCursor(months) { + var next = CalendarModel.stepMonth(pickerYear, pickerMonth, months) + pickerYear = next.year + pickerMonth = next.month + } + + function handleMove(dx, dy) { + if (pickingMonth) movePickerCursor(dx + dy * 4) + else moveSelection(dx + dy * 7) + } + + function openPicker() { + pickerYear = viewYear + pickerMonth = viewMonth + pickingMonth = true + } + + function togglePicker() { + if (pickingMonth) pickingMonth = false + else openPicker() + } + + function choosePickerMonth(year, month) { + selectDay(year, month, CalendarModel.clampDay(year, month, selectedDay)) + } + + function activate() { + if (pickingMonth) choosePickerMonth(pickerYear, pickerMonth) + else goToToday() + } + + function dismissPicker() { + if (!pickingMonth) return false + pickingMonth = false + return true + } + + function selectedSummary() { + var format = selectedYear === today.getFullYear() ? "dddd, MMMM d" : "dddd, MMMM d, yyyy" + return Qt.formatDate(selectedDate, format) + + " · Day " + CalendarModel.dayOfYear(selectedYear, selectedMonth, selectedDay) + + " · " + CalendarModel.relativeDayLabel(selectedOffset) + } + + MonthNavigation { + visible: !root.navigationAtBottom + } + + Item { + width: root.width + height: body.height + + Item { + id: body + anchors.horizontalCenter: parent.horizontalCenter + width: days.width + height: days.height + + CalendarDays { + id: days + calendar: root + visible: !root.pickingMonth + } + + CalendarMonthPicker { + anchors.fill: parent + calendar: root + visible: root.pickingMonth + } + + WheelHandler { + onWheel: function(event) { + if (event.angleDelta.y === 0) return + var step = event.angleDelta.y > 0 ? -1 : 1 + if (root.pickingMonth) root.pickerYear += step + else root.moveMonth(step) + } + } + } + } + + Text { + width: root.width + horizontalAlignment: Text.AlignHCenter + textFormat: Text.PlainText + elide: Text.ElideRight + text: root.selectedSummary() + color: root.mutedForeground + font.family: root.fontFamily + font.pixelSize: Style.font.bodySmall + } + + MonthNavigation { + visible: root.navigationAtBottom + } + + component MonthNavigation: Item { + width: root.width + height: visible ? navigationLabel.implicitHeight + Style.space(10) : 0 + + Item { + anchors.horizontalCenter: parent.horizontalCenter + width: body.width + height: parent.height + + Rectangle { + anchors.centerIn: navigationLabel + width: navigationLabel.width + height: navigationLabel.implicitHeight + Style.space(6) + radius: Style.cornerRadius + color: navigationMouse.containsMouse ? Style.hoverFillFor(root.foreground, Color.accent) : "transparent" + } + + Text { + id: navigationLabel + anchors.centerIn: parent + width: Style.space(150) + horizontalAlignment: Text.AlignHCenter + textFormat: Text.PlainText + text: root.pickingMonth + ? String(root.pickerYear) + : Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase() + color: navigationMouse.containsMouse + ? Style.hoverStateColor(root.foreground, Color.accent) + : Qt.darker(root.foreground, 1.4) + font.family: root.fontFamily + font.pixelSize: Style.font.body + font.letterSpacing: 1 + } + + MouseArea { + id: navigationMouse + anchors.fill: navigationLabel + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.togglePicker() + + PanelToolTip { + visible: navigationMouse.containsMouse + text: root.pickingMonth ? "Back to the days" : "Pick a month" + fontFamily: root.fontFamily + } + } + + PanelActionButton { + anchors.left: parent.left + anchors.leftMargin: -Style.space(8) + anchors.verticalCenter: parent.verticalCenter + iconText: "󰅁" + tooltipText: root.pickingMonth ? "Previous year" : "Previous month" + foreground: root.foreground + fontFamily: root.fontFamily + onClicked: root.pickingMonth ? root.moveYear(-1) : root.moveMonth(-1) + } + + PanelActionButton { + anchors.right: parent.right + anchors.rightMargin: -Style.space(8) + anchors.verticalCenter: parent.verticalCenter + iconText: "󰅂" + tooltipText: root.pickingMonth ? "Next year" : "Next month" + foreground: root.foreground + fontFamily: root.fontFamily + onClicked: root.pickingMonth ? root.moveYear(1) : root.moveMonth(1) + } + } + } +} diff --git a/shell/Ui/qmldir b/shell/Ui/qmldir index d1c61e9..8b3457c 100644 --- a/shell/Ui/qmldir +++ b/shell/Ui/qmldir @@ -38,3 +38,7 @@ CardHeader 1.0 CardHeader.qml StatRow 1.0 StatRow.qml CardIconButton 1.0 CardIconButton.qml Chip 1.0 Chip.qml +CalendarModel 1.0 CalendarModel.js +CalendarView 1.0 CalendarView.qml +CalendarDays 1.0 CalendarDays.qml +CalendarMonthPicker 1.0 CalendarMonthPicker.qml diff --git a/shell/plugins/panels/clock/BarWidget.qml b/shell/plugins/panels/clock/BarWidget.qml index e7a9f82..8424fa2 100644 --- a/shell/plugins/panels/clock/BarWidget.qml +++ b/shell/plugins/panels/clock/BarWidget.qml @@ -53,7 +53,7 @@ BarWidget { } function formatted(date) { - return Qt.formatDateTime(date, activeFormat.replace(/ww/g, Model.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate()))) + return Qt.formatDateTime(date, activeFormat.replace(/ww/g, CalendarModel.isoWeekLiteral(date.getFullYear(), date.getMonth(), date.getDate()))) } // ---- Calendar popup. Shape contract for shell.summon/hide/toggle diff --git a/shell/plugins/panels/clock/Model.js b/shell/plugins/panels/clock/Model.js index 9607a4a..c3c5f31 100644 --- a/shell/plugins/panels/clock/Model.js +++ b/shell/plugins/panels/clock/Model.js @@ -1,13 +1,5 @@ -// Pure date and format math for the clock widget and its calendar panel. -// Everything here is locale- and Qt-free so it can be unit tested under node -// (test/shell.d/clock-test.sh); the QML owns month/weekday naming through -// Qt.locale(). - -var MS_PER_DAY = 86400000 - -// Weekday indices match both JS Date.getDay() and QML's Locale.Sunday… -// Locale.Saturday, so a locale's firstDayOfWeek can be passed straight in. -var WEEKDAY_NAMES = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"] +// Clock label formats and the memento mori bar. Calendar date math lives in +// qs.Ui's CalendarModel.js, shared with the Quick Settings calendar. // ---- Bar label formats. Right-clicking the clock walks these in order and // writes the result back to shell.json, so the label the bar shows and @@ -67,99 +59,6 @@ function nextClockFormat(ring, current) { return ring[(index + 1) % ring.length] } -// Two-digit ISO week, substituted into a format's 'ww' token before Qt -// formats it -- Qt has no ISO week specifier of its own. -function isoWeekLiteral(year, month, day) { - return pad2(isoWeek(year, month, day)) -} - -function pad2(value) { - var n = Number(value) - return (n < 10 ? "0" : "") + n -} - -// Stable "yyyy-MM-dd" identity for a day, so a grid cell can be compared -// against today without dragging Date objects through bindings. -function dateKey(year, month, day) { - return year + "-" + pad2(Number(month) + 1) + "-" + pad2(day) -} - -function keyForDate(date) { - return dateKey(date.getFullYear(), date.getMonth(), date.getDate()) -} - -function coerceWeekStart(value) { - if (value === undefined || value === null) return null - if (typeof value === "number") - return isFinite(value) ? ((Math.round(value) % 7) + 7) % 7 : null - - var text = String(value).replace(/^\s+|\s+$/g, "").toLowerCase() - if (text === "") return null - - for (var i = 0; i < WEEKDAY_NAMES.length; i++) - if (WEEKDAY_NAMES[i] === text || WEEKDAY_NAMES[i].substr(0, 3) === text) return i - - var parsed = parseInt(text, 10) - return isFinite(parsed) ? ((parsed % 7) + 7) % 7 : null -} - -// Configured week start, falling back to the locale's own first day when -// the setting is missing or nonsense. -function normalizedWeekStart(value, fallback) { - var configured = coerceWeekStart(value) - if (configured !== null) return configured - var fallbackStart = coerceWeekStart(fallback) - return fallbackStart === null ? 1 : fallbackStart -} - -function weekStartSettingName(index) { - return WEEKDAY_NAMES[normalizedWeekStart(index, 1)] -} - -// The toggle flips between the two conventions people actually switch -// between. A calendar configured to any other start (Saturday, say) is -// shown as-is and lands on Monday the first time it is toggled. -function toggledWeekStart(index) { - return normalizedWeekStart(index, 1) === 1 ? 0 : 1 -} - -function weekdayOrder(weekStart) { - var start = normalizedWeekStart(weekStart, 1) - var out = [] - for (var i = 0; i < 7; i++) out.push((start + i) % 7) - return out -} - -// ISO-8601 week number: the week owning the Thursday of that date's -// Monday-based week. Mirrors the clock widget's 'ww' format token. -function isoWeek(year, month, day) { - var date = new Date(Date.UTC(year, month, day)) - var weekday = date.getUTCDay() || 7 - date.setUTCDate(date.getUTCDate() + 4 - weekday) - var yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) - return Math.ceil(((date.getTime() - yearStart.getTime()) / MS_PER_DAY + 1) / 7) -} - -function dayOfYear(year, month, day) { - return Math.round((Date.UTC(year, month, day) - Date.UTC(year, 0, 1)) / MS_PER_DAY) + 1 -} - -function daysInYear(year) { - return dayOfYear(year, 11, 31) -} - -// Share of the year already behind you: whole days completed over days in -// the year, so January 1 reads 0% and December 31 reads 100%. -function yearProgress(year, month, day) { - var total = daysInYear(year) - if (total <= 0) return 0 - return Math.max(0, Math.min(1, (dayOfYear(year, month, day) - 1) / total)) -} - -function yearProgressPercent(year, month, day) { - return Math.round(yearProgress(year, month, day) * 100) -} - // Memento mori. The default span is a round number rather than anything from // an actuarial table: the point of the bar is the reminder, not the // arithmetic, and whoever wants a different number can say so. @@ -217,80 +116,16 @@ function lifeProgressPercent(age, expectancy) { return Math.round(lifeProgress(age, expectancy) * 100) } -// Always six rows of seven days. A fixed grid keeps the popup exactly the -// same height in every month, so stepping through the year never makes the -// panel jump under the pointer. -function monthGrid(year, month, weekStart, todayKey) { - var start = normalizedWeekStart(weekStart, 1) - var leading = (new Date(year, month, 1).getDay() - start + 7) % 7 - var cursor = new Date(year, month, 1 - leading) - var today = String(todayKey || "") - var weeks = [] - - for (var w = 0; w < 6; w++) { - var days = [] - var thursday = null - for (var d = 0; d < 7; d++) { - var cellYear = cursor.getFullYear() - var cellMonth = cursor.getMonth() - var cellDay = cursor.getDate() - var weekday = cursor.getDay() - var key = dateKey(cellYear, cellMonth, cellDay) - if (weekday === 4) thursday = { year: cellYear, month: cellMonth, day: cellDay } - days.push({ - key: key, - year: cellYear, - month: cellMonth, - day: cellDay, - weekday: weekday, - inMonth: cellMonth === month && cellYear === year, - weekend: weekday === 0 || weekday === 6, - today: key === today - }) - cursor.setDate(cursor.getDate() + 1) - } - // Number every row by the ISO week owning its Thursday. That is the - // definition itself for Monday-start weeks, and the only answer that - // stays stable for the other starts, where a row straddles two ISO - // weeks but shares all of Monday through Thursday with one of them. - var anchor = thursday || days[0] - weeks.push({ - week: isoWeek(anchor.year, anchor.month, anchor.day), - days: days - }) - } - return weeks -} - -function stepMonth(year, month, delta) { - var target = new Date(year, Number(month) + Number(delta), 1) - return { year: target.getFullYear(), month: target.getMonth() } -} - if (typeof module !== "undefined") { module.exports = { - dateKey: dateKey, - keyForDate: keyForDate, - normalizedWeekStart: normalizedWeekStart, - weekStartSettingName: weekStartSettingName, - toggledWeekStart: toggledWeekStart, - weekdayOrder: weekdayOrder, - isoWeek: isoWeek, - dayOfYear: dayOfYear, - daysInYear: daysInYear, - yearProgress: yearProgress, - yearProgressPercent: yearProgressPercent, parseAge: parseAge, parseBirthYear: parseBirthYear, ageFromBirthYear: ageFromBirthYear, parseLifeExpectancy: parseLifeExpectancy, lifeProgress: lifeProgress, lifeProgressPercent: lifeProgressPercent, - monthGrid: monthGrid, - stepMonth: stepMonth, clockFormats: clockFormats, clockFormatRing: clockFormatRing, - nextClockFormat: nextClockFormat, - isoWeekLiteral: isoWeekLiteral + nextClockFormat: nextClockFormat } } diff --git a/shell/plugins/panels/clock/Panel.qml b/shell/plugins/panels/clock/Panel.qml index 26cc97d..86a6928 100644 --- a/shell/plugins/panels/clock/Panel.qml +++ b/shell/plugins/panels/clock/Panel.qml @@ -8,9 +8,9 @@ import "Model.js" as Model // sit beside the weather panel — same hero-over-detail composition, same // spacing scale, same small-caps labels. // -// The grid is a read-out rather than a picker: today is the only marked -// day, and the only thing that moves is which month is on screen — -// chevrons, the scroll wheel, and the arrow keys all step it. +// The grid itself is the shared CalendarView from qs.Ui: days can be +// selected with the pointer or the arrow keys, the month label opens a +// month picker, and today is filled with the accent color. // // BarWidget.qml owns the bar label and hands this panel the button to // anchor against. @@ -33,20 +33,11 @@ Panel { // ---- Today. SystemClock keeps this honest across midnight so the // highlight rolls over without the panel being reopened. property date today: new Date() - readonly property string todayKey: Model.keyForDate(today) - - // The month on screen. Stepping moves this and nothing else: the grid is - // a read-out, not a picker, so there is no per-day cursor to keep in sync. - property int viewYear: today.getFullYear() - property int viewMonth: today.getMonth() - - readonly property date viewDate: new Date(viewYear, viewMonth, 1) - readonly property bool viewingCurrentMonth: viewYear === today.getFullYear() && viewMonth === today.getMonth() // Pinned to today, not to the month being browsed — stepping through the // calendar does not change how much of the year is gone. - readonly property real yearDone: Model.yearProgress(today.getFullYear(), today.getMonth(), today.getDate()) - readonly property int yearDonePercent: Model.yearProgressPercent(today.getFullYear(), today.getMonth(), today.getDate()) + readonly property real yearDone: CalendarModel.yearProgress(today.getFullYear(), today.getMonth(), today.getDate()) + readonly property int yearDonePercent: CalendarModel.yearProgressPercent(today.getFullYear(), today.getMonth(), today.getDate()) // Memento mori, for anyone who goes looking: double-tapping the year bar // asks for a birth year and a life expectancy, and a second bar tracks one @@ -63,27 +54,18 @@ Panel { // starts out matching the rest of the desktop rather than a hardcoded // convention. Clicking the grid's "W" heading writes the choice back to // shell.json. - readonly property int weekStart: Model.normalizedWeekStart(setting("weekStartDay", null), Qt.locale().firstDayOfWeek) + readonly property int weekStart: CalendarModel.normalizedWeekStart(setting("weekStartDay", null), Qt.locale().firstDayOfWeek) // The interface is English throughout, so day names are not taken from the // system locale. Where the week starts still is: that is a regional // convention rather than a translation, and it stays overridable above. readonly property var labelLocale: Qt.locale("en_US") - readonly property string nextWeekStartLabel: labelLocale.dayName(Model.toggledWeekStart(weekStart), Locale.LongFormat) - readonly property var weekdays: Model.weekdayOrder(weekStart) - readonly property var weeks: Model.monthGrid(viewYear, viewMonth, weekStart, todayKey) - + readonly property string nextWeekStartLabel: labelLocale.dayName(CalendarModel.toggledWeekStart(weekStart), Locale.LongFormat) // Guarded so the widget renders before the bar is injected (the bar-widget // contract instantiates it bare). readonly property color contentForeground: bar ? bar.foreground : Color.foreground readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family - readonly property int cellWidth: Style.space(52) - readonly property int cellHeight: Style.space(34) - readonly property int cellSpacing: Style.space(2) - readonly property int weekColumnWidth: Style.space(32) - readonly property int gutterWidth: Style.space(14) - function open() { refresh() root.controller.show() @@ -127,22 +109,7 @@ Panel { function refresh() { root.today = new Date() - root.goToToday() - } - - function goToToday() { - root.viewYear = today.getFullYear() - root.viewMonth = today.getMonth() - } - - function moveMonth(delta) { - var next = Model.stepMonth(viewYear, viewMonth, delta) - root.viewYear = next.year - root.viewMonth = next.month - } - - function moveYear(delta) { - moveMonth(delta * 12) + calendar.goToToday() } // Applied locally first so the panel redraws on the click itself; the @@ -163,9 +130,9 @@ Panel { } function setWeekStart(day) { - var next = Model.normalizedWeekStart(day, root.weekStart) + var next = CalendarModel.normalizedWeekStart(day, root.weekStart) if (next === root.weekStart) return - persistSettings({ weekStartDay: Model.weekStartSettingName(next) }) + persistSettings({ weekStartDay: CalendarModel.weekStartSettingName(next) }) } function startEditingLife() { @@ -216,22 +183,21 @@ Panel { } function toggleWeekStart() { - setWeekStart(Model.toggledWeekStart(root.weekStart)) + setWeekStart(CalendarModel.toggledWeekStart(root.weekStart)) } - // English short day names, matching the rest of the interface. - function weekdayLabel(weekday) { - return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase() + function closeOrDismissPicker() { + if (!calendar.dismissPicker()) root.close() } SystemClock { id: clock precision: SystemClock.Minutes onDateChanged: { - if (Model.keyForDate(clock.date) === String(root.todayKey)) return - var followToday = root.viewingCurrentMonth + if (CalendarModel.keyForDate(clock.date) === CalendarModel.keyForDate(root.today)) return + var followToday = calendar.selectionIsToday root.today = clock.date - if (followToday) root.goToToday() + if (followToday) calendar.goToToday() } } @@ -250,19 +216,17 @@ Panel { id: keyCatcher anchors.fill: parent blocked: root.editingLife - onMoveRequested: function(dx, dy) { - if (dx !== 0) root.moveMonth(dx) - if (dy !== 0) root.moveYear(dy) - } - onActivateRequested: root.goToToday() - onCloseRequested: root.close() + onMoveRequested: function(dx, dy) { calendar.handleMove(dx, dy) } + onActivateRequested: calendar.activate() + onCloseRequested: root.closeOrDismissPicker() onTabRequested: function(direction) { root.switchPanel(direction) } onTextKey: function(t) { - if (t === "[") root.moveMonth(-1) - else if (t === "]") root.moveMonth(1) - else if (t === "{") root.moveYear(-1) - else if (t === "}") root.moveYear(1) - else if (t === "t" || t === "T") root.goToToday() + if (t === "[") calendar.moveMonth(-1) + else if (t === "]") calendar.moveMonth(1) + else if (t === "{") calendar.moveYear(-1) + else if (t === "}") calendar.moveYear(1) + else if (t === "t" || t === "T") calendar.goToToday() + else if (t === "m" || t === "M") calendar.togglePicker() else if (t === "w" || t === "W") root.toggleWeekStart() } @@ -280,7 +244,7 @@ Panel { // Never narrower than the grid. The popup width is capped to what // the screen allows, and a fixed seven-column grid would otherwise // lose its last days off the edge instead of scrolling. - width: Math.max(calendarScroll.width, gridColumn.width) + width: Math.max(calendarScroll.width, calendar.gridWidth) spacing: Style.space(8) // ---- Hero: today, centered. Once the view has stepped back @@ -331,10 +295,10 @@ Panel { y: heroRow.y width: heroRow.width height: heroRow.height - enabled: !root.viewingCurrentMonth + enabled: !calendar.selectionIsToday || calendar.pickingMonth hoverEnabled: enabled cursorShape: Qt.PointingHandCursor - onClicked: root.goToToday() + onClicked: calendar.goToToday() PanelToolTip { visible: heroMouse.containsMouse @@ -355,7 +319,7 @@ Panel { id: yearBlock y: Style.space(6) anchors.horizontalCenter: parent.horizontalCenter - width: gridColumn.width + width: calendar.gridWidth height: Math.max(yearLabel.implicitHeight, Style.space(10)) TapHandler { @@ -474,7 +438,7 @@ Panel { Item { id: lifeBlock anchors.horizontalCenter: parent.horizontalCenter - width: gridColumn.width + width: calendar.gridWidth height: Math.max(lifeLabel.implicitHeight, Style.space(10)) Text { @@ -538,222 +502,23 @@ Panel { } } - // ---- Month grid: week numbers down a gutter on the left, then - // the seven day columns. Always six rows, so the popup is - // exactly as tall in February as it is in August. - Item { + CalendarView { + id: calendar width: parent.width - height: gridColumn.y + gridColumn.height - - WheelHandler { - onWheel: function(event) { - // Horizontal wheels and touchpad side-scrolls report y === 0; - // without this they would every one read as "next month". - if (event.angleDelta.y === 0) return - root.moveMonth(event.angleDelta.y > 0 ? -1 : 1) - } - } - - Column { - id: gridColumn - // The meter above is a solid rule; the grid needs room to - // read as its own block rather than hanging off it. - y: Style.space(18) - anchors.horizontalCenter: parent.horizontalCenter - spacing: Style.space(3) - - Row { - id: headerRow - spacing: root.cellSpacing - - // The week-number heading doubles as the week-start toggle. - // It is the one control in the panel whose meaning is not - // self-evident, so it carries a tooltip naming the day the - // click will switch to. - Rectangle { - width: root.weekColumnWidth - height: Style.space(16) - radius: Style.cornerRadius - color: weekStartMouse.containsMouse - ? Style.hoverFillFor(root.contentForeground, Color.accent) - : "transparent" - - Text { - anchors.centerIn: parent - text: "W" - color: weekStartMouse.containsMouse - ? Style.hoverStateColor(root.contentForeground, Color.accent) - : Qt.darker(root.contentForeground, 1.9) - font.family: root.contentFontFamily - font.pixelSize: Style.font.caption - font.letterSpacing: 1 - font.bold: true - } - - MouseArea { - id: weekStartMouse - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: root.toggleWeekStart() - } - - PanelToolTip { - visible: weekStartMouse.containsMouse - text: "Start weeks on " + root.nextWeekStartLabel - fontFamily: root.contentFontFamily - } - } - - Item { - width: root.gutterWidth - height: Style.space(16) - } - - Repeater { - model: root.weekdays - - Text { - textFormat: Text.PlainText - required property var modelData - width: root.cellWidth - height: Style.space(16) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: root.weekdayLabel(modelData) - color: Qt.darker(root.contentForeground, 1.5) - font.family: root.contentFontFamily - font.pixelSize: Style.font.caption - font.letterSpacing: 1 - font.bold: true - } - } - } - - Repeater { - model: root.weeks - - Row { - required property var modelData - spacing: root.cellSpacing - - Text { - textFormat: Text.PlainText - width: root.weekColumnWidth - height: root.cellHeight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - text: modelData.week - color: Qt.darker(root.contentForeground, 1.9) - font.family: root.contentFontFamily - font.pixelSize: Style.font.caption - } - - Item { - width: root.gutterWidth - height: root.cellHeight - } - - Repeater { - model: modelData.days - - Rectangle { - required property var modelData - - width: root.cellWidth - height: root.cellHeight - radius: Style.cornerRadius - // Today is outlined, not filled: a lit-up block shouts - // over a grid this quiet. - color: "transparent" - border.width: modelData.today ? Style.spacing.hairline : 0 - border.color: Style.normalBorderFor(root.contentForeground, Color.accent) - - Text { - textFormat: Text.PlainText - anchors.centerIn: parent - text: modelData.day - color: modelData.inMonth - ? (modelData.weekend ? Qt.darker(root.contentForeground, 1.45) : root.contentForeground) - : Qt.darker(root.contentForeground, 2.2) - font.family: root.contentFontFamily - font.pixelSize: Style.font.body - font.bold: modelData.today - } - } - } - } - } - } - - // Hairline down the week-number gutter, drawn only beside the - // day rows so it does not cut through the header band. - Rectangle { - x: gridColumn.x + root.weekColumnWidth + root.cellSpacing + Math.round((root.gutterWidth - width) / 2) - y: gridColumn.y + headerRow.height + gridColumn.spacing - width: Style.spacing.hairline - height: gridColumn.height - headerRow.height - gridColumn.spacing - color: root.contentForeground - opacity: 0.1 - } - } - - // ---- Month stepping, spanning the grid it drives. The chevrons - // sit on the grid's outer bounds, the same edges the year - // rail above uses, so the row reads as the panel's other - // full-width rail instead of a cluster floating in space. - // The label is centered and fixed-width, so it holds still - // from "MAY" to "SEPTEMBER". - Item { - width: parent.width - height: monthNav.height - - Item { - id: monthNav - anchors.horizontalCenter: parent.horizontalCenter - width: gridColumn.width - height: monthLabel.implicitHeight + Style.space(10) - - Text { - id: monthLabel - textFormat: Text.PlainText - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - // Fixed width so the chevrons hold still between a - // "MAY 2026" and a "SEPTEMBER 2026". - width: Style.space(130) - horizontalAlignment: Text.AlignHCenter - text: Qt.formatDate(root.viewDate, "MMMM yyyy").toUpperCase() - color: Qt.darker(root.contentForeground, 1.4) - font.family: root.contentFontFamily - font.pixelSize: Style.font.body - font.letterSpacing: 1 - } - - PanelActionButton { - // Pulled out by the button's own padding so the glyph, not - // its hit box, lines up with the "2026" on the year rail. - anchors.left: parent.left - anchors.leftMargin: -Style.space(8) - anchors.verticalCenter: parent.verticalCenter - iconText: "󰅁" - tooltipText: "Previous month" - foreground: root.contentForeground - fontFamily: root.contentFontFamily - onClicked: root.moveMonth(-1) - } - - PanelActionButton { - anchors.right: parent.right - anchors.rightMargin: -Style.space(8) - anchors.verticalCenter: parent.verticalCenter - iconText: "󰅂" - tooltipText: "Next month" - foreground: root.contentForeground - fontFamily: root.contentFontFamily - onClicked: root.moveMonth(1) - } - } + topPadding: Style.space(10) + today: root.today + weekStart: root.weekStart + foreground: root.contentForeground + fontFamily: root.contentFontFamily + showWeekNumbers: true + navigationAtBottom: true + weekHeaderTooltip: "Start weeks on " + root.nextWeekStartLabel + cellWidth: Style.space(52) + cellHeight: Style.space(34) + cellSpacing: Style.space(2) + weekColumnWidth: Style.space(32) + gutterWidth: Style.space(14) + onWeekHeaderClicked: root.toggleWeekStart() } } } diff --git a/shell/plugins/panels/quick-settings/Calendar.qml b/shell/plugins/panels/quick-settings/Calendar.qml index acafb85..dd078ac 100644 --- a/shell/plugins/panels/quick-settings/Calendar.qml +++ b/shell/plugins/panels/quick-settings/Calendar.qml @@ -1,115 +1,42 @@ import QtQuick -import Quickshell.Io import qs.Commons import qs.Ui Item { id: root - property int monthOffset: 0 - property string monthName: "" - property string monthGrid: "" property bool polling: false - implicitHeight: layout.implicitHeight + implicitHeight: calendar.implicitHeight - function loadMonth() { - if (monthProbe.running) return - monthProbe.running = true + function refreshToday() { + var now = new Date() + if (CalendarModel.keyForDate(now) === CalendarModel.keyForDate(calendar.today)) return + var followToday = calendar.selectionIsToday + calendar.today = now + if (followToday) calendar.goToToday() } - function shiftMonth(delta) { - root.monthOffset += delta - root.loadMonth() - } - - function resetMonth() { - root.monthOffset = 0 - root.loadMonth() - } - - Process { - id: monthProbe - command: ["bash", "-c", - 'first="$(date +%Y-%m-01) ' + root.monthOffset + ' months"; ' + - 'date -d "$first" "+%B %Y"; cal $(date -d "$first" "+%m %Y") | sed "1d"'] - stdout: StdioCollector { - onStreamFinished: { - var lines = String(text).replace(/\s+$/, "").split("\n") - root.monthName = (lines[0] || "").trim() - root.monthGrid = lines.slice(1).join("\n") - } - } + onPollingChanged: { + if (!polling) return + calendar.today = new Date() + calendar.goToToday() } Timer { - interval: 3600000 + interval: 60000 running: root.polling repeat: true - onTriggered: if (root.monthOffset === 0) root.loadMonth() + onTriggered: root.refreshToday() } - Component.onCompleted: root.loadMonth() - - WheelHandler { - onWheel: function(event) { - root.shiftMonth(event.angleDelta.y > 0 ? -1 : 1) - } - } - - Column { - id: layout + CalendarView { + id: calendar width: parent.width - spacing: Style.spaceReal(6) - - Item { - width: parent.width - implicitHeight: Math.max(monthLabel.implicitHeight, navigation.implicitHeight) - - Text { - id: monthLabel - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - text: root.monthName - textFormat: Text.PlainText - color: Color.foreground - font.family: Style.font.family - font.pixelSize: Style.font.body - font.bold: true - } - - Row { - id: navigation - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - spacing: Style.spaceReal(4) - - CardIconButton { - icon: "<" - onActivated: root.shiftMonth(-1) - } - - CardIconButton { - icon: "Today" - visible: root.monthOffset !== 0 - implicitWidth: Style.spaceReal(44) - onActivated: root.resetMonth() - } - - CardIconButton { - icon: ">" - onActivated: root.shiftMonth(1) - } - } - } - - Text { - anchors.horizontalCenter: parent.horizontalCenter - text: root.monthGrid - textFormat: Text.PlainText - color: Util.alpha(Color.foreground, 0.85) - font.family: Style.font.family - font.pixelSize: Style.font.bodySmall - } + foreground: Color.foreground + fontFamily: Style.font.family + cellSpacing: Style.space(2) + cellWidth: Math.floor((width - cellSpacing * 6) / 7) + cellHeight: Style.space(24) } }