diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-15 14:25:51 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-15 14:25:51 +0200 |
| commit | cb7c95ae7e14249cd9b6d8bba7923713a66eed39 (patch) | |
| tree | 8fd70b27336167bd22caa370e6b482763270a5eb | |
| parent | 9efe9458afdffe474944a04bc2f81864c0eac879 (diff) | |
| download | quickshell-cb7c95ae7e14249cd9b6d8bba7923713a66eed39.tar.gz quickshell-cb7c95ae7e14249cd9b6d8bba7923713a66eed39.zip | |
fix(notifications): keep the balloon model stable across ticks
The Repeater model was a fresh filter of Notify.queue on every 250ms tick
because the filter read now, so Qt Quick recreated every balloon delegate
four times a second, reloading icons and resetting hover. Model is now
Notify.queue itself, and each balloon drops itself at expiry through its
own visible binding, driven by the shared tick.
Removing required from the balloon's properties is part of the same fix: a
required property makes QML create the delegate in its own context, where
modelData and index are undefined, so notification: modelData silently
arrived undefined once a live queue item was actually drawn.
| -rw-r--r-- | notifications/Balloons.qml | 25 | ||||
| -rw-r--r-- | notifications/NotificationBalloon.qml | 20 |
2 files changed, 31 insertions, 14 deletions
diff --git a/notifications/Balloons.qml b/notifications/Balloons.qml index 28ffafb..9fa5a85 100644 --- a/notifications/Balloons.qml +++ b/notifications/Balloons.qml @@ -27,21 +27,20 @@ Scope { onTriggered: root.now = Date.now() } - // Which live notifications draw here. Suppression is deliberately here - // and not in the daemon: the drawer lists a notification DND chose not to - // pop, because a list the user opened is not an interruption. - readonly property var visible: (Notify.queue || []).filter(p => { - if (Notify.drawerOpen) return false; - if (p.expires !== 0 && root.now >= p.expires) return false; - if (Notify.snoozeUntil > root.now) return false; - if (Status.dnd && p.urgency !== "critical") return false; - return true; - }) + // The Repeater model is Notify.queue itself, so its identity only changes + // when the daemon rewrites the file, not on the 250ms tick. Expiry and + // snooze are the balloon's own business: the tick reassigns `now`, each + // balloon re-evaluates its visible binding and leaves the column at + // expiry. Rebuilding the model on the tick would recreate every delegate + // (reloading icons, resetting hover) four times a second. + readonly property var live: Notify.queue || [] PanelWindow { id: win - visible: root.visible.length > 0 + // Each balloon hides itself when it expires or is suppressed, so the + // column collapses to zero and the window follows it. + visible: column.implicitHeight > 0 screen: Quickshell.screens.find(s => s.name === "DP-1") ?? Quickshell.screens[0] anchors { bottom: true; right: true } margins { bottom: 12; right: 12 } @@ -60,8 +59,8 @@ Scope { spacing: 8 Repeater { - model: root.visible - NotificationBalloon { notification: modelData } + model: root.live + NotificationBalloon { notification: modelData; now: root.now } } } } diff --git a/notifications/NotificationBalloon.qml b/notifications/NotificationBalloon.qml index dc8072f..e3763da 100644 --- a/notifications/NotificationBalloon.qml +++ b/notifications/NotificationBalloon.qml @@ -16,7 +16,25 @@ import QtQuick Rectangle { id: b - required property var notification + // Neither property is `required` on purpose. A required property makes + // QML create the delegate in its own context, where `modelData` and + // `index` are not defined, so the Repeater's `notification: modelData` + // silently arrives undefined and every binding on it fails. + property var notification + property double now: 0 + + // Driven by the shell's tick. The balloon drops itself once its expiry + // passes, or while a suppression says so: the drawer and snooze withhold + // everything, DND withholds low and normal but still shows critical. The + // drawer lists a notification DND chose not to pop, because a list the + // user opened is not an interruption. + visible: { + if (Notify.drawerOpen) return false; + if (Notify.snoozeUntil > b.now) return false; + if (Status.dnd && b.notification.urgency !== "critical") return false; + if (b.notification.expires !== 0 && b.now >= b.notification.expires) return false; + return true; + } width: parent ? parent.width : 340 implicitHeight: texts.implicitHeight + 20 |
