From c1588ceb5e9932a16b593963ae6264c31a70ce60 Mon Sep 17 00:00:00 2001 From: Damocles Date: Wed, 22 Apr 2026 22:44:39 +0200 Subject: [PATCH] extract NotifApplet, fold NotifCenter into NotificationsModule with hover+pin --- .../NotifApplet.qml} | 125 ++++-------------- shell/applets/qmldir | 1 + shell/modules/Bar.qml | 1 - shell/modules/NotificationsModule.qml | 101 +++++++++++--- shell/modules/qmldir | 1 - 5 files changed, 106 insertions(+), 123 deletions(-) rename shell/{modules/NotifCenter.qml => applets/NotifApplet.qml} (77%) diff --git a/shell/modules/NotifCenter.qml b/shell/applets/NotifApplet.qml similarity index 77% rename from shell/modules/NotifCenter.qml rename to shell/applets/NotifApplet.qml index 27180a8..7454b1f 100644 --- a/shell/modules/NotifCenter.qml +++ b/shell/applets/NotifApplet.qml @@ -1,81 +1,26 @@ import QtQuick import Quickshell -import "." as M import "../services" as S +import "../modules" as M -M.HoverPanel { - id: menuWindow +Column { + id: root - popupMode: true - contentWidth: 350 + required property color accentColor + required property int contentWidth - // Header: title + clear all + DND toggle - Item { - width: menuWindow.contentWidth - height: 32 - - Text { - anchors.left: parent.left - anchors.leftMargin: 12 - anchors.verticalCenter: parent.verticalCenter - text: "Notifications" - color: S.Theme.base05 - font.pixelSize: S.Theme.fontSize + 1 - font.family: S.Theme.fontFamily - font.bold: true - } - - Row { - anchors.right: parent.right - anchors.rightMargin: 8 - anchors.verticalCenter: parent.verticalCenter - spacing: 8 - - // DND toggle - Text { - text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C" - color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04 - font.pixelSize: S.Theme.fontSize - font.family: S.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: S.NotifService.toggleDnd() - } - } - - // Clear all - Text { - text: "\uF1F8" - color: clearArea.containsMouse ? S.Theme.base08 : S.Theme.base04 - font.pixelSize: S.Theme.fontSize - font.family: S.Theme.iconFontFamily - anchors.verticalCenter: parent.verticalCenter - visible: S.NotifService.count > 0 - - MouseArea { - id: clearArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: menuWindow._cascadeDismiss() - } - } - } + function cascadeDismiss() { + _cascadeDismiss(); } + // ── Cascade dismiss logic ─────────────────────────────────────────── property var _pendingDismissIds: [] - - // Collapsed groups set — reassign to trigger reactivity property var _collapsedGroups: ({}) - property real _savedScrollY: 0 property bool _restoringScroll: false function _toggleCollapse(appName) { - _savedScrollY = notifList.contentY; + _savedScrollY = _notifList.contentY; _restoringScroll = true; const next = Object.assign({}, _collapsedGroups); if (next[appName]) @@ -85,7 +30,6 @@ M.HoverPanel { _collapsedGroups = next; } - // Group notifications by appName, sorted by max urgency desc then most recent time desc readonly property var _groups: { const map = {}; for (const n of S.NotifService.list) { @@ -111,7 +55,6 @@ M.HoverPanel { }); } - // Flat model: group header followed by its notifications (omitted when collapsed) readonly property var _flatModel: { const arr = []; for (const g of _groups) { @@ -135,7 +78,6 @@ M.HoverPanel { return arr; } - // Collect visible (non-dismissing) notif delegates, optionally filtered by appName function _getVisibleNotifDelegates(appName) { const result = []; for (let i = 0; i < _flatModel.length; i++) { @@ -144,7 +86,7 @@ M.HoverPanel { continue; if (appName !== undefined && item.data.appName !== appName) continue; - const d = notifList.itemAtIndex(i); + const d = _notifList.itemAtIndex(i); if (d && d._type === "notif" && d._notif?.state !== "dismissing") result.push(d); } @@ -158,7 +100,7 @@ M.HoverPanel { return; } for (let i = 0; i < visibles.length; i++) { - _cascadeTimer.createObject(menuWindow, { + _cascadeTimer.createObject(root, { _target: visibles[i], _delay: i * 60, _isLast: i === visibles.length - 1 @@ -198,7 +140,7 @@ M.HoverPanel { if (_target && _target.dismissVisualOnly) _target.dismissVisualOnly(); if (_isLast) - _bulkTimer.createObject(menuWindow, {}); + root._bulkTimer.createObject(root, {}); destroy(); } } @@ -206,36 +148,28 @@ M.HoverPanel { property Component _bulkTimer: Component { Timer { - interval: 400 // swipe (200) + collapse (150) + margin + interval: 400 running: true onTriggered: { - menuWindow._finishCascade(); + root._finishCascade(); destroy(); } } } - // Separator - Rectangle { - width: menuWindow.contentWidth - 16 - height: 1 - anchors.horizontalCenter: parent.horizontalCenter - color: S.Theme.base03 - } - - // Notification list (scrollable) + // ── Notification list ──────────────────────────────────────────────── ListView { - id: notifList - width: menuWindow.contentWidth + id: _notifList + width: root.contentWidth height: Math.min(contentHeight, 60 * (S.Modules.notifications.maxVisible || 10)) clip: true boundsBehavior: Flickable.StopAtBounds - model: menuWindow._flatModel + model: root._flatModel onContentHeightChanged: { - if (menuWindow._restoringScroll) { - contentY = Math.min(menuWindow._savedScrollY, Math.max(0, contentHeight - height)); - menuWindow._restoringScroll = false; + if (root._restoringScroll) { + contentY = Math.min(root._savedScrollY, Math.max(0, contentHeight - height)); + root._restoringScroll = false; } } @@ -247,7 +181,7 @@ M.HoverPanel { readonly property string _type: modelData.type readonly property var _notif: _type === "notif" ? modelData.data : null - width: menuWindow.contentWidth + width: root.contentWidth height: _displayTargetHeight * _heightScale clip: true opacity: 0 @@ -258,7 +192,6 @@ M.HoverPanel { return _notifCard.implicitHeight; } - // Animated version of _targetHeight — smoothly transitions header height on collapse property real _displayTargetHeight: _targetHeight Behavior on _displayTargetHeight { NumberAnimation { @@ -305,7 +238,6 @@ M.HoverPanel { id: _headerHover } - // Tap target for collapse — covers header row only, excludes dismiss button Item { anchors.left: parent.left anchors.right: _groupDismissBtn.left @@ -316,7 +248,7 @@ M.HoverPanel { cursorShape: Qt.PointingHandCursor } TapHandler { - onTapped: menuWindow._toggleCollapse(notifDelegate.modelData.appName) + onTapped: root._toggleCollapse(notifDelegate.modelData.appName) } } @@ -342,7 +274,6 @@ M.HoverPanel { asynchronous: true } - // Collapse chevron Text { id: _chevron anchors.right: _groupDismissBtn.left @@ -357,7 +288,6 @@ M.HoverPanel { opacity: _headerHover.hovered ? 1 : 0 } - // App name Text { anchors.left: _headerIcon.visible ? _headerIcon.right : parent.left anchors.leftMargin: _headerIcon.visible ? 6 : 10 @@ -374,7 +304,6 @@ M.HoverPanel { elide: Text.ElideRight } - // Dismiss button — opacity-hidden when header not hovered Text { id: _groupDismissBtn anchors.right: parent.right @@ -396,12 +325,11 @@ M.HoverPanel { TapHandler { onTapped: { if (notifDelegate._type === "header") - menuWindow._cascadeGroupDismiss(notifDelegate.modelData.appName); + root._cascadeGroupDismiss(notifDelegate.modelData.appName); } } } - // Collapsed preview: one line per notification summary Repeater { model: (notifDelegate._type === "header" && notifDelegate.modelData.collapsed) ? notifDelegate.modelData.summaries : [] @@ -425,7 +353,6 @@ M.HoverPanel { } // ---- Individual notification ---- - M.NotifCard { id: _notifCard visible: notifDelegate._type === "notif" @@ -445,7 +372,7 @@ M.HoverPanel { NumberAnimation { target: notifDelegate property: "x" - to: menuWindow.contentWidth + to: root.contentWidth duration: 200 easing.type: Easing.InCubic } @@ -477,7 +404,7 @@ M.HoverPanel { // Empty state Text { visible: S.NotifService.count === 0 - width: menuWindow.contentWidth + width: root.contentWidth height: 48 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter diff --git a/shell/applets/qmldir b/shell/applets/qmldir index f74b6f7..bf26e90 100644 --- a/shell/applets/qmldir +++ b/shell/applets/qmldir @@ -10,6 +10,7 @@ HexWaveBackground 1.0 HexWaveBackground.qml MemoryApplet 1.0 MemoryApplet.qml MprisApplet 1.0 MprisApplet.qml NetworkApplet 1.0 NetworkApplet.qml +NotifApplet 1.0 NotifApplet.qml TemperatureApplet 1.0 TemperatureApplet.qml VolumeApplet 1.0 VolumeApplet.qml WeatherApplet 1.0 WeatherApplet.qml diff --git a/shell/modules/Bar.qml b/shell/modules/Bar.qml index fd86f92..0458381 100644 --- a/shell/modules/Bar.qml +++ b/shell/modules/Bar.qml @@ -114,7 +114,6 @@ PanelWindow { visible: S.Modules.clock.enable } M.NotificationsModule { - bar: bar visible: S.Modules.notifications.enable } } diff --git a/shell/modules/NotificationsModule.qml b/shell/modules/NotificationsModule.qml index 4783f44..7df4b2b 100644 --- a/shell/modules/NotificationsModule.qml +++ b/shell/modules/NotificationsModule.qml @@ -3,21 +3,32 @@ import Quickshell import Quickshell.Services.Notifications import "." as M import "../services" as S +import "../applets" as C M.BarSection { id: root spacing: S.Theme.moduleSpacing - tooltip: { - const parts = [S.NotifService.count + " notification" + (S.NotifService.count !== 1 ? "s" : "")]; - if (S.NotifService.dnd) - parts.push("Do not disturb"); - return parts.join("\n"); - } - - required property var bar + tooltip: "" readonly property bool hasUrgent: S.NotifService.list.some(n => n.urgency === NotificationUrgency.Critical && n.state !== "dismissed") + property bool _pinned: false + readonly property bool _anyHover: root._hovered || hoverPanel.panelHovered + readonly property bool _showPanel: _anyHover || _pinned + + on_AnyHoverChanged: { + if (_anyHover) + _unpinTimer.stop(); + else if (_pinned) + _unpinTimer.start(); + } + + Timer { + id: _unpinTimer + interval: 500 + onTriggered: root._pinned = false + } + M.BarIcon { icon: { if (S.NotifService.dnd) @@ -26,12 +37,18 @@ M.BarSection { } color: S.NotifService.dnd ? S.Theme.base04 : root.accentColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + onTapped: root._pinned = !root._pinned + } } M.BarLabel { id: countLabel label: S.NotifService.count > 0 ? String(S.NotifService.count) + (root.hasUrgent ? "!" : "") : "" color: root.hasUrgent ? S.Theme.base08 : root.accentColor anchors.verticalCenter: parent.verticalCenter + TapHandler { + onTapped: root._pinned = !root._pinned + } transform: Scale { id: countScale @@ -68,26 +85,66 @@ M.BarSection { } } - TapHandler { - acceptedButtons: Qt.LeftButton - onTapped: { - centerLoader.active = !centerLoader.active; - M.FlyoutState.visible = false; - } - } + // Right-click DND quick toggle TapHandler { acceptedButtons: Qt.RightButton onTapped: S.NotifService.toggleDnd() } - LazyLoader { - id: centerLoader - active: false - M.NotifCenter { + M.HoverPanel { + id: hoverPanel + showPanel: root._showPanel + screen: QsWindow.window?.screen ?? null + anchorItem: root + accentColor: root.accentColor + panelNamespace: "nova-notifications" + panelTitle: "Notifications" + contentWidth: 350 + titleActionsComponent: Component { + Row { + spacing: 8 + + // DND toggle + Text { + text: S.NotifService.dnd ? "\uDB82\uDE93" : "\uDB80\uDC9C" + color: S.NotifService.dnd ? S.Theme.base09 : S.Theme.base04 + font.pixelSize: S.Theme.fontSize + font.family: S.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + + HoverHandler { + cursorShape: Qt.PointingHandCursor + } + TapHandler { + onTapped: S.NotifService.toggleDnd() + } + } + + // Clear all + Text { + text: "\uF1F8" + color: _clearHover.hovered ? S.Theme.base08 : S.Theme.base04 + font.pixelSize: S.Theme.fontSize + font.family: S.Theme.iconFontFamily + anchors.verticalCenter: parent.verticalCenter + visible: S.NotifService.count > 0 + + HoverHandler { + id: _clearHover + cursorShape: Qt.PointingHandCursor + } + TapHandler { + onTapped: _notifApplet.cascadeDismiss() + } + } + } + } + + C.NotifApplet { + id: _notifApplet + width: hoverPanel.contentWidth + contentWidth: hoverPanel.contentWidth accentColor: root.accentColor - screen: root.bar.screen - anchorX: root.mapToGlobal(root.width / 2, 0).x - (QsWindow.window?.screen?.x ?? 0) - onDismissed: centerLoader.active = false } } } diff --git a/shell/modules/qmldir b/shell/modules/qmldir index b4a55ee..3a8eee3 100644 --- a/shell/modules/qmldir +++ b/shell/modules/qmldir @@ -20,7 +20,6 @@ MemoryModule 1.0 MemoryModule.qml MprisModule 1.0 MprisModule.qml NetworkModule 1.0 NetworkModule.qml NotifCard 1.0 NotifCard.qml -NotifCenter 1.0 NotifCenter.qml NotifPopup 1.0 NotifPopup.qml NotificationsModule 1.0 NotificationsModule.qml OverviewBackdrop 1.0 OverviewBackdrop.qml