From 27401c25a05a028ee9f47a5e4dcc8de7ebe61005 Mon Sep 17 00:00:00 2001 From: Hannes Date: Wed, 2 Sep 2026 00:16:15 +0200 Subject: [PATCH] feat: refactor notification system to manage hover states centrally and auto close --- modules/bar/Bar.qml | 9 +++ modules/bar/widgets/NotificationButton.qml | 24 +++--- modules/notification/NotificationHistory.qml | 80 +++++++++++++++++-- .../NotificationHistoryLoader.qml | 26 +++++- .../notificationServer/NotificationServer.qml | 15 +++- 5 files changed, 129 insertions(+), 25 deletions(-) diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 847fa58..295c4da 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -84,3 +84,12 @@ PanelWindow { } } } + + +76 closeNHistory() + +157 notificationPopupEnabled +169 notificationPopupEnabled + +206, 207 ^^ + diff --git a/modules/bar/widgets/NotificationButton.qml b/modules/bar/widgets/NotificationButton.qml index ea99716..41af271 100644 --- a/modules/bar/widgets/NotificationButton.qml +++ b/modules/bar/widgets/NotificationButton.qml @@ -13,11 +13,20 @@ MouseArea { cursorShape: Qt.PointingHandCursor onClicked: { - // NotificationS.soundEnabled = !NotificationS.soundEnabled + // NotificationS.notificationPopupEnabled = !NotificationS.notificationPopupEnabled console.log("[NotificationButton] Opening NotificationHistory") root.toggleNotificationHistory() } + // ── Report hover to NotificationServer for auto-close logic ── + onContainsMouseChanged: NotificationS.buttonHovered = containsMouse + Component.onDestruction: { + if (containsMouse) NotificationS.buttonHovered = false + } + onVisibleChanged: { + if (!visible && NotificationS.buttonHovered) NotificationS.buttonHovered = false + } + Item { anchors.top: parent.topBar @@ -37,7 +46,7 @@ MouseArea { color: Colors.secondaryContainer_fg } } - + Rectangle { anchors.fill: parent color: "transparent" @@ -46,18 +55,11 @@ MouseArea { Text { id: notificationText anchors.centerIn: parent - // text: Quickshell.iconPath("discord") - text: NotificationS.soundEnabled ? "󰂚" : "󰂛" + // text: Quickshell.iconPath("discord") + text: NotificationS.notificationPopupEnabled ? "󰂚" : "󰂛" font.pixelSize: Config.bigIconSize(bar.height) font.family: Config.font color: Colors.secondaryContainer_fg } - - // Image{ - // id: iconImage - // source: Quickshell.iconPath("discord") - // width: Config.bigIconSize(notificationButton.height) - // height: Config.bigIconSize(notificationButton.height) - // } } } \ No newline at end of file diff --git a/modules/notification/NotificationHistory.qml b/modules/notification/NotificationHistory.qml index 3c1dd0d..629ce73 100644 --- a/modules/notification/NotificationHistory.qml +++ b/modules/notification/NotificationHistory.qml @@ -31,8 +31,7 @@ PanelWindow { property var grouped: { const vals = NotificationS.trackedNotifications.values - const len = vals.length - if (len < 0) return [] + if (!vals || vals.length === 0) return [] let map = {} for (let i = 0; i < vals.length; ++i) { const n = vals[i] @@ -65,21 +64,86 @@ PanelWindow { expandedGroups = ({}) } + function closeNHistory() { + // Centralized close via singleton signal – fixes previous ReferenceError + // where this file tried to access notifificationHistoryLoader.id directly + // (that id lives in NotificationHistoryLoader.qml and is not in scope here). + console.log("[NotificationHistory] closeNHistory() requested") + NotificationS.historyHovered = false + NotificationS.closeHistoryRequested() + } + + // ── Hover tracking via NotificationServer ── + // History reports its hover state to the singleton; button does the same. + // Timer auto-closes when neither is hovered for historyAutoCloseDelay ms. + property bool _allowAutoClose: false + + HoverHandler { + id: historyHoverHandler + onHoveredChanged: NotificationS.historyHovered = hovered + } + + // Fallback MouseArea that doesn't steal clicks (acceptedButtons: NoButton) + // Ensures hover is detected even if HoverHandler is missed on PanelWindow boundaries. + // Placed with high z but NoButton so it doesn't block inner MouseAreas. + MouseArea { + id: historyHoverMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onContainsMouseChanged: NotificationS.historyHovered = containsMouse + } + + // Grace timer: don't allow auto-close immediately after opening, + // giving keyboard users time to move mouse to history. + Timer { + id: graceTimer + interval: 500 + running: root.visible && !_allowAutoClose + repeat: false + onTriggered: _allowAutoClose = true + } + + Timer { + id: hoverCloseTimer + interval: NotificationS.historyAutoCloseDelay + // Only run when panel is visible, grace passed, and neither element is hovered + running: root.visible && _allowAutoClose && !NotificationS.historyHovered && !NotificationS.buttonHovered + repeat: false + onTriggered: { + if (!NotificationS.historyHovered && !NotificationS.buttonHovered) { + console.log("[NotificationHistory] Auto-closing after hover timeout (" + interval + "ms)") + root.closeNHistory() + } + } + } + + // Ensure hover state is cleared when window hides/destroys + onVisibleChanged: { + if (!visible) { + NotificationS.historyHovered = false + _allowAutoClose = false + } + } + Component.onDestruction: NotificationS.historyHovered = false FocusScope { id: nHistoryScope + anchors.fill: parent focus: true Keys.onReleased: function(event) { if (event.key === Qt.Key_Escape) { - console.log("Pressed Escape, closing JLearner") - notifificationHistoryLoader.active = false + console.log("Pressed Escape, closing NotificationHistory") + root.closeNHistory() } } Rectangle { anchors.fill: parent color: "lightblue" + visible: false // debug helper, keep hidden in normal use } } @@ -154,7 +218,7 @@ PanelWindow { spacing: Config.screen_spacing_fun(screen.height) Text{ - text: NotificationS.soundEnabled ? "󰂚" : "󰂛" + text: NotificationS.notificationPopupEnabled ? "󰂚" : "󰂛" font.family: Config.font font.pixelSize: topBar.height*0.75 color: textColor @@ -166,7 +230,7 @@ PanelWindow { implicitWidth: notificationVolumeSwitch.height*2 implicitHeight: topBar.height*0.75 anchors.verticalCenter: parent.verticalCenter - checked: NotificationS.soundEnabled + checked: NotificationS.notificationPopupEnabled indicator: Rectangle { implicitWidth: notificationVolumeSwitch.width @@ -203,8 +267,8 @@ PanelWindow { } onCheckedChanged: { - NotificationS.soundEnabled = notificationVolumeSwitch.checked - console.log("NotificationS.soundEnabled:", NotificationS.soundEnabled) + NotificationS.notificationPopupEnabled = notificationVolumeSwitch.checked + console.log("NotificationS.notificationPopupEnabled:", NotificationS.notificationPopupEnabled) } } diff --git a/modules/notification/NotificationHistoryLoader.qml b/modules/notification/NotificationHistoryLoader.qml index 28c6cbe..c48e970 100644 --- a/modules/notification/NotificationHistoryLoader.qml +++ b/modules/notification/NotificationHistoryLoader.qml @@ -15,10 +15,20 @@ Item { // container, invisible active: false NotificationHistory { id: nHistory + // closeNHistory() is defined inside NotificationHistory.qml and + // emits NotificationS.closeHistoryRequested(); the loader listens below + // and actually deactivates. Keeping instance id for potential direct access. + } + } - function closeNHistory() { - nHistoryLoader.closeNHistory() - } + // React to close requests coming from NotificationHistory (hover timer, Escape) + // or any other component via the singleton signal. This is the single + // source of truth for deactivating the history panel. + Connections { + target: NotificationS + function onCloseHistoryRequested() { + console.log("[NotificationHistoryLoader] closeHistoryRequested received -> closing") + nHistoryLoader.closeNHistory() } } @@ -37,11 +47,19 @@ Item { // container, invisible } function closeNHistory() { + // Reset hover states so timer doesn't immediately re-trigger on next open + NotificationS.historyHovered = false + // buttonHovered is managed by the button itself; don't force false here + // in case button is still hovered when history closes notifificationHistoryLoader.active = false } function toggleNHistory() { - notifificationHistoryLoader.active = !notifificationHistoryLoader.active + if (notifificationHistoryLoader.active) { + closeNHistory() + } else { + openNHistory() + } } Component.onCompleted: { diff --git a/modules/notification/notificationServer/NotificationServer.qml b/modules/notification/notificationServer/NotificationServer.qml index 443ea1e..93ef568 100644 --- a/modules/notification/notificationServer/NotificationServer.qml +++ b/modules/notification/notificationServer/NotificationServer.qml @@ -12,13 +12,24 @@ Item { property var focusedMonitor: null property var newestNotification: null - property bool soundEnabled: true + property bool notificationPopupEnabled: true property alias trackedNotifications: notificationServer.trackedNotifications property int notificationNum: trackedNotifications.values.length signal notificationReceived(var notification) + // ── Hover state for history auto-close ── + // Updated by NotificationHistory.qml (PanelWindow hover) and NotificationButton.qml (bar button hover) + property bool historyHovered: false + property bool buttonHovered: false + property int historyAutoCloseDelay: 1000 + signal closeHistoryRequested() + + // Debug logging for hover transitions + onHistoryHoveredChanged: Config.debug == "Notification" && console.log("[NotificationServer] historyHovered:", historyHovered) + onButtonHoveredChanged: Config.debug == "Notification" && console.log("[NotificationServer] buttonHovered:", buttonHovered) + function correctMonitor(screen) { return (Hyprland.monitorFor(screen) === focusedMonitor) } @@ -83,7 +94,7 @@ Item { repeat: false onTriggered: { - if((!notification.lastGeneration && notificationServerState.soundEnabled) || Config.debug == "Notification"){ + if((!notification.lastGeneration && notificationServerState.notificationPopupEnabled) || Config.debug == "Notification"){ focusedMonitor = Hyprland.focusedMonitor notificationReceived(notification) notificationSound.play()