diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml index 3ace8c55..83230ee3 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopup.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopup.qml @@ -16,6 +16,7 @@ PanelWindow { required property var notificationData required property string notificationId readonly property bool hasValidData: notificationData && notificationData.notification + readonly property alias hovered: cardHoverHandler.hovered property int screenY: 0 property bool exiting: false property bool _isDestroying: false @@ -48,7 +49,7 @@ PanelWindow { signal entered signal exitStarted signal exitFinished - signal popupHeightChanged() + signal popupHeightChanged function startExit() { if (exiting || _isDestroying) { @@ -428,14 +429,14 @@ PanelWindow { anchors.left: parent.left anchors.top: parent.top anchors.topMargin: { - if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) { - const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2; - return Math.max(0, headerSummary / 2 - popupIconSize / 2); + if (SettingsData.notificationPopupPrivacyMode && !descriptionExpanded) { + const headerSummary = Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2; + return Math.max(0, headerSummary / 2 - popupIconSize / 2); + } + if (descriptionExpanded) + return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2); + return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2); } - if (descriptionExpanded) - return Math.max(0, Theme.fontSizeSmall * 1.2 + (Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) / 2 - popupIconSize / 2); - return Math.max(0, Theme.fontSizeSmall * 1.2 + (textContainer.height - Theme.fontSizeSmall * 1.2) / 2 - popupIconSize / 2); - } imageSource: { if (!notificationData) diff --git a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml index c5012fa8..df0dc3a1 100644 --- a/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml +++ b/quickshell/Modules/Notifications/Popup/NotificationPopupManager.qml @@ -15,20 +15,14 @@ QtObject { readonly property real popupSpacing: compactMode ? 0 : Theme.spacingXS readonly property real collapsedContentHeight: Math.max(popupIconSize, Theme.fontSizeSmall * 1.2 + Theme.fontSizeMedium * 1.2 + Theme.fontSizeSmall * 1.2 * (compactMode ? 1 : 2)) readonly property int baseNotificationHeight: cardPadding * 2 + collapsedContentHeight + actionButtonHeight + contentSpacing + popupSpacing - property int maxTargetNotifications: 4 - property var popupWindows: [] // strong refs to windows (live until exitFinished) + property var popupWindows: [] property var destroyingWindows: new Set() property var pendingDestroys: [] property int destroyDelayMs: 100 - property var pendingCreates: [] - property int createDelayMs: 50 - property bool createBusy: false property Component popupComponent popupComponent: Component { NotificationPopup { - onEntered: manager._onPopupEntered(this) - onExitStarted: manager._onPopupExitStarted(this) onExitFinished: manager._onPopupExitFinished(this) onPopupHeightChanged: manager._onPopupHeightChanged(this) } @@ -74,36 +68,6 @@ QtObject { destroyTimer.restart(); } - property Timer createTimer: Timer { - interval: createDelayMs - running: false - repeat: false - onTriggered: manager._processCreateQueue() - } - - function _processCreateQueue() { - createBusy = false; - if (pendingCreates.length === 0) - return; - const wrapper = pendingCreates.shift(); - if (wrapper) - _doInsertNewestAtTop(wrapper); - if (pendingCreates.length > 0) { - createBusy = true; - createTimer.restart(); - } - } - - function _scheduleCreate(wrapper) { - if (!wrapper) - return; - pendingCreates.push(wrapper); - if (!createBusy) { - createBusy = true; - createTimer.restart(); - } - } - sweeper: Timer { interval: 500 running: false @@ -129,11 +93,10 @@ QtObject { } if (toRemove.length) { popupWindows = popupWindows.filter(p => toRemove.indexOf(p) === -1); - _repositionAllActivePopups(); + _repositionAll(); } - if (popupWindows.length === 0) { + if (popupWindows.length === 0) sweeper.stop(); - } } } @@ -145,98 +108,29 @@ QtObject { return p && p.status !== Component.Null && !p._isDestroying && p.hasValidData; } - function _canMakeRoomFor(wrapper) { - const activeWindows = _active(); - if (activeWindows.length < maxTargetNotifications) { - return true; - } - if (!wrapper || !wrapper.notification) { - return false; - } - const incomingUrgency = wrapper.urgency || 0; - for (const p of activeWindows) { - if (!p.notificationData || !p.notificationData.notification) { - continue; - } - const existingUrgency = p.notificationData.urgency || 0; - if (existingUrgency < incomingUrgency) { - return true; - } - if (existingUrgency === incomingUrgency) { - const timer = p.notificationData.timer; - if (timer && !timer.running) { - return true; - } - } - } - return false; - } - - function _makeRoomForNew(wrapper) { - const activeWindows = _active(); - if (activeWindows.length < maxTargetNotifications) { - return; - } - const toRemove = _selectPopupToRemove(activeWindows, wrapper); - if (toRemove && !toRemove.exiting) { - toRemove.notificationData.removedByLimit = true; - toRemove.notificationData.popup = false; - if (toRemove.notificationData.timer) { - toRemove.notificationData.timer.stop(); - } - } - } - - function _selectPopupToRemove(activeWindows, incomingWrapper) { - const sortedWindows = activeWindows.slice().sort((a, b) => { - const aUrgency = (a.notificationData) ? a.notificationData.urgency || 0 : 0; - const bUrgency = (b.notificationData) ? b.notificationData.urgency || 0 : 0; - if (aUrgency !== bUrgency) { - return aUrgency - bUrgency; - } - const aTimer = a.notificationData && a.notificationData.timer; - const bTimer = b.notificationData && b.notificationData.timer; - const aRunning = aTimer && aTimer.running; - const bRunning = bTimer && bTimer.running; - if (aRunning !== bRunning) { - return aRunning ? 1 : -1; - } - return b.screenY - a.screenY; - }); - return sortedWindows[0]; - } - function _sync(newWrappers) { - for (const w of newWrappers) { - if (w && !_hasWindowFor(w)) { - insertNewestAtTop(w); - } - } for (const p of popupWindows.slice()) { - if (!_isValidWindow(p)) { + if (!_isValidWindow(p) || p.exiting) continue; - } - if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1 && !p.exiting) { + if (p.notificationData && newWrappers.indexOf(p.notificationData) === -1) { p.notificationData.removedByLimit = true; p.notificationData.popup = false; } } - } - - function insertNewestAtTop(wrapper) { - if (!wrapper) - return; - if (createBusy || pendingCreates.length > 0) { - _scheduleCreate(wrapper); - return; + for (const w of newWrappers) { + if (w && !_hasWindowFor(w)) + _insertAtTop(w); } - _doInsertNewestAtTop(wrapper); } - function _doInsertNewestAtTop(wrapper) { + function _popupHeight(p) { + return (p.alignedHeight || p.implicitHeight || (baseNotificationHeight - popupSpacing)) + popupSpacing; + } + + function _insertAtTop(wrapper) { if (!wrapper) return; - const notificationId = wrapper && wrapper.notification ? wrapper.notification.id : ""; + const notificationId = wrapper?.notification ? wrapper.notification.id : ""; const win = popupComponent.createObject(null, { "notificationData": wrapper, "notificationId": notificationId, @@ -250,85 +144,69 @@ QtObject { return; } popupWindows.unshift(win); - - _repositionAllActivePopups(); - - createBusy = true; - createTimer.restart(); + _repositionAll(); if (!sweeper.running) sweeper.start(); } - function _active() { - return popupWindows.filter(p => _isValidWindow(p) && p.notificationData && p.notificationData.popup && !p.exiting); - } + function _repositionAll() { + const active = popupWindows.filter(p => _isValidWindow(p) && p.notificationData?.popup && !p.exiting); - function _bottom() { - let b = null; - let maxY = -1; - for (const p of _active()) { - if (p.screenY > maxY) { - maxY = p.screenY; - b = p; - } + const pinnedSlots = []; + for (const p of active) { + if (!p.hovered) + continue; + pinnedSlots.push({ + y: p.screenY, + end: p.screenY + _popupHeight(p) + }); } - return b; - } + pinnedSlots.sort((a, b) => a.y - b.y); - function _onPopupEntered(p) { + let currentY = topMargin; + for (const win of active) { + if (win.hovered) + continue; + for (const slot of pinnedSlots) { + if (currentY >= slot.y - 1 && currentY < slot.end) + currentY = slot.end; + } + win.screenY = currentY; + currentY += _popupHeight(win); + } } function _onPopupHeightChanged(p) { if (!p || p.exiting || p._isDestroying) return; - _repositionAllActivePopups(); - } - - function _repositionAllActivePopups() { - const activeWindows = _active().sort((a, b) => a.screenY - b.screenY); - let currentY = topMargin; - for (const win of activeWindows) { - win.screenY = currentY; - const popupHeight = win.alignedHeight || win.implicitHeight; - currentY += popupHeight + popupSpacing; - } - } - - function _onPopupExitStarted(p) { - if (!p) + if (popupWindows.indexOf(p) === -1) return; - _repositionAllActivePopups(); + _repositionAll(); } function _onPopupExitFinished(p) { - if (!p) { + if (!p) return; - } const windowId = p.toString(); - if (destroyingWindows.has(windowId)) { + if (destroyingWindows.has(windowId)) return; - } destroyingWindows.add(windowId); const i = popupWindows.indexOf(p); if (i !== -1) { popupWindows.splice(i, 1); popupWindows = popupWindows.slice(); } - if (NotificationService.releaseWrapper && p.notificationData) { + if (NotificationService.releaseWrapper && p.notificationData) NotificationService.releaseWrapper(p.notificationData); - } _scheduleDestroy(p); Qt.callLater(() => destroyingWindows.delete(windowId)); - _repositionAllActivePopups(); + _repositionAll(); } function cleanupAllWindows() { sweeper.stop(); destroyTimer.stop(); - createTimer.stop(); pendingDestroys = []; - pendingCreates = []; - createBusy = false; for (const p of popupWindows.slice()) { if (p) { try { diff --git a/quickshell/Services/NotificationService.qml b/quickshell/Services/NotificationService.qml index cdd773a7..7655b682 100644 --- a/quickshell/Services/NotificationService.qml +++ b/quickshell/Services/NotificationService.qml @@ -22,7 +22,7 @@ Singleton { property list notificationQueue: [] property list visibleNotifications: [] - property int maxVisibleNotifications: 3 + property int maxVisibleNotifications: 4 property bool addGateBusy: false property int enterAnimMs: 400 property int seqCounter: 0 @@ -253,7 +253,9 @@ Singleton { return timeStr; try { const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US"; - const weekday = date.toLocaleDateString(localeName, { weekday: "long" }); + const weekday = date.toLocaleDateString(localeName, { + weekday: "long" + }); return weekday + ", " + timeStr; } catch (e) { return timeStr; @@ -488,7 +490,7 @@ Singleton { Timer { id: addGate - interval: enterAnimMs + 50 + interval: 80 running: false repeat: false onTriggered: { @@ -694,7 +696,9 @@ Singleton { try { const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US"; - const weekday = time.toLocaleDateString(localeName, { weekday: "long" }); + const weekday = time.toLocaleDateString(localeName, { + weekday: "long" + }); return `${weekday}, ${formatTime(time)}`; } catch (e) { return formatTime(time); @@ -843,39 +847,54 @@ Singleton { } } - function processQueue() { - if (addGateBusy) { - return; - } - if (popupsDisabled) { - return; - } - if (SessionData.doNotDisturb) { - return; - } - if (notificationQueue.length === 0) { - return; - } + property bool _processingQueue: false - const activePopupCount = visibleNotifications.filter(n => n && n.popup).length; - if (activePopupCount >= maxVisibleNotifications) { + function processQueue() { + if (addGateBusy || _processingQueue) return; - } + if (popupsDisabled) + return; + if (SessionData.doNotDisturb) + return; + if (notificationQueue.length === 0) + return; + + _processingQueue = true; const next = notificationQueue.shift(); - if (!next) + if (!next) { + _processingQueue = false; return; + } next.seq = ++seqCounter; - visibleNotifications = [...visibleNotifications, next]; + + const activePopups = visibleNotifications.filter(n => n && n.popup); + let evicted = null; + if (activePopups.length >= maxVisibleNotifications) { + const unhovered = activePopups.filter(n => n.timer?.running); + const pool = unhovered.length > 0 ? unhovered : activePopups; + evicted = pool.reduce((min, n) => (n.seq < min.seq) ? n : min, pool[0]); + if (evicted) + evicted.removedByLimit = true; + } + + if (evicted) { + visibleNotifications = [...visibleNotifications.filter(n => n !== evicted), next]; + } else { + visibleNotifications = [...visibleNotifications, next]; + } + + if (evicted) + evicted.popup = false; next.popup = true; - if (next.timer.interval > 0) { + if (next.timer.interval > 0) next.timer.start(); - } addGateBusy = true; addGate.restart(); + _processingQueue = false; } function removeFromVisibleNotifications(wrapper) {