From 86283b526d79222b702f72a50fb1c6dc0ae147d6 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Fri, 17 Apr 2026 07:10:23 +0200 Subject: [PATCH] Move notification popup to bottom-right in convergence The mobile popup system always anchored to top-center which looked out of place on a desktop screen. In convergence mode, position popups at the bottom-right corner and slide them in from below. Swipe-to-dismiss flips to match (drag down to dismiss instead of up). Stacked card previews and the popup drawer tap area are hidden since notifications are already accessible in the action drawer. --- .../notifications/NotificationPopup.qml | 48 ++++++++++++++----- .../NotificationPopupManager.qml | 15 +++++- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/components/mobileshell/qml/popups/notifications/NotificationPopup.qml b/components/mobileshell/qml/popups/notifications/NotificationPopup.qml index 6b8e28a6..564de2b1 100644 --- a/components/mobileshell/qml/popups/notifications/NotificationPopup.qml +++ b/components/mobileshell/qml/popups/notifications/NotificationPopup.qml @@ -27,6 +27,15 @@ Item { // 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager' property int popupWidth property real openOffset + property bool isConvergence: false + + // In convergence the popup enters from the bottom-right corner + readonly property real effectiveOpenOffset: isConvergence + ? (Screen.height - openOffset - popupHeight) + : openOffset + readonly property real effectiveClosedOffset: isConvergence + ? (Screen.height + Kirigami.Units.smallSpacing) + : closedOffset // calculate the position needed to at when the expanded drawer is active readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0 @@ -244,7 +253,7 @@ Item { waiting = false; inPopupDrawer = true; if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { - notificationItem.offset = openOffset; + notificationItem.offset = effectiveOpenOffset; notificationItem.scale = 0.75; notificationItem.popupOpacity = 0.0; } @@ -255,7 +264,7 @@ Item { function openPopup() { if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { - notificationItem.offset = openOffset; + notificationItem.offset = effectiveOpenOffset; notificationItem.scale = 0.75; notificationItem.popupOpacity = 0.0; } @@ -343,7 +352,7 @@ Item { onDismissRequested: closePopup(popupIndex) - property real offset: closedOffset + property real offset: notificationPopup.effectiveClosedOffset property real scale: 1.0 property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer property real drawerScale: { @@ -365,6 +374,9 @@ Item { return Kirigami.Units.gridUnit * 0.5 * indexClamped; } property real drawerOpacity: { + if (notificationPopup.isConvergence && notificationPopup.inPopupDrawer) { + return 0; + } let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; if (index > 2 && !notificationPopup.popupDrawerOpened) { return 0; // make this popup invisible if it is below 3 other popups @@ -399,7 +411,7 @@ Item { State { name: "open" PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset + target: notificationItem; offset: notificationPopup.effectiveOpenOffset } PropertyChanges { target: notificationItem; scale: 1.0 @@ -411,7 +423,7 @@ Item { State { name: "closeWithMove" PropertyChanges { - target: notificationItem; offset: notificationPopup.closedOffset + target: notificationItem; offset: notificationPopup.effectiveClosedOffset } PropertyChanges { target: notificationItem; scale: 1.0 @@ -423,7 +435,7 @@ Item { State { name: "closeWithScale" PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset + target: notificationItem; offset: notificationPopup.effectiveOpenOffset } PropertyChanges { target: notificationItem; scale: 0.75 @@ -435,7 +447,7 @@ Item { State { name: "inDrawerClosed" PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset + target: notificationItem; offset: notificationPopup.effectiveOpenOffset } PropertyChanges { target: notificationItem; scale: 1 @@ -556,15 +568,29 @@ Item { startActive = false; } lastOffset = notificationPopup.dragOffset; - notificationPopup.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 0); + let rawOffset = startDragOffset + (translation.y - startPosition); + if (notificationPopup.isConvergence) { + notificationPopup.dragOffset = -calculateResistance(-rawOffset, 0); + } else { + notificationPopup.dragOffset = calculateResistance(rawOffset, 0); + } } onActiveChanged: { startActive = active; notificationPopup.preventDismissTimeout = true; if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) { - if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) { - // this code is called when the notification is swiped or dragged to the top. + let dominated = false; + if (notificationPopup.isConvergence) { + // convergence: dismiss on swipe down + dominated = (notificationPopup.dragOffset - lastOffset > 1.0 && notificationPopup.dragOffset > 0) + || (notificationPopup.dragOffset > (notificationPopup.effectiveClosedOffset - notificationPopup.effectiveOpenOffset) / 4); + } else { + // mobile: dismiss on swipe up + dominated = (lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) + || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset); + } + if (dominated) { notificationPopup.closedWithSwipe = true; notificationPopup.closePopup(popupIndex); return; @@ -589,7 +615,7 @@ Item { height: Kirigami.Units.gridUnit * 2 - enabled: !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1) + enabled: !notificationPopup.isConvergence && !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1) onReleased: { notificationPopup.openPopupDrawer(); diff --git a/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml b/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml index f0fc8ca1..75d0cd6c 100644 --- a/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml +++ b/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml @@ -11,6 +11,7 @@ import QtQuick.Window import org.kde.kirigami as Kirigami import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell.state as MobileShellState +import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.layershell 1.0 as LayerShell @@ -31,6 +32,7 @@ Window { readonly property int popupWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2) readonly property real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3 readonly property int longestLength: Math.max(Screen.width, Screen.height) + readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.scope: "notification" @@ -97,7 +99,13 @@ Window { console.warn("popupNotification: could not retrieve current popup height - falling back to a default value") } - ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1))); + if (isConvergence) { + let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit * 4; + let regionY = Screen.height - openOffset - popupHeight - Kirigami.Units.gridUnit; + ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit * 2, popupHeight + Kirigami.Units.gridUnit * 2)); + } else { + ShellUtil.setInputRegion(notificationPopupManager, Qt.rect((notificationPopupManager.width - notificationPopupManager.popupWidth - Kirigami.Units.gridUnit) / 2, openOffset - Kirigami.Units.gridUnit / 2, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit * ((notifications.count - notifications.currentPopupIndex > 1) ? 4 : 1))); + } } } @@ -194,9 +202,12 @@ Window { delegate: NotificationPopup { id: popup - anchors.horizontalCenter: parent.horizontalCenter + x: notificationPopupManager.isConvergence + ? (parent.width - width - Kirigami.Units.gridUnit * 2) + : (parent.width - width) / 2 z: notifications.count - index + isConvergence: notificationPopupManager.isConvergence popupWidth: notificationPopupManager.popupWidth openOffset: notificationPopupManager.openOffset