From cb7c95ae7e14249cd9b6d8bba7923713a66eed39 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Tue, 15 Sep 2026 14:25:51 +0200 Subject: 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. --- notifications/Balloons.qml | 25 ++++++++++++------------- notifications/NotificationBalloon.qml | 20 +++++++++++++++++++- 2 files changed, 31 insertions(+), 14 deletions(-) (limited to 'notifications') 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 -- cgit v1.2.3