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.
This commit is contained in:
Marco Allegretti 2026-04-17 07:10:23 +02:00
parent c2b864d8b4
commit 86283b526d
2 changed files with 50 additions and 13 deletions

View file

@ -27,6 +27,15 @@ Item {
// 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager' // 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager'
property int popupWidth property int popupWidth
property real openOffset 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 // calculate the position needed to at when the expanded drawer is active
readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0 readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0
@ -244,7 +253,7 @@ Item {
waiting = false; waiting = false;
inPopupDrawer = true; inPopupDrawer = true;
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset; notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
@ -255,7 +264,7 @@ Item {
function openPopup() { function openPopup() {
if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") {
notificationItem.offset = openOffset; notificationItem.offset = effectiveOpenOffset;
notificationItem.scale = 0.75; notificationItem.scale = 0.75;
notificationItem.popupOpacity = 0.0; notificationItem.popupOpacity = 0.0;
} }
@ -343,7 +352,7 @@ Item {
onDismissRequested: closePopup(popupIndex) onDismissRequested: closePopup(popupIndex)
property real offset: closedOffset property real offset: notificationPopup.effectiveClosedOffset
property real scale: 1.0 property real scale: 1.0
property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer property real popupOpacity: 1.0 // controls the opacity of the notification popup when outside the popup drawer
property real drawerScale: { property real drawerScale: {
@ -365,6 +374,9 @@ Item {
return Kirigami.Units.gridUnit * 0.5 * indexClamped; return Kirigami.Units.gridUnit * 0.5 * indexClamped;
} }
property real drawerOpacity: { property real drawerOpacity: {
if (notificationPopup.isConvergence && notificationPopup.inPopupDrawer) {
return 0;
}
let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex;
if (index > 2 && !notificationPopup.popupDrawerOpened) { if (index > 2 && !notificationPopup.popupDrawerOpened) {
return 0; // make this popup invisible if it is below 3 other popups return 0; // make this popup invisible if it is below 3 other popups
@ -399,7 +411,7 @@ Item {
State { State {
name: "open" name: "open"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
@ -411,7 +423,7 @@ Item {
State { State {
name: "closeWithMove" name: "closeWithMove"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.closedOffset target: notificationItem; offset: notificationPopup.effectiveClosedOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1.0 target: notificationItem; scale: 1.0
@ -423,7 +435,7 @@ Item {
State { State {
name: "closeWithScale" name: "closeWithScale"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 0.75 target: notificationItem; scale: 0.75
@ -435,7 +447,7 @@ Item {
State { State {
name: "inDrawerClosed" name: "inDrawerClosed"
PropertyChanges { PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset target: notificationItem; offset: notificationPopup.effectiveOpenOffset
} }
PropertyChanges { PropertyChanges {
target: notificationItem; scale: 1 target: notificationItem; scale: 1
@ -556,15 +568,29 @@ Item {
startActive = false; startActive = false;
} }
lastOffset = notificationPopup.dragOffset; 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: { onActiveChanged: {
startActive = active; startActive = active;
notificationPopup.preventDismissTimeout = true; notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) { if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) { let dominated = false;
// this code is called when the notification is swiped or dragged to the top. 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.closedWithSwipe = true;
notificationPopup.closePopup(popupIndex); notificationPopup.closePopup(popupIndex);
return; return;
@ -589,7 +615,7 @@ Item {
height: Kirigami.Units.gridUnit * 2 height: Kirigami.Units.gridUnit * 2
enabled: !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1) enabled: !notificationPopup.isConvergence && !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1)
onReleased: { onReleased: {
notificationPopup.openPopupDrawer(); notificationPopup.openPopupDrawer();

View file

@ -11,6 +11,7 @@ import QtQuick.Window
import org.kde.kirigami as Kirigami import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.private.mobileshell.state as MobileShellState 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 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 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 real openOffset: Kirigami.Units.gridUnit + Kirigami.Units.smallSpacing * 3
readonly property int longestLength: Math.max(Screen.width, Screen.height) readonly property int longestLength: Math.max(Screen.width, Screen.height)
readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled
property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
LayerShell.Window.scope: "notification" LayerShell.Window.scope: "notification"
@ -97,7 +99,13 @@ Window {
console.warn("popupNotification: could not retrieve current popup height - falling back to a default value") 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 { delegate: NotificationPopup {
id: popup id: popup
anchors.horizontalCenter: parent.horizontalCenter x: notificationPopupManager.isConvergence
? (parent.width - width - Kirigami.Units.gridUnit * 2)
: (parent.width - width) / 2
z: notifications.count - index z: notifications.count - index
isConvergence: notificationPopupManager.isConvergence
popupWidth: notificationPopupManager.popupWidth popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset openOffset: notificationPopupManager.openOffset