Share one calendar with day selection and a month picker
This commit is contained in:
1 parent
d59725f193
commit
3164e50b36
10 files changed
+773
-543
No files matched your search
@@ -61,6 +61,33 @@ notifications are silenced.
|
|||||||
The centre module named by `centerAnchor` cannot be dragged out of the centre.
|
The centre module named by `centerAnchor` cannot be dragged out of the centre.
|
||||||
Every other widget still reorders.
|
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
|
## Differences from upstream
|
||||||
|
|
||||||
This shell was forked from Omarchy 4.0.4 and is not tracked against it. The
|
This shell was forked from Omarchy 4.0.4 and is not tracked against it. The
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,3 +38,7 @@ CardHeader 1.0 CardHeader.qml
|
|||||||
StatRow 1.0 StatRow.qml
|
StatRow 1.0 StatRow.qml
|
||||||
CardIconButton 1.0 CardIconButton.qml
|
CardIconButton 1.0 CardIconButton.qml
|
||||||
Chip 1.0 Chip.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
|
||||||
@@ -53,7 +53,7 @@ BarWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatted(date) {
|
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
|
// ---- Calendar popup. Shape contract for shell.summon/hide/toggle
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
// Pure date and format math for the clock widget and its calendar panel.
|
// Clock label formats and the memento mori bar. Calendar date math lives in
|
||||||
// Everything here is locale- and Qt-free so it can be unit tested under node
|
// qs.Ui's CalendarModel.js, shared with the Quick Settings calendar.
|
||||||
// (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"]
|
|
||||||
|
|
||||||
// ---- Bar label formats. Right-clicking the clock walks these in order and
|
// ---- 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
|
// 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]
|
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
|
// 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
|
// an actuarial table: the point of the bar is the reminder, not the
|
||||||
// arithmetic, and whoever wants a different number can say so.
|
// 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)
|
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") {
|
if (typeof module !== "undefined") {
|
||||||
module.exports = {
|
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,
|
parseAge: parseAge,
|
||||||
parseBirthYear: parseBirthYear,
|
parseBirthYear: parseBirthYear,
|
||||||
ageFromBirthYear: ageFromBirthYear,
|
ageFromBirthYear: ageFromBirthYear,
|
||||||
parseLifeExpectancy: parseLifeExpectancy,
|
parseLifeExpectancy: parseLifeExpectancy,
|
||||||
lifeProgress: lifeProgress,
|
lifeProgress: lifeProgress,
|
||||||
lifeProgressPercent: lifeProgressPercent,
|
lifeProgressPercent: lifeProgressPercent,
|
||||||
monthGrid: monthGrid,
|
|
||||||
stepMonth: stepMonth,
|
|
||||||
clockFormats: clockFormats,
|
clockFormats: clockFormats,
|
||||||
clockFormatRing: clockFormatRing,
|
clockFormatRing: clockFormatRing,
|
||||||
nextClockFormat: nextClockFormat,
|
nextClockFormat: nextClockFormat
|
||||||
isoWeekLiteral: isoWeekLiteral
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,9 +8,9 @@ import "Model.js" as Model
|
|||||||
// sit beside the weather panel — same hero-over-detail composition, same
|
// sit beside the weather panel — same hero-over-detail composition, same
|
||||||
// spacing scale, same small-caps labels.
|
// spacing scale, same small-caps labels.
|
||||||
//
|
//
|
||||||
// The grid is a read-out rather than a picker: today is the only marked
|
// The grid itself is the shared CalendarView from qs.Ui: days can be
|
||||||
// day, and the only thing that moves is which month is on screen —
|
// selected with the pointer or the arrow keys, the month label opens a
|
||||||
// chevrons, the scroll wheel, and the arrow keys all step it.
|
// month picker, and today is filled with the accent color.
|
||||||
//
|
//
|
||||||
// BarWidget.qml owns the bar label and hands this panel the button to
|
// BarWidget.qml owns the bar label and hands this panel the button to
|
||||||
// anchor against.
|
// anchor against.
|
||||||
@@ -33,20 +33,11 @@ Panel {
|
|||||||
// ---- Today. SystemClock keeps this honest across midnight so the
|
// ---- Today. SystemClock keeps this honest across midnight so the
|
||||||
// highlight rolls over without the panel being reopened.
|
// highlight rolls over without the panel being reopened.
|
||||||
property date today: new Date()
|
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
|
// Pinned to today, not to the month being browsed — stepping through the
|
||||||
// calendar does not change how much of the year is gone.
|
// 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 real yearDone: CalendarModel.yearProgress(today.getFullYear(), today.getMonth(), today.getDate())
|
||||||
readonly property int yearDonePercent: Model.yearProgressPercent(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
|
// 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
|
// 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
|
// starts out matching the rest of the desktop rather than a hardcoded
|
||||||
// convention. Clicking the grid's "W" heading writes the choice back to
|
// convention. Clicking the grid's "W" heading writes the choice back to
|
||||||
// shell.json.
|
// 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
|
// 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
|
// system locale. Where the week starts still is: that is a regional
|
||||||
// convention rather than a translation, and it stays overridable above.
|
// convention rather than a translation, and it stays overridable above.
|
||||||
readonly property var labelLocale: Qt.locale("en_US")
|
readonly property var labelLocale: Qt.locale("en_US")
|
||||||
readonly property string nextWeekStartLabel: labelLocale.dayName(Model.toggledWeekStart(weekStart), Locale.LongFormat)
|
readonly property string nextWeekStartLabel: labelLocale.dayName(CalendarModel.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
|
// Guarded so the widget renders before the bar is injected (the bar-widget
|
||||||
// contract instantiates it bare).
|
// contract instantiates it bare).
|
||||||
readonly property color contentForeground: bar ? bar.foreground : Color.foreground
|
readonly property color contentForeground: bar ? bar.foreground : Color.foreground
|
||||||
readonly property string contentFontFamily: bar ? bar.fontFamily : Style.font.family
|
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() {
|
function open() {
|
||||||
refresh()
|
refresh()
|
||||||
root.controller.show()
|
root.controller.show()
|
||||||
@@ -127,22 +109,7 @@ Panel {
|
|||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
root.today = new Date()
|
root.today = new Date()
|
||||||
root.goToToday()
|
calendar.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
|
// Applied locally first so the panel redraws on the click itself; the
|
||||||
@@ -163,9 +130,9 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setWeekStart(day) {
|
function setWeekStart(day) {
|
||||||
var next = Model.normalizedWeekStart(day, root.weekStart)
|
var next = CalendarModel.normalizedWeekStart(day, root.weekStart)
|
||||||
if (next === root.weekStart) return
|
if (next === root.weekStart) return
|
||||||
persistSettings({ weekStartDay: Model.weekStartSettingName(next) })
|
persistSettings({ weekStartDay: CalendarModel.weekStartSettingName(next) })
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEditingLife() {
|
function startEditingLife() {
|
||||||
@@ -216,22 +183,21 @@ Panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleWeekStart() {
|
function toggleWeekStart() {
|
||||||
setWeekStart(Model.toggledWeekStart(root.weekStart))
|
setWeekStart(CalendarModel.toggledWeekStart(root.weekStart))
|
||||||
}
|
}
|
||||||
|
|
||||||
// English short day names, matching the rest of the interface.
|
function closeOrDismissPicker() {
|
||||||
function weekdayLabel(weekday) {
|
if (!calendar.dismissPicker()) root.close()
|
||||||
return String(labelLocale.dayName(weekday, Locale.ShortFormat)).toUpperCase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemClock {
|
SystemClock {
|
||||||
id: clock
|
id: clock
|
||||||
precision: SystemClock.Minutes
|
precision: SystemClock.Minutes
|
||||||
onDateChanged: {
|
onDateChanged: {
|
||||||
if (Model.keyForDate(clock.date) === String(root.todayKey)) return
|
if (CalendarModel.keyForDate(clock.date) === CalendarModel.keyForDate(root.today)) return
|
||||||
var followToday = root.viewingCurrentMonth
|
var followToday = calendar.selectionIsToday
|
||||||
root.today = clock.date
|
root.today = clock.date
|
||||||
if (followToday) root.goToToday()
|
if (followToday) calendar.goToToday()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,19 +216,17 @@ Panel {
|
|||||||
id: keyCatcher
|
id: keyCatcher
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
blocked: root.editingLife
|
blocked: root.editingLife
|
||||||
onMoveRequested: function(dx, dy) {
|
onMoveRequested: function(dx, dy) { calendar.handleMove(dx, dy) }
|
||||||
if (dx !== 0) root.moveMonth(dx)
|
onActivateRequested: calendar.activate()
|
||||||
if (dy !== 0) root.moveYear(dy)
|
onCloseRequested: root.closeOrDismissPicker()
|
||||||
}
|
|
||||||
onActivateRequested: root.goToToday()
|
|
||||||
onCloseRequested: root.close()
|
|
||||||
onTabRequested: function(direction) { root.switchPanel(direction) }
|
onTabRequested: function(direction) { root.switchPanel(direction) }
|
||||||
onTextKey: function(t) {
|
onTextKey: function(t) {
|
||||||
if (t === "[") root.moveMonth(-1)
|
if (t === "[") calendar.moveMonth(-1)
|
||||||
else if (t === "]") root.moveMonth(1)
|
else if (t === "]") calendar.moveMonth(1)
|
||||||
else if (t === "{") root.moveYear(-1)
|
else if (t === "{") calendar.moveYear(-1)
|
||||||
else if (t === "}") root.moveYear(1)
|
else if (t === "}") calendar.moveYear(1)
|
||||||
else if (t === "t" || t === "T") root.goToToday()
|
else if (t === "t" || t === "T") calendar.goToToday()
|
||||||
|
else if (t === "m" || t === "M") calendar.togglePicker()
|
||||||
else if (t === "w" || t === "W") root.toggleWeekStart()
|
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
|
// Never narrower than the grid. The popup width is capped to what
|
||||||
// the screen allows, and a fixed seven-column grid would otherwise
|
// the screen allows, and a fixed seven-column grid would otherwise
|
||||||
// lose its last days off the edge instead of scrolling.
|
// 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)
|
spacing: Style.space(8)
|
||||||
|
|
||||||
// ---- Hero: today, centered. Once the view has stepped back
|
// ---- Hero: today, centered. Once the view has stepped back
|
||||||
@@ -331,10 +295,10 @@ Panel {
|
|||||||
y: heroRow.y
|
y: heroRow.y
|
||||||
width: heroRow.width
|
width: heroRow.width
|
||||||
height: heroRow.height
|
height: heroRow.height
|
||||||
enabled: !root.viewingCurrentMonth
|
enabled: !calendar.selectionIsToday || calendar.pickingMonth
|
||||||
hoverEnabled: enabled
|
hoverEnabled: enabled
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: root.goToToday()
|
onClicked: calendar.goToToday()
|
||||||
|
|
||||||
PanelToolTip {
|
PanelToolTip {
|
||||||
visible: heroMouse.containsMouse
|
visible: heroMouse.containsMouse
|
||||||
@@ -355,7 +319,7 @@ Panel {
|
|||||||
id: yearBlock
|
id: yearBlock
|
||||||
y: Style.space(6)
|
y: Style.space(6)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: gridColumn.width
|
width: calendar.gridWidth
|
||||||
height: Math.max(yearLabel.implicitHeight, Style.space(10))
|
height: Math.max(yearLabel.implicitHeight, Style.space(10))
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
@@ -474,7 +438,7 @@ Panel {
|
|||||||
Item {
|
Item {
|
||||||
id: lifeBlock
|
id: lifeBlock
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: gridColumn.width
|
width: calendar.gridWidth
|
||||||
height: Math.max(lifeLabel.implicitHeight, Style.space(10))
|
height: Math.max(lifeLabel.implicitHeight, Style.space(10))
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -538,222 +502,23 @@ Panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Month grid: week numbers down a gutter on the left, then
|
CalendarView {
|
||||||
// the seven day columns. Always six rows, so the popup is
|
id: calendar
|
||||||
// exactly as tall in February as it is in August.
|
|
||||||
Item {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: gridColumn.y + gridColumn.height
|
topPadding: Style.space(10)
|
||||||
|
today: root.today
|
||||||
WheelHandler {
|
weekStart: root.weekStart
|
||||||
onWheel: function(event) {
|
foreground: root.contentForeground
|
||||||
// Horizontal wheels and touchpad side-scrolls report y === 0;
|
fontFamily: root.contentFontFamily
|
||||||
// without this they would every one read as "next month".
|
showWeekNumbers: true
|
||||||
if (event.angleDelta.y === 0) return
|
navigationAtBottom: true
|
||||||
root.moveMonth(event.angleDelta.y > 0 ? -1 : 1)
|
weekHeaderTooltip: "Start weeks on " + root.nextWeekStartLabel
|
||||||
}
|
cellWidth: Style.space(52)
|
||||||
}
|
cellHeight: Style.space(34)
|
||||||
|
cellSpacing: Style.space(2)
|
||||||
Column {
|
weekColumnWidth: Style.space(32)
|
||||||
id: gridColumn
|
gutterWidth: Style.space(14)
|
||||||
// The meter above is a solid rule; the grid needs room to
|
onWeekHeaderClicked: root.toggleWeekStart()
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,115 +1,42 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Io
|
|
||||||
import qs.Commons
|
import qs.Commons
|
||||||
import qs.Ui
|
import qs.Ui
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property int monthOffset: 0
|
|
||||||
property string monthName: ""
|
|
||||||
property string monthGrid: ""
|
|
||||||
property bool polling: false
|
property bool polling: false
|
||||||
|
|
||||||
implicitHeight: layout.implicitHeight
|
implicitHeight: calendar.implicitHeight
|
||||||
|
|
||||||
function loadMonth() {
|
function refreshToday() {
|
||||||
if (monthProbe.running) return
|
var now = new Date()
|
||||||
monthProbe.running = true
|
if (CalendarModel.keyForDate(now) === CalendarModel.keyForDate(calendar.today)) return
|
||||||
|
var followToday = calendar.selectionIsToday
|
||||||
|
calendar.today = now
|
||||||
|
if (followToday) calendar.goToToday()
|
||||||
}
|
}
|
||||||
|
|
||||||
function shiftMonth(delta) {
|
onPollingChanged: {
|
||||||
root.monthOffset += delta
|
if (!polling) return
|
||||||
root.loadMonth()
|
calendar.today = new Date()
|
||||||
}
|
calendar.goToToday()
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 3600000
|
interval: 60000
|
||||||
running: root.polling
|
running: root.polling
|
||||||
repeat: true
|
repeat: true
|
||||||
onTriggered: if (root.monthOffset === 0) root.loadMonth()
|
onTriggered: root.refreshToday()
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: root.loadMonth()
|
CalendarView {
|
||||||
|
id: calendar
|
||||||
WheelHandler {
|
|
||||||
onWheel: function(event) {
|
|
||||||
root.shiftMonth(event.angleDelta.y > 0 ? -1 : 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: layout
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Style.spaceReal(6)
|
foreground: Color.foreground
|
||||||
|
fontFamily: Style.font.family
|
||||||
Item {
|
cellSpacing: Style.space(2)
|
||||||
width: parent.width
|
cellWidth: Math.floor((width - cellSpacing * 6) / 7)
|
||||||
implicitHeight: Math.max(monthLabel.implicitHeight, navigation.implicitHeight)
|
cellHeight: Style.space(24)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user