The calendar grid's header row and the week-start toggle label were the only text in the shell that followed the system locale, so a German desktop drew MO DI MI over an interface that is English everywhere else. Nothing chose that; they were the only two places reading day names off Qt.locale(). Take them from en_US instead. Where the week starts still follows the locale: that is a regional convention rather than a translation, and it stays overridable through weekStartDay. Dropping the trailing-period strip with it, since that existed only for the locales this no longer renders. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
753 lines
29 KiB
QML
753 lines
29 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "Model.js" as Model
|
|
|
|
// The clock's calendar popup: a month grid with ISO week numbers, built to
|
|
// 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.
|
|
//
|
|
// BarWidget.qml owns the bar label and hands this panel the button to
|
|
// anchor against.
|
|
Panel {
|
|
id: root
|
|
moduleName: "omarchy.clock"
|
|
ipcTarget: "omarchy.clock"
|
|
manageIpc: false
|
|
|
|
property var anchorItem: null
|
|
|
|
// The bar tracks the widget mounted in its slot — BarWidget.qml — not this
|
|
// nested panel. Everything the bar identifies a panel by has to be that
|
|
// widget: the popout coordinator (and with it the open-panel dot under the
|
|
// pill) compares against `slot.activeItem`, and switchPanelFrom looks the
|
|
// slot up the same way.
|
|
property var hostWidget: null
|
|
readonly property var barIdentity: hostWidget || root
|
|
|
|
// ---- 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())
|
|
|
|
// 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
|
|
// against the other. A birth year rather than an age, so it keeps counting
|
|
// on its own. Without one the bar stays hidden.
|
|
readonly property int birthYear: Model.parseBirthYear(setting("birthYear", 0), today.getFullYear())
|
|
readonly property int age: Model.ageFromBirthYear(birthYear, today.getFullYear())
|
|
readonly property int lifeExpectancy: Model.parseLifeExpectancy(setting("lifeExpectancy", 0))
|
|
readonly property real lifeDone: Model.lifeProgress(age, lifeExpectancy)
|
|
readonly property int lifeDonePercent: Model.lifeProgressPercent(age, lifeExpectancy)
|
|
property bool editingLife: false
|
|
|
|
// Unset falls through to the locale's own first day, so a fresh install
|
|
// 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)
|
|
// 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)
|
|
|
|
|
|
// 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()
|
|
// Set after showing, not before: showing hands the popout coordinator
|
|
// over, which closes whichever panel was open, and that close clears the
|
|
// shared flag. Deferring means the panel taking over always wins, while
|
|
// a handoff to a panel that does not manage the flag still leaves it
|
|
// cleared rather than stuck on.
|
|
Qt.callLater(function() {
|
|
if (root.opened) setCenterHoverRevealSuppressed(true)
|
|
})
|
|
}
|
|
|
|
function close() {
|
|
setCenterHoverRevealSuppressed(false)
|
|
// Dismissing the panel mid-edit would otherwise leave the inputs up,
|
|
// waiting behind a closed popup for the next time it opens.
|
|
if (root.editingLife) root.cancelEditingLife()
|
|
root.controller.hide()
|
|
}
|
|
|
|
function toggle() {
|
|
if (root.opened) root.close()
|
|
else root.open()
|
|
}
|
|
|
|
function switchPanel(direction) {
|
|
if (root.bar && typeof root.bar.switchPanelFrom === "function")
|
|
return root.bar.switchPanelFrom(root.barIdentity, direction)
|
|
return false
|
|
}
|
|
|
|
// Summoning by hotkey moves no pointer, so a hover the bar was still
|
|
// holding must not keep the center indicators revealed behind the panel.
|
|
function setCenterHoverRevealSuppressed(value) {
|
|
if (root.bar && "centerHoverRevealSuppressed" in root.bar)
|
|
root.bar.centerHoverRevealSuppressed = value
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Applied locally first so the panel redraws on the click itself; the
|
|
// shell.json write comes back through the bar as the same value. With no
|
|
// writable entry (the widget is not in the layout) it stays a session-only
|
|
// preference rather than doing nothing. The host widget builds its own
|
|
// entry when the label format is cycled, so it has to be kept in step or
|
|
// it would write this key straight back out from a stale copy.
|
|
function persistSettings(values) {
|
|
var entry = { id: root.moduleName }
|
|
for (var existing in root.settings) if (existing !== "id") entry[existing] = root.settings[existing]
|
|
for (var key in values) entry[key] = values[key]
|
|
|
|
root.settings = entry
|
|
if (root.hostWidget && "settings" in root.hostWidget) root.hostWidget.settings = entry
|
|
if (root.bar && root.bar.shell && typeof root.bar.shell.updateEntryInline === "function")
|
|
root.bar.shell.updateEntryInline(root.moduleName, entry)
|
|
}
|
|
|
|
function setWeekStart(day) {
|
|
var next = Model.normalizedWeekStart(day, root.weekStart)
|
|
if (next === root.weekStart) return
|
|
persistSettings({ weekStartDay: Model.weekStartSettingName(next) })
|
|
}
|
|
|
|
function startEditingLife() {
|
|
root.editingLife = true
|
|
Qt.callLater(function() {
|
|
bornField.text = root.birthYear > 0 ? String(root.birthYear) : ""
|
|
expectancyField.text = String(root.lifeExpectancy)
|
|
bornField.selectAll()
|
|
bornField.forceActiveFocus()
|
|
})
|
|
}
|
|
|
|
function cancelEditingLife() {
|
|
root.editingLife = false
|
|
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
|
}
|
|
|
|
// Shared by both fields: Tab hops to the other one, Enter commits the pair,
|
|
// Escape drops the lot.
|
|
function handleLifeKey(event, other) {
|
|
if (event.key === Qt.Key_Escape) {
|
|
root.cancelEditingLife()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
|
root.commitLife()
|
|
event.accepted = true
|
|
} else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab) {
|
|
other.selectAll()
|
|
other.forceActiveFocus()
|
|
event.accepted = true
|
|
}
|
|
}
|
|
|
|
// Double-tapping the life bar puts it away again. The expectancy stays in
|
|
// the config so setting a birth year again brings your own number back
|
|
// rather than the default.
|
|
function clearLife() {
|
|
if (root.birthYear <= 0) return
|
|
persistSettings({ birthYear: 0 })
|
|
}
|
|
|
|
function commitLife() {
|
|
var born = Model.parseBirthYear(bornField.text, today.getFullYear())
|
|
var span = Model.parseLifeExpectancy(expectancyField.text)
|
|
if (born !== root.birthYear || span !== root.lifeExpectancy)
|
|
persistSettings({ birthYear: born, lifeExpectancy: span })
|
|
cancelEditingLife()
|
|
}
|
|
|
|
function toggleWeekStart() {
|
|
setWeekStart(Model.toggledWeekStart(root.weekStart))
|
|
}
|
|
|
|
// English short day names, matching the rest of the interface.
|
|
function weekdayLabel(weekday) {
|
|
return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase()
|
|
}
|
|
|
|
SystemClock {
|
|
id: clock
|
|
precision: SystemClock.Minutes
|
|
onDateChanged: {
|
|
if (Model.keyForDate(clock.date) === String(root.todayKey)) return
|
|
var followToday = root.viewingCurrentMonth
|
|
root.today = clock.date
|
|
if (followToday) root.goToToday()
|
|
}
|
|
}
|
|
|
|
KeyboardPanel {
|
|
id: panel
|
|
anchorItem: root.anchorItem
|
|
owner: root.barIdentity
|
|
bar: root.bar
|
|
open: root.opened
|
|
centerOnBar: true
|
|
focusTarget: keyCatcher
|
|
contentWidth: panel.fittedContentWidth(Style.space(560))
|
|
contentHeight: panel.fittedContentHeight(calendarColumn.implicitHeight)
|
|
|
|
PanelKeyCatcher {
|
|
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()
|
|
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()
|
|
else if (t === "w" || t === "W") root.toggleWeekStart()
|
|
}
|
|
|
|
Flickable {
|
|
id: calendarScroll
|
|
anchors.fill: parent
|
|
contentWidth: calendarColumn.width
|
|
contentHeight: calendarColumn.implicitHeight
|
|
clip: true
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
interactive: contentHeight > height || contentWidth > width
|
|
|
|
Column {
|
|
id: calendarColumn
|
|
// 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)
|
|
spacing: Style.space(8)
|
|
|
|
// ---- Hero: today, centered. Once the view has stepped back
|
|
// it is also the way home — clicking the date you are
|
|
// looking for beats hunting for a reset button.
|
|
Item {
|
|
width: parent.width
|
|
height: heroRow.height
|
|
|
|
Row {
|
|
id: heroRow
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Style.space(22)
|
|
|
|
Text {
|
|
// Baseline-aligned, not center-aligned: "July 26" carries a
|
|
// descender, so centering the two boxes leaves the icon
|
|
// sitting visibly low against the digits.
|
|
anchors.baseline: heroDate.baseline
|
|
text: ""
|
|
color: heroMouse.containsMouse
|
|
? Style.hoverStateColor(root.contentForeground, Color.accent)
|
|
: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
// Decorative, and deliberately outside the Style.font.*
|
|
// scale. Sized so the glyph reads at the cap height of the
|
|
// date beside it rather than towering over it.
|
|
font.pixelSize: 48
|
|
}
|
|
|
|
Text {
|
|
id: heroDate
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: Qt.formatDate(root.today, "MMMM d")
|
|
color: heroMouse.containsMouse
|
|
? Style.hoverStateColor(root.contentForeground, Color.accent)
|
|
: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: 52
|
|
font.bold: true
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: heroMouse
|
|
x: heroRow.x
|
|
y: heroRow.y
|
|
width: heroRow.width
|
|
height: heroRow.height
|
|
enabled: !root.viewingCurrentMonth
|
|
hoverEnabled: enabled
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: root.goToToday()
|
|
|
|
PanelToolTip {
|
|
visible: heroMouse.containsMouse
|
|
text: "Back to today"
|
|
fontFamily: root.contentFontFamily
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Year progress, doubling as the rule under the hero:
|
|
// a plain hairline said nothing, and whole days done
|
|
// over days in the year says the same thing louder.
|
|
Item {
|
|
width: parent.width
|
|
height: yearBlock.y + yearBlock.height
|
|
|
|
Item {
|
|
id: yearBlock
|
|
y: Style.space(6)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: gridColumn.width
|
|
height: Math.max(yearLabel.implicitHeight, Style.space(10))
|
|
|
|
TapHandler {
|
|
enabled: !root.editingLife
|
|
onDoubleTapped: root.startEditingLife()
|
|
}
|
|
|
|
Row {
|
|
visible: root.editingLife
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: Style.space(10)
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "BORN"
|
|
color: Qt.darker(root.contentForeground, 1.5)
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
TextField {
|
|
id: bornField
|
|
width: Style.space(70)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
placeholderText: "year"
|
|
foreground: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
inputMethodHints: Qt.ImhDigitsOnly
|
|
|
|
Keys.onPressed: function(event) { root.handleLifeKey(event, expectancyField) }
|
|
}
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.verticalCenterOffset: 0
|
|
leftPadding: Style.space(6)
|
|
text: "LIVE TO"
|
|
color: Qt.darker(root.contentForeground, 1.5)
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
TextField {
|
|
id: expectancyField
|
|
width: Style.space(60)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
placeholderText: "90"
|
|
foreground: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
inputMethodHints: Qt.ImhDigitsOnly
|
|
|
|
Keys.onPressed: function(event) { root.handleLifeKey(event, bornField) }
|
|
}
|
|
}
|
|
|
|
Text {
|
|
id: yearLabel
|
|
visible: !root.editingLife
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.today.getFullYear()
|
|
color: Qt.darker(root.contentForeground, 1.5)
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
Text {
|
|
id: yearPercent
|
|
visible: !root.editingLife
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.yearDonePercent + "%"
|
|
color: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
Rectangle {
|
|
id: yearTrack
|
|
visible: !root.editingLife
|
|
anchors.left: yearLabel.right
|
|
anchors.right: yearPercent.left
|
|
anchors.leftMargin: Style.space(12)
|
|
anchors.rightMargin: Style.space(12)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: Style.space(6)
|
|
radius: Style.cornerRadius > 0 ? height / 2 : 0
|
|
color: Qt.rgba(root.contentForeground.r, root.contentForeground.g, root.contentForeground.b, 0.12)
|
|
|
|
Rectangle {
|
|
width: Math.round(parent.width * root.yearDone)
|
|
height: parent.height
|
|
radius: parent.radius
|
|
color: Style.selectedStateColor(root.contentForeground, Color.accent)
|
|
|
|
Behavior on width { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Memento mori. Only here once someone has gone looking and
|
|
// given an age; the same rail as the year above it, measured
|
|
// against a nominal lifetime.
|
|
Item {
|
|
visible: root.birthYear > 0
|
|
width: parent.width
|
|
height: visible ? lifeBlock.height : 0
|
|
|
|
Item {
|
|
id: lifeBlock
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
width: gridColumn.width
|
|
height: Math.max(lifeLabel.implicitHeight, Style.space(10))
|
|
|
|
Text {
|
|
id: lifeLabel
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "LIFE"
|
|
color: Qt.darker(root.contentForeground, 1.5)
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
Text {
|
|
id: lifePercent
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.lifeDonePercent + "%"
|
|
color: root.contentForeground
|
|
font.family: root.contentFontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.left: lifeLabel.right
|
|
anchors.right: lifePercent.left
|
|
anchors.leftMargin: Style.space(12)
|
|
anchors.rightMargin: Style.space(12)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
height: Style.space(6)
|
|
radius: Style.cornerRadius > 0 ? height / 2 : 0
|
|
color: Qt.rgba(root.contentForeground.r, root.contentForeground.g, root.contentForeground.b, 0.12)
|
|
|
|
Rectangle {
|
|
width: Math.round(parent.width * root.lifeDone)
|
|
height: parent.height
|
|
radius: parent.radius
|
|
color: Style.selectedStateColor(root.contentForeground, Color.accent)
|
|
|
|
Behavior on width { NumberAnimation { duration: 160; easing.type: Easing.OutCubic } }
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
onDoubleTapped: root.clearLife()
|
|
}
|
|
|
|
MouseArea {
|
|
id: lifeMouse
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.NoButton
|
|
|
|
PanelToolTip {
|
|
visible: lifeMouse.containsMouse
|
|
text: "Memento Mori"
|
|
fontFamily: root.contentFontFamily
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- 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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|