From c68c9be676f2fde659e62037e6f8e9e222116f63 Mon Sep 17 00:00:00 2001 From: sima Date: Thu, 12 Feb 2026 17:15:23 +0800 Subject: [PATCH] Use vertical swipe dismiss for top/bottom centered notifications --- Modules/Notification/Notification.qml | 65 +++++++++++++++++++++++---- Modules/Toast/Toast.qml | 56 ++++++++++++++++++++--- 2 files changed, 107 insertions(+), 14 deletions(-) diff --git a/Modules/Notification/Notification.qml b/Modules/Notification/Notification.qml index 7c35b10af..68459b608 100644 --- a/Modules/Notification/Notification.qml +++ b/Modules/Notification/Notification.qml @@ -212,17 +212,21 @@ Variants { property real opacityValue: 0.0 property real slideOffset: 0 property real swipeOffset: 0 + property real swipeOffsetY: 0 property real pressGlobalX: 0 + property real pressGlobalY: 0 property bool isSwiping: false property bool suppressClick: false + readonly property bool useVerticalSwipe: notifWindow.location === "bottom" || notifWindow.location === "top" readonly property real swipeStartThreshold: Math.round(18 * Style.uiScaleRatio) readonly property real swipeDismissThreshold: Math.max(110, cardBackground.width * 0.32) + readonly property real verticalSwipeDismissThreshold: Math.max(70, cardBackground.height * 0.35) scale: scaleValue opacity: opacityValue transform: Translate { x: card.swipeOffset - y: card.slideOffset + y: card.slideOffset + card.swipeOffsetY } readonly property real slideInOffset: notifWindow.isTop ? -slideDistance : slideDistance @@ -236,6 +240,14 @@ Variants { return deltaX; } + function clampVerticalSwipeDelta(deltaY) { + if (notifWindow.isBottom) + return Math.max(0, deltaY); + if (notifWindow.isTop) + return Math.min(0, deltaY); + return deltaY; + } + // Background with border Rectangle { id: cardBackground @@ -326,6 +338,7 @@ Variants { if (mouse.button === Qt.LeftButton) { const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y); card.pressGlobalX = globalPoint.x; + card.pressGlobalY = globalPoint.y; card.isSwiping = false; card.suppressClick = false; } @@ -334,13 +347,28 @@ Variants { if (!(mouse.buttons & Qt.LeftButton) || card.isRemoving) return; const globalPoint = cardDragArea.mapToGlobal(mouse.x, mouse.y); - const deltaX = card.clampSwipeDelta(globalPoint.x - card.pressGlobalX); + const rawDeltaX = globalPoint.x - card.pressGlobalX; + const rawDeltaY = globalPoint.y - card.pressGlobalY; + const deltaX = card.clampSwipeDelta(rawDeltaX); + const deltaY = card.clampVerticalSwipeDelta(rawDeltaY); if (!card.isSwiping) { - if (Math.abs(deltaX) < card.swipeStartThreshold) - return; - card.isSwiping = true; + if (card.useVerticalSwipe) { + if (Math.abs(deltaY) < card.swipeStartThreshold) + return; + card.isSwiping = true; + } else { + if (Math.abs(deltaX) < card.swipeStartThreshold) + return; + card.isSwiping = true; + } + } + if (card.useVerticalSwipe) { + card.swipeOffset = 0; + card.swipeOffsetY = deltaY; + } else { + card.swipeOffset = deltaX; + card.swipeOffsetY = 0; } - card.swipeOffset = deltaX; } onReleased: mouse => { if (mouse.button === Qt.RightButton) { @@ -352,10 +380,13 @@ Variants { return; if (card.isSwiping) { - if (Math.abs(card.swipeOffset) >= card.swipeDismissThreshold) { + const dismissDistance = card.useVerticalSwipe ? Math.abs(card.swipeOffsetY) : Math.abs(card.swipeOffset); + const threshold = card.useVerticalSwipe ? card.verticalSwipeDismissThreshold : card.swipeDismissThreshold; + if (dismissDistance >= threshold) { card.dismissBySwipe(); } else { card.swipeOffset = 0; + card.swipeOffsetY = 0; } card.suppressClick = true; card.isSwiping = false; @@ -378,6 +409,7 @@ Variants { onCanceled: { card.isSwiping = false; card.swipeOffset = 0; + card.swipeOffsetY = 0; } } // Animation setup @@ -389,6 +421,7 @@ Variants { hoverCount = 0; isSwiping = false; swipeOffset = 0; + swipeOffsetY = 0; if (Settings.data.general.animationDisabled) { slideOffset = 0; scaleValue = 1.0; @@ -428,6 +461,7 @@ Variants { isRemoving = true; isSwiping = false; swipeOffset = 0; + swipeOffsetY = 0; if (!Settings.data.general.animationDisabled) { slideOffset = slideOutOffset; scaleValue = 0.8; @@ -443,11 +477,18 @@ Variants { isRemoving = true; isSwiping = false; if (!Settings.data.general.animationDisabled) { - swipeOffset = swipeOffset >= 0 ? cardBackground.width + Style.marginXL : -cardBackground.width - Style.marginXL; + if (useVerticalSwipe) { + swipeOffset = 0; + swipeOffsetY = swipeOffsetY >= 0 ? cardBackground.height + Style.marginXL : -cardBackground.height - Style.marginXL; + } else { + swipeOffset = swipeOffset >= 0 ? cardBackground.width + Style.marginXL : -cardBackground.width - Style.marginXL; + swipeOffsetY = 0; + } scaleValue = 0.8; opacityValue = 0.0; } else { swipeOffset = 0; + swipeOffsetY = 0; } } @@ -516,6 +557,14 @@ Variants { } } + Behavior on swipeOffsetY { + enabled: !Settings.data.general.animationDisabled && !card.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + // Content ColumnLayout { id: notificationContent diff --git a/Modules/Toast/Toast.qml b/Modules/Toast/Toast.qml index 375e9fc7d..9c4e12ad8 100644 --- a/Modules/Toast/Toast.qml +++ b/Modules/Toast/Toast.qml @@ -31,16 +31,21 @@ Item { property real progress: 1.0 property int hoverCount: 0 property real swipeOffset: 0 + property real swipeOffsetY: 0 property real pressGlobalX: 0 + property real pressGlobalY: 0 property bool isSwiping: false readonly property string location: Settings.data.notifications?.location || "top_right" readonly property bool isLeft: location.endsWith("_left") readonly property bool isRight: location.endsWith("_right") + readonly property bool useVerticalSwipe: location === "bottom" || location === "top" readonly property real swipeStartThreshold: Math.round(18 * Style.uiScaleRatio) readonly property real swipeDismissThreshold: Math.max(110, background.width * 0.32) + readonly property real verticalSwipeDismissThreshold: Math.max(70, background.height * 0.35) transform: Translate { x: root.swipeOffset + y: root.swipeOffsetY } function clampSwipeDelta(deltaX) { @@ -51,6 +56,14 @@ Item { return deltaX; } + function clampVerticalSwipeDelta(deltaY) { + if (location === "bottom") + return Math.max(0, deltaY); + if (location === "top") + return Math.min(0, deltaY); + return deltaY; + } + onHoverCountChanged: { if (hoverCount > 0) { resumeTimer.stop(); @@ -176,6 +189,14 @@ Item { } } + Behavior on swipeOffsetY { + enabled: !root.isSwiping + NumberAnimation { + duration: Style.animationFast + easing.type: Easing.OutCubic + } + } + Timer { id: hideAnimation interval: Style.animationFast @@ -206,29 +227,48 @@ Item { onPressed: mouse => { const globalPoint = toastDragArea.mapToGlobal(mouse.x, mouse.y); root.pressGlobalX = globalPoint.x; + root.pressGlobalY = globalPoint.y; root.isSwiping = false; } onPositionChanged: mouse => { if (!(mouse.buttons & Qt.LeftButton)) return; const globalPoint = toastDragArea.mapToGlobal(mouse.x, mouse.y); - const deltaX = root.clampSwipeDelta(globalPoint.x - root.pressGlobalX); + const rawDeltaX = globalPoint.x - root.pressGlobalX; + const rawDeltaY = globalPoint.y - root.pressGlobalY; + const deltaX = root.clampSwipeDelta(rawDeltaX); + const deltaY = root.clampVerticalSwipeDelta(rawDeltaY); if (!root.isSwiping) { - if (Math.abs(deltaX) < root.swipeStartThreshold) - return; - root.isSwiping = true; + if (root.useVerticalSwipe) { + if (Math.abs(deltaY) < root.swipeStartThreshold) + return; + root.isSwiping = true; + } else { + if (Math.abs(deltaX) < root.swipeStartThreshold) + return; + root.isSwiping = true; + } + } + if (root.useVerticalSwipe) { + root.swipeOffset = 0; + root.swipeOffsetY = deltaY; + } else { + root.swipeOffset = deltaX; + root.swipeOffsetY = 0; } - root.swipeOffset = deltaX; } onReleased: mouse => { if (mouse.button !== Qt.LeftButton) return; if (root.isSwiping) { root.isSwiping = false; - if (Math.abs(root.swipeOffset) >= root.swipeDismissThreshold) { + const dismissDistance = root.useVerticalSwipe ? Math.abs(root.swipeOffsetY) : Math.abs(root.swipeOffset); + const threshold = root.useVerticalSwipe ? root.verticalSwipeDismissThreshold : root.swipeDismissThreshold; + if (dismissDistance >= threshold) { root.hide(); } else { root.swipeOffset = 0; + root.swipeOffsetY = 0; } return; } @@ -237,6 +277,7 @@ Item { onCanceled: { root.isSwiping = false; root.swipeOffset = 0; + root.swipeOffsetY = 0; } cursorShape: Qt.PointingHandCursor } @@ -345,6 +386,7 @@ Item { hoverCount = 0; isSwiping = false; swipeOffset = 0; + swipeOffsetY = 0; // Configure and start animation progressAnimation.duration = duration; @@ -357,6 +399,7 @@ Item { progressAnimation.stop(); isSwiping = false; swipeOffset = 0; + swipeOffsetY = 0; opacity = 0; scale = initialScale; hideAnimation.restart(); @@ -367,6 +410,7 @@ Item { progressAnimation.stop(); isSwiping = false; swipeOffset = 0; + swipeOffsetY = 0; opacity = 0; scale = initialScale; root.hidden();