aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-15 14:25:51 +0200
committerDanilo M. <danix@danix.xyz>2026-09-15 14:25:51 +0200
commitcb7c95ae7e14249cd9b6d8bba7923713a66eed39 (patch)
tree8fd70b27336167bd22caa370e6b482763270a5eb
parent9efe9458afdffe474944a04bc2f81864c0eac879 (diff)
downloadquickshell-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.qml25
-rw-r--r--notifications/NotificationBalloon.qml20
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