Route the interactive Panama date menu
This commit is contained in:
@@ -24,7 +24,7 @@ Pill {
|
|||||||
return date + time;
|
return date + time;
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivated: ShellState.toggle("notifications")
|
onActivated: ShellState.toggleDateMenu("agenda")
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -4,17 +4,20 @@
|
|||||||
// months and the panel below it never jumps. Days belonging to the neighbouring
|
// months and the panel below it never jumps. Days belonging to the neighbouring
|
||||||
// months fill the edges, dimmed. Today wears the accent.
|
// months fill the edges, dimmed. Today wears the accent.
|
||||||
//
|
//
|
||||||
// Deliberately non-interactive apart from the header: there is no calendar
|
// Days become real navigation targets now that Evolution Data Server backs the
|
||||||
// backend behind this, so a day cell that highlighted on hover would be
|
// panel. Up to three source-coloured markers show whether a date has events.
|
||||||
// promising a click that does nothing.
|
|
||||||
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.config
|
import qs.config
|
||||||
|
import qs.services
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
signal dateActivated(date value)
|
||||||
|
signal visibleMonthChanged(int year, int month)
|
||||||
|
|
||||||
implicitHeight: body.implicitHeight
|
implicitHeight: body.implicitHeight
|
||||||
|
|
||||||
// Cells divide the available width evenly; the height is fixed so the grid
|
// Cells divide the available width evenly; the height is fixed so the grid
|
||||||
@@ -37,6 +40,7 @@ Item {
|
|||||||
// panel opens, so it never comes back showing wherever you paged off to.
|
// panel opens, so it never comes back showing wherever you paged off to.
|
||||||
property int viewYear: root.todayYear
|
property int viewYear: root.todayYear
|
||||||
property int viewMonth: root.todayMonth
|
property int viewMonth: root.todayMonth
|
||||||
|
property date selectedDate: CalendarAgenda.selectedDate
|
||||||
|
|
||||||
readonly property bool onCurrentMonth: root.viewYear === root.todayYear && root.viewMonth === root.todayMonth
|
readonly property bool onCurrentMonth: root.viewYear === root.todayYear && root.viewMonth === root.todayMonth
|
||||||
|
|
||||||
@@ -53,30 +57,29 @@ Item {
|
|||||||
root.viewMonth = d.getMonth();
|
root.viewMonth = d.getMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activateCell(model: var): void {
|
||||||
|
if (!model.current) {
|
||||||
|
root.viewYear = model.date.getFullYear();
|
||||||
|
root.viewMonth = model.date.getMonth();
|
||||||
|
}
|
||||||
|
root.dateActivated(model.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewYearChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth)
|
||||||
|
onViewMonthChanged: root.visibleMonthChanged(root.viewYear, root.viewMonth)
|
||||||
|
|
||||||
readonly property var cells: {
|
readonly property var cells: {
|
||||||
const first = new Date(root.viewYear, root.viewMonth, 1);
|
const first = new Date(root.viewYear, root.viewMonth, 1);
|
||||||
const offset = first.getDay(); // 0 = Sunday, matching the header row
|
const offset = first.getDay(); // 0 = Sunday, matching the header row
|
||||||
const inThisMonth = new Date(root.viewYear, root.viewMonth + 1, 0).getDate();
|
|
||||||
const inPrevMonth = new Date(root.viewYear, root.viewMonth, 0).getDate();
|
|
||||||
|
|
||||||
const out = [];
|
const out = [];
|
||||||
for (let i = 0; i < 42; i++) {
|
for (let i = 0; i < 42; i++) {
|
||||||
const n = i - offset + 1;
|
const date = new Date(root.viewYear, root.viewMonth, i - offset + 1);
|
||||||
if (n < 1)
|
out.push({
|
||||||
out.push({
|
date: date,
|
||||||
day: inPrevMonth + n,
|
day: date.getDate(),
|
||||||
current: false
|
current: date.getMonth() === root.viewMonth && date.getFullYear() === root.viewYear
|
||||||
});
|
});
|
||||||
else if (n > inThisMonth)
|
|
||||||
out.push({
|
|
||||||
day: n - inThisMonth,
|
|
||||||
current: false
|
|
||||||
});
|
|
||||||
else
|
|
||||||
out.push({
|
|
||||||
day: n,
|
|
||||||
current: true
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -198,6 +201,8 @@ Item {
|
|||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.onCurrentMonth
|
readonly property bool isToday: cell.modelData.current && cell.modelData.day === root.todayDay && root.onCurrentMonth
|
||||||
|
readonly property bool isSelected: CalendarAgenda.dateKey(cell.modelData.date) === CalendarAgenda.dateKey(root.selectedDate)
|
||||||
|
readonly property var markers: CalendarAgenda.markersForDate(cell.modelData.date).slice(0, 3)
|
||||||
|
|
||||||
width: root.cellWidth
|
width: root.cellWidth
|
||||||
height: root.cellHeight
|
height: root.cellHeight
|
||||||
@@ -208,8 +213,18 @@ Item {
|
|||||||
height: root.cellHeight - 4
|
height: root.cellHeight - 4
|
||||||
radius: width / 2
|
radius: width / 2
|
||||||
border.width: 0
|
border.width: 0
|
||||||
visible: cell.isToday
|
visible: cell.isToday || cell.isSelected || cellMouse.containsMouse
|
||||||
color: Theme.accent
|
color: {
|
||||||
|
if (cell.isToday)
|
||||||
|
return Theme.accent;
|
||||||
|
if (cell.isSelected)
|
||||||
|
return Theme.alpha(Theme.accent, 0.20);
|
||||||
|
return Theme.alpha(Theme.fg, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.durFast }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
@@ -223,11 +238,44 @@ Item {
|
|||||||
color: {
|
color: {
|
||||||
if (cell.isToday)
|
if (cell.isToday)
|
||||||
return Theme.bgDark;
|
return Theme.bgDark;
|
||||||
|
if (cell.isSelected)
|
||||||
|
return Theme.accent;
|
||||||
return cell.modelData.current ? Theme.fg : Theme.fgMuted;
|
return cell.modelData.current ? Theme.fg : Theme.fgMuted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: 1
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: cell.markers
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
width: 3
|
||||||
|
height: 3
|
||||||
|
radius: 2
|
||||||
|
border.width: 0
|
||||||
|
color: cell.isToday ? Theme.bgDark : modelData.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: cellMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.activateCell(cell.modelData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: root.visibleMonthChanged(root.viewYear, root.viewMonth)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ Singleton {
|
|||||||
|
|
||||||
readonly property bool anyOverlayOpen: activeOverlay !== ""
|
readonly property bool anyOverlayOpen: activeOverlay !== ""
|
||||||
|
|
||||||
|
// The date menu is one surface with two deliberate entry points. The clock
|
||||||
|
// opens the Daybook; Super+B and notification actions open message history.
|
||||||
|
property string dateMenuPage: "agenda"
|
||||||
|
|
||||||
// 0 means "use the currently focused workspace". A positive value lets a
|
// 0 means "use the currently focused workspace". A positive value lets a
|
||||||
// contextual surface, such as Focus, open Mission Control at its target.
|
// contextual surface, such as Focus, open Mission Control at its target.
|
||||||
property int overviewWorkspaceId: 0
|
property int overviewWorkspaceId: 0
|
||||||
@@ -48,6 +52,21 @@ Singleton {
|
|||||||
root.activeOverlay = name;
|
root.activeOverlay = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openDateMenu(page: string): void {
|
||||||
|
root.dateMenuPage = page === "notifications" ? "notifications" : "agenda";
|
||||||
|
root.activeOverlay = "notifications";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDateMenu(page: string): void {
|
||||||
|
const target = page === "notifications" ? "notifications" : "agenda";
|
||||||
|
if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
|
||||||
|
root.activeOverlay = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.dateMenuPage = target;
|
||||||
|
root.activeOverlay = "notifications";
|
||||||
|
}
|
||||||
|
|
||||||
function openOverview(workspaceId: int): void {
|
function openOverview(workspaceId: int): void {
|
||||||
root.overviewWorkspaceId = Math.max(0, workspaceId);
|
root.overviewWorkspaceId = Math.max(0, workspaceId);
|
||||||
root.overviewQuery = "";
|
root.overviewQuery = "";
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ ShellRoot {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "notifications"
|
target: "notifications"
|
||||||
function toggle(): void { ShellState.toggle("notifications"); }
|
function toggle(): void { ShellState.toggleDateMenu("notifications"); }
|
||||||
|
function open(): void { ShellState.openDateMenu("notifications"); }
|
||||||
function clear(): void { Notifs.dismissAll(); }
|
function clear(): void { Notifs.dismissAll(); }
|
||||||
function dnd(): bool {
|
function dnd(): bool {
|
||||||
Notifs.doNotDisturb = !Notifs.doNotDisturb;
|
Notifs.doNotDisturb = !Notifs.doNotDisturb;
|
||||||
@@ -266,13 +267,15 @@ ShellRoot {
|
|||||||
function fixture(name: string): void { CalendarAgenda.applyFixture(name); }
|
function fixture(name: string): void { CalendarAgenda.applyFixture(name); }
|
||||||
function reset(): void { CalendarAgenda.clearFixture(); }
|
function reset(): void { CalendarAgenda.clearFixture(); }
|
||||||
function refresh(): void { CalendarAgenda.refresh(); }
|
function refresh(): void { CalendarAgenda.refresh(); }
|
||||||
function open(): void { ShellState.open("notifications"); }
|
function open(): void { ShellState.openDateMenu("agenda"); }
|
||||||
function close(): void { ShellState.close(); }
|
function close(): void { ShellState.close(); }
|
||||||
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
|
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
|
||||||
function status(): string {
|
function status(): string {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
phase: CalendarAgenda.phase,
|
phase: CalendarAgenda.phase,
|
||||||
fixture: CalendarAgenda.fixtureMode,
|
fixture: CalendarAgenda.fixtureMode,
|
||||||
|
open: ShellState.notificationsOpen,
|
||||||
|
page: ShellState.dateMenuPage,
|
||||||
sourceCount: CalendarAgenda.sources.length,
|
sourceCount: CalendarAgenda.sources.length,
|
||||||
eventCount: CalendarAgenda.events.length,
|
eventCount: CalendarAgenda.events.length,
|
||||||
selectedDate: CalendarAgenda.selectedDateKey,
|
selectedDate: CalendarAgenda.selectedDateKey,
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ git commit -m "Add live calendar state to Quickshell"
|
|||||||
- Produces: `ShellState.dateMenuPage`, `ShellState.toggleDateMenu(page)`, and `ShellState.openDateMenu(page)`.
|
- Produces: `ShellState.dateMenuPage`, `ShellState.toggleDateMenu(page)`, and `ShellState.openDateMenu(page)`.
|
||||||
- Produces from CalendarGrid: signals `dateActivated(date)` and `visibleMonthChanged(year, month)`.
|
- Produces from CalendarGrid: signals `dateActivated(date)` and `visibleMonthChanged(year, month)`.
|
||||||
|
|
||||||
- [ ] **Step 1: Extend the failing contract with page-routing assertions**
|
- [x] **Step 1: Extend the failing contract with page-routing assertions**
|
||||||
|
|
||||||
Append fixture assertions:
|
Append fixture assertions:
|
||||||
|
|
||||||
@@ -433,11 +433,11 @@ jq -e '.open == true and .page == "notifications"' <<<"$(qs ipc call calendar-ag
|
|||||||
|
|
||||||
Update the calendar status response to include only `open: ShellState.notificationsOpen` and `page: ShellState.dateMenuPage` in addition to the non-sensitive fields from Task 2.
|
Update the calendar status response to include only `open: ShellState.notificationsOpen` and `page: ShellState.dateMenuPage` in addition to the non-sensitive fields from Task 2.
|
||||||
|
|
||||||
- [ ] **Step 2: Run the contract and verify RED**
|
- [x] **Step 2: Run the contract and verify RED**
|
||||||
|
|
||||||
Expected: FAIL because `dateMenuPage` and notification `open()` do not exist.
|
Expected: FAIL because `dateMenuPage` and notification `open()` do not exist.
|
||||||
|
|
||||||
- [ ] **Step 3: Implement explicit ShellState routing**
|
- [x] **Step 3: Implement explicit ShellState routing**
|
||||||
|
|
||||||
Add:
|
Add:
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ function toggleDateMenu(page: string): void {
|
|||||||
|
|
||||||
Change Clock activation to `ShellState.toggleDateMenu("agenda")`. Change notification IPC `toggle()` to `ShellState.toggleDateMenu("notifications")`, add `open()` using `openDateMenu("notifications")`, and leave `Super+B` routed through that IPC target.
|
Change Clock activation to `ShellState.toggleDateMenu("agenda")`. Change notification IPC `toggle()` to `ShellState.toggleDateMenu("notifications")`, add `open()` using `openDateMenu("notifications")`, and leave `Super+B` routed through that IPC target.
|
||||||
|
|
||||||
- [ ] **Step 4: Make CalendarGrid date-aware and event-aware**
|
- [x] **Step 4: Make CalendarGrid date-aware and event-aware**
|
||||||
|
|
||||||
Replace numeric-only cell objects with full local `Date` values and add:
|
Replace numeric-only cell objects with full local `Date` values and add:
|
||||||
|
|
||||||
@@ -484,7 +484,7 @@ Every cell receives `markers: CalendarAgenda.markersForDate(cell.date).slice(0,
|
|||||||
|
|
||||||
Month changes call `visibleMonthChanged(viewYear, viewMonth)`. Adjacent-month date activation changes month and selection in one click.
|
Month changes call `visibleMonthChanged(viewYear, viewMonth)`. Adjacent-month date activation changes month and selection in one click.
|
||||||
|
|
||||||
- [ ] **Step 5: Verify routing and grid contracts**
|
- [x] **Step 5: Verify routing and grid contracts**
|
||||||
|
|
||||||
Run:
|
Run:
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ rg -q 'dateActivated' config/dot/quickshell/modules/datemenu/CalendarGrid.qml
|
|||||||
|
|
||||||
Expected: contract PASS and both interaction hooks present.
|
Expected: contract PASS and both interaction hooks present.
|
||||||
|
|
||||||
- [ ] **Step 6: Commit routing and grid behavior**
|
- [x] **Step 6: Commit routing and grid behavior**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git add config/dot/quickshell/services/ShellState.qml \
|
git add config/dot/quickshell/services/ShellState.qml \
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ jq -e '.selectedDate == "2026-08-18" and .selectedEventCount == 1' \
|
|||||||
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|| fail 'date selection did not update events'
|
|| fail 'date selection did not update events'
|
||||||
|
|
||||||
|
qs ipc call calendar-agenda open >/dev/null
|
||||||
|
jq -e '.open == true and .page == "agenda"' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'calendar open did not route to Agenda'
|
||||||
|
|
||||||
|
qs ipc call notifications open >/dev/null
|
||||||
|
jq -e '.open == true and .page == "notifications"' \
|
||||||
|
<<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|
|| fail 'notification open did not route to Notifications'
|
||||||
|
|
||||||
qs ipc call calendar-agenda reset >/dev/null
|
qs ipc call calendar-agenda reset >/dev/null
|
||||||
jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
|
||||||
|| fail 'fixture reset did not restore live mode'
|
|| fail 'fixture reset did not restore live mode'
|
||||||
|
|||||||
Reference in New Issue
Block a user