Add Ongoing to the Panama Daybook

This commit is contained in:
Gabriel Brown
2026-08-17 12:18:04 -04:00
parent 1ce5c732b7
commit a5450fdb6d
12 changed files with 483 additions and 47 deletions
@@ -7,8 +7,6 @@ import qs.modules.quicksettings
Item {
id: root
signal requestNotifications
readonly property bool isToday: CalendarAgenda.selectedDateKey === CalendarAgenda.dateKey(clock.date)
readonly property var upNext: CalendarAgenda.nextEventForSelectedDate
readonly property var listedEvents: CalendarAgenda.selectedEvents.filter(event => !root.upNext || event.id !== root.upNext.id)
@@ -238,10 +236,6 @@ Item {
}
}
NotificationSummary {
width: parent.width
onActivated: root.requestNotifications()
}
}
function relativeStart(event: var): string {
@@ -1,4 +1,4 @@
// GNOME's date/time menu: the calendar and the message tray in one panel,
// GNOME's date/time menu: calendar, ongoing activity and messages in one panel,
// dropping from the top centre because that is where GNOME put it and the
// muscle memory is the whole point.
//
@@ -20,8 +20,10 @@ import qs.widgets
PanelWindow {
id: root
// "notifications" is the existing overlay key, so Super+B, the notification
// IPC target and the clock all land here.
readonly property bool ongoingContentFits: panel.ongoingContentFits
// "notifications" remains the overlay key so existing keybinds keep their
// contract; the surface itself now has Agenda, Ongoing and Notifications.
visible: ShellState.notificationsOpen
color: "transparent"
@@ -1,5 +1,5 @@
// The approved A · Daybook: month context on the left, the selected day's
// schedule or grouped notification history on the right.
// schedule, persistent activity or grouped notification history on the right.
import QtQuick
import Quickshell
@@ -46,7 +46,7 @@ Item {
Text {
anchors.left: parent.left
anchors.leftMargin: 4
anchors.right: headerActions.left
anchors.right: dateMenuTabs.left
anchors.rightMargin: Theme.itemSpacing
anchors.verticalCenter: parent.verticalCenter
text: Qt.formatDateTime(clock.date, "dddd, MMMM d")
@@ -59,35 +59,36 @@ Item {
}
Row {
id: headerActions
id: dateMenuTabs
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 6
HeaderPill {
visible: ShellState.dateMenuPage === "notifications"
text: "← Back to agenda"
text: "Agenda"
active: ShellState.dateMenuPage === "agenda"
onClicked: ShellState.dateMenuPage = "agenda"
}
HeaderPill {
text: "Do Not Disturb"
active: Notifs.doNotDisturb
icon: "notifications-disabled-symbolic"
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
text: "Ongoing"
active: ShellState.dateMenuPage === "ongoing"
badge: Ongoing.count
onClicked: ShellState.dateMenuPage = "ongoing"
}
HeaderPill {
text: "Notifications"
active: ShellState.dateMenuPage === "notifications"
badge: Notifs.history.length
onClicked: ShellState.dateMenuPage = "notifications"
}
HeaderPill {
visible: ShellState.dateMenuPage === "agenda"
text: "Open Calendar ↗"
text: "Calendar ↗"
onClicked: CalendarAgenda.openCalendar(CalendarAgenda.selectedDate)
}
HeaderPill {
visible: ShellState.dateMenuPage === "notifications" && Notifs.hasNotifications
text: "Clear all"
onClicked: Notifs.dismissAll()
}
}
}
@@ -165,10 +166,16 @@ Item {
anchors.top: parent.top
anchors.margins: 14
visible: ShellState.dateMenuPage === "agenda"
onRequestNotifications: {
ShellState.dateMenuPage = "notifications";
Notifs.markAllRead();
}
}
OngoingPane {
id: ongoingPane
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 14
maxHeight: body.height - 28
visible: ShellState.dateMenuPage === "ongoing"
}
Column {
@@ -186,6 +193,8 @@ Item {
Text {
anchors.left: parent.left
anchors.right: notificationActions.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
text: "Notifications"
color: Theme.fg
@@ -194,14 +203,24 @@ Item {
font.weight: Font.DemiBold
}
Text {
Row {
id: notificationActions
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
text: Notifs.history.length + (Notifs.history.length === 1 ? " item" : " items")
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
spacing: 6
HeaderPill {
text: "Do Not Disturb"
active: Notifs.doNotDisturb
icon: "notifications-disabled-symbolic"
onClicked: Notifs.doNotDisturb = !Notifs.doNotDisturb
}
HeaderPill {
visible: Notifs.hasNotifications
text: "Clear all"
onClicked: Notifs.dismissAll()
}
}
}
@@ -215,9 +234,14 @@ Item {
}
}
readonly property real rightContentHeight: ShellState.dateMenuPage === "notifications"
? notificationPane.implicitHeight
: agenda.implicitHeight
readonly property real rightContentHeight: {
if (ShellState.dateMenuPage === "notifications")
return notificationPane.implicitHeight;
if (ShellState.dateMenuPage === "ongoing")
return ongoingPane.implicitHeight;
return agenda.implicitHeight;
}
readonly property bool ongoingContentFits: ongoingPane.implicitHeight <= body.height - 28
component HeaderPill: Rectangle {
id: pill
@@ -225,6 +249,7 @@ Item {
required property string text
property string icon: ""
property bool active: false
property int badge: -1
signal clicked
width: pillRow.implicitWidth + 20
@@ -260,6 +285,27 @@ Item {
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
Rectangle {
anchors.verticalCenter: parent.verticalCenter
visible: pill.badge > 0
width: visible ? badgeText.implicitWidth + 9 : 0
height: 17
radius: Theme.pillRadius
border.width: 0
color: Theme.alpha(pill.active ? Theme.accent : Theme.fg, 0.12)
Text {
id: badgeText
anchors.centerIn: parent
text: String(pill.badge)
color: pill.active ? Theme.accent : Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: 9
font.weight: Font.DemiBold
}
}
}
MouseArea {
@@ -0,0 +1,263 @@
// Persistent work and privacy activity inside the Daybook.
import QtQuick
import qs.config
import qs.services
import qs.modules.quicksettings
Item {
id: root
property real maxHeight: 500
implicitHeight: content.implicitHeight
Column {
id: content
width: parent.width
spacing: 12
Item {
width: parent.width
height: 38
Column {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: 2
Text {
text: "PERSISTENT ACTIVITY"
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.letterSpacing: 0.9
}
Text {
text: "Ongoing"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
}
}
Text {
anchors.right: parent.right
anchors.bottom: parent.bottom
text: Ongoing.count === 0 ? "Nothing active" : Ongoing.count + (Ongoing.count === 1 ? " activity" : " activities")
color: Theme.fgDim
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
}
}
Item {
width: parent.width
height: visible ? 190 : 0
visible: Ongoing.count === 0
Column {
anchors.centerIn: parent
spacing: 8
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "\u{F04A5}"
color: Theme.fgMuted
font.family: Theme.fontMono
font.pixelSize: 24
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Nothing ongoing"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Focus, Caffeine, recording and privacy activity appear here."
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
ScrollColumn {
width: parent.width
// Header + footer + the two 12px Column gaps consume 116px.
// Keeping that outside the scroll budget prevents the pane from
// growing beyond the Daybook when every producer is active.
maxHeight: Math.max(0, root.maxHeight - 116)
spacing: 8
visible: Ongoing.count > 0
Repeater {
model: Ongoing.activities
Rectangle {
id: activityRow
required property var modelData
width: parent.width
height: 62
radius: Theme.cardRadius
border.width: 0
color: rowMouse.containsMouse
? Theme.alpha(Theme.fg, 0.10)
: Theme.alpha(Theme.fg, 0.055)
Behavior on color { ColorAnimation { duration: Theme.durFast } }
Text {
id: activityIcon
anchors.left: parent.left
anchors.leftMargin: 13
anchors.verticalCenter: parent.verticalCenter
text: activityRow.modelData.glyph
color: root.toneColor(activityRow.modelData.tone)
font.family: Theme.fontMono
font.pixelSize: 17
}
Column {
anchors.left: activityIcon.right
anchors.leftMargin: 11
anchors.right: activityAction.visible ? activityAction.left : activityState.left
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: activityRow.modelData.label
color: Theme.fg
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.DemiBold
}
Text {
width: parent.width
text: activityRow.modelData.detail
color: Theme.fgDim
elide: Text.ElideRight
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
Rectangle {
id: activityState
anchors.right: activityAction.visible ? activityAction.left : parent.right
anchors.rightMargin: activityAction.visible ? 7 : 12
anchors.verticalCenter: parent.verticalCenter
width: stateLabel.implicitWidth + 16
height: 25
radius: Theme.pillRadius
border.width: 0
color: Theme.alpha(root.toneColor(activityRow.modelData.tone), 0.12)
Text {
id: stateLabel
anchors.centerIn: parent
text: activityRow.modelData.state
color: root.toneColor(activityRow.modelData.tone)
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
}
Rectangle {
id: activityAction
z: 2
anchors.right: parent.right
anchors.rightMargin: 9
anchors.verticalCenter: parent.verticalCenter
visible: activityRow.modelData.action !== ""
width: visible ? actionLabel.implicitWidth + 18 : 0
height: 28
radius: Theme.pillRadius
border.width: 0
color: actionMouse.containsMouse
? Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.20)
: Theme.alpha(activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg, 0.08)
Text {
id: actionLabel
anchors.centerIn: parent
text: activityRow.modelData.action
color: activityRow.modelData.tone === "danger" ? Theme.danger : Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
}
MouseArea {
id: actionMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: Ongoing.invoke(activityRow.modelData.kind)
}
}
MouseArea {
id: rowMouse
anchors.fill: parent
z: 1
enabled: activityRow.modelData.kind !== "caffeine"
hoverEnabled: enabled
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
onClicked: Ongoing.activate(activityRow.modelData.kind)
}
}
}
}
Rectangle {
width: parent.width
height: 54
radius: Theme.cardRadius
border.width: 0
visible: Ongoing.count > 0
color: Theme.alpha(Theme.fg, 0.035)
Column {
anchors.left: parent.left
anchors.leftMargin: 13
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
text: "Visible until it ends"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
Text {
text: "Completed screenshots, devices and other brief events stay in Signal Glass."
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
}
function toneColor(tone: string): color {
if (tone === "danger")
return Theme.danger;
if (tone === "warn")
return Theme.warn;
if (tone === "ok")
return Theme.ok;
return Theme.accent;
}
}
@@ -9,4 +9,5 @@ MonthArrow 1.0 MonthArrow.qml
NotificationGroup 1.0 NotificationGroup.qml
NotificationList 1.0 NotificationList.qml
NotificationSummary 1.0 NotificationSummary.qml
OngoingPane 1.0 OngoingPane.qml
WeatherCard 1.0 WeatherCard.qml
@@ -0,0 +1,98 @@
pragma Singleton
// One presentation model for state that remains active until it is explicitly
// stopped or naturally completes. Transient confirmations stay in Signal Glass.
import Quickshell
import QtQuick
Singleton {
id: root
readonly property var activities: {
const result = [];
if (FocusSession.active) {
result.push({
kind: "focus",
glyph: "\u{F051F}",
label: "Focus session",
detail: (FocusSession.workspaceLabel || "Focused workspace") + " · " + FocusSession.statusText,
state: FocusSession.remainingText,
tone: FocusSession.paused ? "warn" : "accent",
action: FocusSession.paused ? "Resume" : "Pause"
});
}
if (Caffeine.enabled) {
result.push({
kind: "caffeine",
glyph: "\u{F0F2E}",
label: "Caffeine",
detail: "Automatic sleep is blocked",
state: "On",
tone: "ok",
action: "Turn off"
});
}
if (PrivacyState.recordingActive) {
result.push({
kind: "recording",
glyph: "\u{F044A}",
label: "Screen recording",
detail: "Panama · " + root.recordingElapsed,
state: "Live",
tone: "danger",
action: "Stop"
});
}
if (PrivacyState.screenSharingActive)
result.push(root.privacyActivity("screen", "\u{F0379}", "Screen sharing", PrivacyState.screenSharingApp));
if (PrivacyState.cameraActive)
result.push(root.privacyActivity("camera", "\u{F0100}", "Camera", PrivacyState.cameraApp));
if (PrivacyState.microphoneActive)
result.push(root.privacyActivity("microphone", "\u{F036C}", "Microphone", PrivacyState.microphoneApp));
return result;
}
readonly property int count: activities.length
readonly property string recordingElapsed: {
const total = Capture.recordingSeconds;
const seconds = String(total % 60).padStart(2, "0");
const minutes = Math.floor(total / 60) % 60;
const hours = Math.floor(total / 3600);
return hours > 0
? `${hours}:${String(minutes).padStart(2, "0")}:${seconds}`
: `${minutes}:${seconds}`;
}
function privacyActivity(kind: string, glyph: string, label: string, app: string): var {
return {
kind,
glyph,
label,
detail: app || "Managed by the application",
state: "In use",
tone: "warn",
action: ""
};
}
function activate(kind: string): void {
if (kind === "focus")
FocusSession.activateWorkspace();
else if (["recording", "screen", "camera", "microphone"].indexOf(kind) >= 0)
ShellState.open("activity");
}
function invoke(kind: string): void {
if (kind === "focus")
FocusSession.pauseOrResume();
else if (kind === "caffeine")
Caffeine.toggle();
else if (kind === "recording")
Capture.stopRecording();
}
}
@@ -138,6 +138,8 @@ Singleton {
root.applyStates(false, true, false, "", "Contract fixture", "", true);
else if (name === "screen-on")
root.applyStates(false, false, true, "", "", "Contract fixture", true);
else if (name === "all-on")
root.applyStates(true, true, true, "Contract fixture", "Contract fixture", "Contract fixture", true);
else if (name === "all-off")
root.applyStates(false, false, false, "", "", "", true);
}
@@ -35,8 +35,8 @@ Singleton {
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.
// The date menu keeps calendar context fixed while its right pane switches
// between the schedule, persistent activity and message history.
property string dateMenuPage: "agenda"
// 0 means "use the currently focused workspace". A positive value lets a
@@ -53,12 +53,12 @@ Singleton {
}
function openDateMenu(page: string): void {
root.dateMenuPage = page === "notifications" ? "notifications" : "agenda";
root.dateMenuPage = root.normalizedDateMenuPage(page);
root.activeOverlay = "notifications";
}
function toggleDateMenu(page: string): void {
const target = page === "notifications" ? "notifications" : "agenda";
const target = root.normalizedDateMenuPage(page);
if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
root.activeOverlay = "";
return;
@@ -67,6 +67,10 @@ Singleton {
root.activeOverlay = "notifications";
}
function normalizedDateMenuPage(page: string): string {
return ["agenda", "ongoing", "notifications"].indexOf(page) >= 0 ? page : "agenda";
}
function openOverview(workspaceId: int): void {
root.overviewWorkspaceId = Math.max(0, workspaceId);
root.overviewQuery = "";
+5 -2
View File
@@ -79,7 +79,7 @@ ShellRoot {
Overview {}
// QuickSettings is the window; QuickSettingsPanel is its content.
QuickSettings {}
DateMenu {}
DateMenu { id: dateMenu }
ClipboardPanel {}
CaptureOverlay {}
IntelligenceResult {}
@@ -268,6 +268,7 @@ ShellRoot {
function reset(): void { CalendarAgenda.clearFixture(); }
function refresh(): void { CalendarAgenda.refresh(); }
function open(): void { ShellState.openDateMenu("agenda"); }
function page(name: string): void { ShellState.openDateMenu(name); }
function close(): void { ShellState.close(); }
function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
function status(): string {
@@ -281,7 +282,9 @@ ShellRoot {
selectedDate: CalendarAgenda.selectedDateKey,
selectedEventCount: CalendarAgenda.selectedEvents.length,
capsuleVisible: CalendarAgenda.capsuleVisible,
capsuleText: CalendarAgenda.capsuleText
capsuleText: CalendarAgenda.capsuleText,
ongoingCount: Ongoing.count,
ongoingFits: dateMenu.ongoingContentFits
});
}
}