[plasma/plasma-mobile] components/mobileshell/qml: popups/notifications: Notification Popup Code Refactoring
Micah Stanley <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 53da8e8e03f67a89ca5bed8ba3efce6954772713 by Micah Stanley. Committed on 19/07/2026 at 16:59. Pushed by micahstanley into branch 'master'. popups/notifications: Notification Popup Code Refactoring Refactor the notification popup code around a `ListView` instead on manually managing the positions of the popups ourselves, making the codebase for it significantly smaller and easier to maintain. M +228 -473 components/mobileshell/qml/popups/notifications/NotificationPopup.qml M +226 -185 components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml M +4 -6 components/mobileshell/qml/popups/notifications/NotificationPopupProvider.qml M +12 -3 components/mobileshell/qml/widgets/notifications/NotificationCard.qml M +4 -0 components/mobileshell/qml/widgets/notifications/NotificationItem.qml M +4 -0 components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml https://invent.kde.org/plasma/plasma-mobile/-/commit/53da8e8e03f67a89ca5bed8ba3efce6954772713 diff --git a/components/mobileshell/qml/popups/notifications/NotificationPopup.qml b/components/mobileshell/qml/popups/notifications/NotificationPopup.qml index 6b8e28a65..0a2e1d075 100644 --- a/components/mobileshell/qml/popups/notifications/NotificationPopup.qml +++ b/components/mobileshell/qml/popups/notifications/NotificationPopup.qml @@ -1,7 +1,7 @@ /* - * SPDX-FileCopyrightText: 2024-2025 Micah Stanley <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later + SPDX-FileCopyrightText: 2024-2025 Micah Stanley <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick @@ -19,290 +19,224 @@ import org.kde.notificationmanager as NotificationManager import org.kde.plasma.clock Item { - id: notificationPopup - - readonly property int popupHeight: notificationItem.implicitHeight - readonly property bool isClosing: notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale" - readonly property real closedOffset: -(popupHeight + Kirigami.Units.smallSpacing) - // 'popupWidth' and 'openOffset' is set by the 'notificationPopupManager' - property int popupWidth - property real openOffset - - // calculate the position needed to at when the expanded drawer is active - readonly property real fullOpenOffset: popupDrawerOpened ? aboveNotificationFullOffset + aboveNotificationHeight + Kirigami.Units.largeSpacing : 0 - property real aboveNotificationFullOffset: 0 - property int aboveNotificationHeight: 0 - - // set to true when notification is swiped up by user - property bool closedWithSwipe: false - - // the drag offset on the current popup notification - used to position notification when stacked underneath - property real currentDragOffset: { - let current = popupNotifications.currentPopupIndex == notificationPopup.popupIndex; - return current || popupDrawerOpened ? 0 : Math.max(popupNotifications.currentDragOffset, 0) - } - - // due to it not looking great to have a notification sliding up while another one is sliding down - // we use a timer so that the current notification can know to use "closeWithScale" instead - property Timer queueTimer: Timer { - interval: Kirigami.Units.veryLongDuration - running: true - onTriggered: { - visible = true; - updateNotificationPopups(); - } - } + id: popupDelegate - // The timer for when the notification will dismiss - Timer { - id: hideTimer - interval: notificationPopup.effectiveTimeout - running: { - if (interval <= 0) { - return false; - } - if (notificationPopup.preventDismissTimeout) { - return false; - } - if (notificationPopup.inPopupDrawer) { - return false; - } - if (notificationPopup.popupDrawerOpened) { - return false; - } - return true; - } - onTriggered: notificationPopup.closePopup(popupIndex); - } + height: (notificationItem.cardHeight + Kirigami.Units.largeSpacing) * heightAnimationValue + opacity: Math.max(1 * popupDelegate.popupDrawerAnimationValue, popupDelegate.opacityAnimationValue) - // the value of how much time is left, normalized from 1 to 0 - property real remainingTimeProgress: 1 - NumberAnimation on remainingTimeProgress { - from: 1 - to: 0 - duration: hideTimer.interval - running: hideTimer.running - } + required property var model + required property int index - // set the height and width of the notification container with a extra space for starting a drag - width: popupWidth + Kirigami.Units.gridUnit - height: popupHeight + Kirigami.Units.gridUnit + property var popupManager + property var keyboardInteractivity + property bool popupDrawerOpened: false + property var notificationsModel + property int notificationsModelType + property var timeDataSource + property int notificationCount + property real topPopupOffset + property real popupDrawerAnimationValue: 0 + + property bool isCurrentPopup: index < 1 + property real currentPopupHeight + property real currentPopupDragOffset // the drag offset on the current popup notification - used to position notification when stacked underneath + property real offsetFromCurrentPopup + + property real heightAnimationValue: 1 + property real opacityAnimationValue: index < 3 ? 1 : 0 + property real verticalOffset: 0 + property real dragOffset: 0 + property bool closedWithSwipe: false // set to true when notification is swiped up by user + property string removalType: "slide" - visible: false + property bool preventDismissTimeout: false + property int timeout + property int dismissTimeout + property int defaultTimeout: 5000 + property bool isExpired: model.expired + property real remainingTimeProgress: 1 // the value of how much time is left, normalized from 1 to 0 - Kirigami.Theme.colorSet: Kirigami.Theme.View - Kirigami.Theme.inherit: false + readonly property real cardHeight: notificationItem.cardHeight + readonly property real closedOffset: -(notificationItem.height + Kirigami.Units.smallSpacing) + readonly property int effectiveTimeout: { + if (timeout === -1) return defaultTimeout; + if (dismissTimeout) return dismissTimeout; + return model.timeout; + } - signal expired() - signal dismissClicked() signal updateTouchArea() - signal setInputTransparent() signal openPopupDrawer() signal setKeyboardFocus() signal removeKeyboardFocus() + signal dismissClicked() + signal expired() - // animate the notifications entering and exiting the expanded drawer - property real fullOffsetAn: fullOpenOffset - Behavior on fullOffsetAn { - NumberAnimation { - duration: Kirigami.Units.veryLongDuration * 1.5 - easing.type: Easing.OutExpo + onHeightChanged: { + if (isCurrentPopup) { + popupDelegate.updateTouchArea(); } } - // when a notification is grouped behind the current Notification - // we need the y origin to be a the bottom - // however we need it at the center when "closeWithScale" is used - // animate this value so that the popup in some situations will not jump around - property real scaleOriginY: inPopupDrawer && !popupDrawerOpened ? popupNotifications.currentPopupHeight : Math.round(popupHeight / 2) - Behavior on scaleOriginY { - NumberAnimation { - duration: Kirigami.Units.veryLongDuration - easing.type: Easing.OutExpo + onIsCurrentPopupChanged: { + if (isCurrentPopup) { + popupDelegate.updateTouchArea(); } } - // the vertical drag offset for the notification popup - // we drag is released, animate back to 0 - property real dragOffset: 0 - NumberAnimation on dragOffset { - id: dragOffsetAn - running: false - to: 0 - duration: Kirigami.Units.veryLongDuration * 1.5 - easing.type: Easing.OutExpo - } - - // if the popup height ever changes, update the notification below with new height - // also update the allowed touch area for the main window - onPopupHeightChanged: { - let abovePopup = popupNotifications.objectAt(popupIndex + 1) - if (popupIndex + 1 < popupCount && abovePopup) { - abovePopup.aboveNotificationHeight = popupHeight; - } - if (popupNotifications.currentPopupIndex == notificationPopup.popupIndex && notificationItem.state == "open") { - notificationPopup.updateTouchArea(); + onIsExpiredChanged: { + if (isExpired) { + popupDelegate.closePopup(); } } - // if the offset position need in the expanded drawer changes, update the notification below with new offset - onFullOpenOffsetChanged: { - let abovePopup = popupNotifications.objectAt(popupIndex + 1) - if (popupIndex + 1 < popupCount && abovePopup) { - abovePopup.aboveNotificationFullOffset = fullOpenOffset; - } - } - // if the notification is being dragged and is the current one - // update 'currentDragOffset' so all notifications can easily access this value - onDragOffsetChanged: { - let abovePopup = popupNotifications.objectAt(popupIndex + 1) - if (popupNotifications.currentPopupIndex == notificationPopup.popupIndex) { - popupNotifications.currentDragOffset = dragOffset; + // attached property action animations + ListView.onAdd: { + if (popupDrawerOpened) { + notificationItem.opacity = 0; + fadeInAnimation.restart(); + } else { + popupDelegate.verticalOffset = -popupDelegate.height - popupDelegate.topPopupOffset; + slideInAnimation.restart(); } } - // if a new notification is added, update the above notification values need for the expanded drawer - onPopupCountChanged: { - let abovePopup = popupNotifications.objectAt(popupIndex + 1) - if (popupIndex + 1 < popupCount && abovePopup) { - abovePopup.aboveNotificationHeight = popupHeight; - abovePopup.aboveNotificationFullOffset = fullOpenOffset; + + ListView.onRemove: { + fadeInAnimation.stop(); + slideInAnimation.stop(); + notificationItem.opacity = 1; + verticalOffset = 0; + ListView.delayRemove = true; + popupManager.activeRemovalAnimations += 1; + + if (popupDrawerOpened || !isCurrentPopup) { + fadeOutAnimation.restart(); + } else { + slideOutAnimation.restart(); } } - // update the current popup index value if the index ever changes. - onPopupIndexChanged: { - if (!isClosing && !inPopupDrawer && !waiting) { - // if index goes below zero, assume it is being closed externally and move over to the next popup - if (popupIndex < 0 ) { - closePopup(0); - return; - } - popupNotifications.currentPopupIndex = popupIndex; + + // state transition animations / timers + Behavior on opacityAnimationValue { + NumberAnimation { + duration: Kirigami.Units.veryLongDuration * 1.5 + easing.type: Easing.OutQuint } } - property bool isActionDrawerOpen: MobileShellState.ShellDBusClient.isActionDrawerOpen - - property bool waiting: true - property bool popupDrawerOpened: false - property bool inPopupDrawer: false - - property var keyboardInteractivity - property Instantiator popupNotifications - property int popupCount: popupNotifications.count - property int popupIndex - property var popupModel - property var notificationsModel - property int notificationsModelType - property var timeDataSource + NumberAnimation on dragOffset { + id: dragOffsetAnimation + running: false + to: 0 + duration: Kirigami.Units.veryLongDuration * 1.5 + easing.type: Easing.OutExpo + } - property bool preventDismissTimeout: true - property int timeout - property int dismissTimeout + NumberAnimation on remainingTimeProgress { + from: 1 + to: 0 + duration: hideTimer.interval + running: hideTimer.running + } - property int defaultTimeout: 5000 - readonly property int effectiveTimeout: { - if (timeout === -1) { - return defaultTimeout; - } - if (dismissTimeout) { - return dismissTimeout; + ParallelAnimation { + id: slideInAnimation + NumberAnimation { + target: popupDelegate + property: "verticalOffset" + to: 0 + duration: Kirigami.Units.veryLongDuration * 1.25 + easing.type: Easing.OutQuint } - return model.timeout; } - // show the top most notification in the list and move the rest to the popup drawer - function updateNotificationPopups() { - if (popupCount != 1) { - for (var i = 0; i < popupCount - 1; i++) { - popupNotifications.objectAt(i + 1).moveToPopupDrawer(); + ParallelAnimation { + id: slideOutAnimation + NumberAnimation { + target: popupDelegate + property: "verticalOffset" + to: { + if (popupDelegate.closedWithSwipe) { + return -popupDelegate.height - popupDelegate.topPopupOffset - popupDelegate.dragOffset; + } else { + return -popupDelegate.height - popupDelegate.topPopupOffset; + } } + duration: (popupDelegate.closedWithSwipe || popupDelegate.notificationCount > 0) ? Kirigami.Units.veryLongDuration * 0.5 : Kirigami.Units.veryLongDuration * 1.25 + easing.type: (popupDelegate.closedWithSwipe || popupDelegate.notificationCount > 0) ? Easing.Linear : Easing.InQuint + } + onStopped: { + popupDelegate.ListView.delayRemove = false; + popupManager.activeRemovalAnimations -= 1; } - popupNotifications.objectAt(0).showNotificationPopup(); - visible = true; } - function showNotificationPopup() { - if (isClosing) { - closePopup(popupIndex); - return; + ParallelAnimation { + id: fadeOutAnimation + NumberAnimation { + target: notificationItem + property: "opacity" + to: 0 + duration: Kirigami.Units.veryLongDuration * 1.25 + easing.type: Easing.OutQuint } - if (notificationItem.state != "open") { - preventDismissTimeout = true; + NumberAnimation { + target: popupDelegate + property: "heightAnimationValue" + to: 0 + duration: Kirigami.Units.veryLongDuration * 1.25 + easing.type: Easing.OutQuint + } + onStopped: { + popupDelegate.ListView.delayRemove = false; + popupManager.activeRemovalAnimations -= 1; } - waiting = false; - inPopupDrawer = false; - popupNotifications.currentPopupIndex = popupIndex; - visible = true; - openPopup(); - updateTouchArea(); } - function moveToPopupDrawer() { - if (isClosing) { - return; + ParallelAnimation { + id: fadeInAnimation + NumberAnimation { + target: notificationItem + property: "opacity" + to: 1 + duration: Kirigami.Units.veryLongDuration * 1.25 + easing.type: Easing.InQuint } - waiting = false; - inPopupDrawer = true; - if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") { - notificationItem.offset = openOffset; - notificationItem.scale = 0.75; - notificationItem.popupOpacity = 0.0; + NumberAnimation { + target: popupDelegate + property: "heightAnimationValue" + from: 0 + to: 1 + duration: Kirigami.Units.veryLongDuration * 1.25 + easing.type: Easing.OutQuint } - notificationItem.state = "inDrawerClosed"; - notificationPopup.removeKeyboardFocus(); - visible = true; } - function openPopup() { - if (notificationPopup.popupDrawerOpened && notificationItem.state != "open" && notificationItem.state != "inDrawerClosed") { - notificationItem.offset = openOffset; - notificationItem.scale = 0.75; - notificationItem.popupOpacity = 0.0; + // the timer for when the notification will dismiss + Timer { + id: hideTimer + interval: popupDelegate.effectiveTimeout + running: { + if (interval <= 0) return false; + if (popupDelegate.preventDismissTimeout) return false; + if (!popupDelegate.isCurrentPopup) return false; + if (popupDelegate.popupDrawerOpened) return false; + return true; } - notificationItem.state = "open"; - notificationPopup.removeKeyboardFocus(); + onTriggered: popupDelegate.closePopup() } - // if the notification ever expires, close it and move on to the next one in the list. - property bool isExpired: model.expired - onIsExpiredChanged: closePopup(popupIndex) - - // this closes the popup notification with the relvent animation while updating the popup below to show, if any exist - function closePopup(index: int) { - notificationPopup.removeKeyboardFocus(); - notificationPopup.setInputTransparent(); - if (index + 1 < popupCount) { - popupNotifications.objectAt(index + 1).aboveNotificationHeight = 0; - popupNotifications.objectAt(index + 1).aboveNotificationFullOffset = 0; - } - - if (popupCount > 1) { - let nextNotificationIdx = index + (index < popupCount - 1 ? 1 : -1); - let nextNotification = popupNotifications.objectAt(nextNotificationIdx); - - if (nextNotification != null) { - nextNotification.showNotificationPopup(); - if (!isExpired) { - if (!dragOffsetAn.running && nextNotification.queueTimer.running) { - nextNotification.queueTimer.stop(); - notificationItem.state = "closeWithScale"; - } else { - notificationItem.state = "closeWithMove"; - } - return; - } - } - } + // helper functions + function closePopup() { if (isExpired) { notificationItem.close(); return; } - notificationItem.state = "closeWithMove"; + dismissClicked(); } - function calculateResistance(value : double, threshold : int) : double { + function calculateResistance(value: real, threshold: int): real { if (value > threshold) { - return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((longestLength - threshold) * 15), 0.35)); + return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((Screen.height - threshold) * 15), 0.35)); } else { return value; } @@ -311,290 +245,111 @@ Item { NotificationPopupItem { id: notificationItem - inPopupDrawer: notificationPopup.inPopupDrawer && !notificationPopup.popupDrawerOpened - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - - width: notificationPopup.popupWidth - height: notificationPopup.popupHeight - - model: notificationPopup.popupModel - modelIndex: notificationPopup.popupIndex - notificationsModel: notificationPopup.notificationsModel - notificationsModelType: notificationPopup.notificationsModelType - clockSource: notificationPopup.timeDataSource - panelType: notificationPopup.popupDrawerOpened ? - MobileShell.PanelBackground.PanelType.Drawer : - MobileShell.PanelBackground.PanelType.Popup - - currentPopupHeight: popupNotifications.currentPopupHeight + anchors.left: parent.left + anchors.right: parent.right - remainingTimeProgress: notificationPopup.remainingTimeProgress + inPopupDrawer: !popupDelegate.isCurrentPopup && !popupDelegate.popupDrawerOpened + model: popupDelegate.model + modelIndex: popupDelegate.index + notificationsModel: popupDelegate.notificationsModel + notificationsModelType: popupDelegate.notificationsModelType + clockSource: popupDelegate.timeDataSource + panelType: popupDelegate.popupDrawerOpened ? MobileShell.PanelBackground.PanelType.Drawer : MobileShell.PanelBackground.PanelType.Popup + animateHeight: popupDelegate.isCurrentPopup + currentPopupHeight: popupDelegate.currentPopupHeight + remainingTimeProgress: popupDelegate.remainingTimeProgress closeTimerRunning: hideTimer.running - onDragStart: preventDismissTimeout = true - onDragEnd: preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand) + property real popupScale: (popupDelegate.index > 0) ? (1 - Math.min(popupDelegate.index, 3) * 0.075) : 1 - onTakeFocus: { - notificationPopup.setKeyboardFocus(); - preventDismissTimeout = true; - } - - onDismissRequested: closePopup(popupIndex) - - property real offset: closedOffset - 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: { - if (notificationPopup.popupDrawerOpened) { - return 0; // when popup drawer is opened, reset scale to 0 - } - let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; - // clamp the index value to avoid scaling too much with animations - let indexClamped = Math.max(Math.min(index, 2), 0); - return indexClamped * 0.075; - } - property real drawerAddedOffset: { - if (notificationPopup.popupDrawerOpened) { - return 0; // when popup drawer is opened, reset any added height to 0 - } - let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; - // clamp the index value to avoid moving too much with animations - let indexClamped = Math.max(Math.min(index, 2), -1); - return Kirigami.Units.gridUnit * 0.5 * indexClamped; - } - property real drawerOpacity: { - let index = notificationPopup.popupIndex - popupNotifications.currentPopupIndex; - if (index > 2 && !notificationPopup.popupDrawerOpened) { - return 0; // make this popup invisible if it is below 3 other popups - } else { - return 1; // when popup drawer is opened, reset opacity to 1 - } - } - Behavior on drawerScale { + Behavior on popupScale { NumberAnimation { - duration: Kirigami.Units.veryLongDuration * 1.25 + duration: Kirigami.Units.veryLongDuration * 1.5 easing.type: Easing.OutQuint } } - Behavior on drawerAddedOffset { - NumberAnimation { - duration: Kirigami.Units.veryLongDuration * 1.25 - easing.type: Easing.OutQuint - } - } - Behavior on drawerOpacity { - NumberAnimation { - duration: Kirigami.Units.veryLongDuration * 1.25 - easing.type: Easing.OutQuint - } - } - - opacity: Math.min(popupOpacity, drawerOpacity) - - state: "" - states: [ - State { - name: "open" - PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset - } - PropertyChanges { - target: notificationItem; scale: 1.0 - } - PropertyChanges { - target: notificationItem; popupOpacity: 1.0 - } - }, - State { - name: "closeWithMove" - PropertyChanges { - target: notificationItem; offset: notificationPopup.closedOffset - } - PropertyChanges { - target: notificationItem; scale: 1.0 - } - PropertyChanges { - target: notificationItem; popupOpacity: 1.0 - } - }, - State { - name: "closeWithScale" - PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset - } - PropertyChanges { - target: notificationItem; scale: 0.75 - } - PropertyChanges { - target: notificationItem; popupOpacity: 0.0 - } + transform: [ + Scale { + origin.x: Math.round(notificationItem.width * 0.5) + origin.y: popupDelegate.currentPopupHeight + Kirigami.Units.gridUnit * 5 + xScale: (notificationItem.popupScale * (1 - popupDelegate.popupDrawerAnimationValue)) + popupDelegate.popupDrawerAnimationValue + yScale: (notificationItem.popupScale * (1 - popupDelegate.popupDrawerAnimationValue)) + popupDelegate.popupDrawerAnimationValue }, - State { - name: "inDrawerClosed" - PropertyChanges { - target: notificationItem; offset: notificationPopup.openOffset - } - PropertyChanges { - target: notificationItem; scale: 1 - } - PropertyChanges { - target: notificationItem; popupOpacity: 1 - } + Translate { + y: (popupDelegate.offsetFromCurrentPopup * (1 - popupDelegate.popupDrawerAnimationValue)) + popupDelegate.verticalOffset + popupDelegate.dragOffset + Math.max(currentPopupDragOffset, 0) } ] - readonly property int notificationEasing: { - // check whether the popup is the current one or above it - let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex; - // check whether the popup has any popups below it - let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1; - let popupOpening = notificationItem.state == "open" || notificationItem.state == "inDrawerClosed"; - let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale" - if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) { - // set the easing type to linear when closed with a swipe or if a popup is below when closing - // as to make sure the popup feels like it is keeping it's momentum - return Easing.Linear; - } else if (popupOpening) { - // set the easing type to 'Out' when opening so the popup will have a gentle landing - return Easing.OutQuint; - } else { - // if above conditions fail, set the easing type to 'In' so the popup will build up speed for it's exit - return Easing.InQuint; - } - } + onDragStart: preventDismissTimeout = true + onDragEnd: preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand) - readonly property real notificationDuration: { - // check whether the popup is the current one or above it - let topPopup = popupNotifications.currentPopupIndex >= notificationPopup.popupIndex; - // check whether the popup has any popups below it - let popupBelow = notificationPopup.popupCount - notificationPopup.popupIndex > 1; - let popupClosing = notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale" - if (notificationPopup.closedWithSwipe || (topPopup && popupClosing && popupBelow)) { - // make sure the speed it faster when closed with a swipe or if there is a popup below when closing - // as to make sure the speed feels comparable with the easing type is set to linear - return Kirigami.Units.veryLongDuration * 0.5; - } else { - return Kirigami.Units.veryLongDuration * 1.25; - } + onTakeFocus: { + popupDelegate.setKeyboardFocus(); + preventDismissTimeout = true; } - transitions: Transition { - SequentialAnimation { - ParallelAnimation { - PropertyAnimation { - properties: "offset" - easing.type: notificationItem.notificationEasing - duration: notificationItem.notificationDuration - } - PropertyAnimation { - properties: "scale" - easing.type: notificationItem.notificationEasing - duration: notificationItem.notificationDuration - } - PropertyAnimation { - properties: "popupOpacity" - easing.type: notificationItem.notificationEasing - duration: notificationItem.notificationDuration - } - } - ScriptAction { - script: { - if (notificationItem.state == "open") { - notificationPopup.preventDismissTimeout = false; - notificationPopup.updateTouchArea(); - } else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") { - notificationPopup.preventDismissTimeout = true; - if (notificationPopup.dismissTimeout) { - notificationPopup.dismissClicked(); - } else { - notificationPopup.expired(); - } - } - } - } - } - } + onDismissRequested: notificationItem.close() - transform: [ - Scale { - origin.x: Math.round(notificationPopup.popupWidth / 2) - origin.y: notificationPopup.scaleOriginY - xScale: notificationItem.scale - notificationItem.drawerScale - yScale: notificationItem.scale - notificationItem.drawerScale + // capture taps were the notifications are grouping together to open the popup notification drawer + MouseArea { + id: drawerInteractionArea + anchors.left: notificationItem.left + anchors.right: notificationItem.right + anchors.top: notificationItem.bottom + height: Kirigami.Units.gridUnit * 1.5 + + enabled: !popupDelegate.popupDrawerOpened && popupDelegate.notificationCount > 1 && popupDelegate.isCurrentPopup + + onReleased: { + popupDelegate.openPopupDrawer(); + popupDelegate.updateTouchArea(); + popupDelegate.setKeyboardFocus(); } - ] - } - - transform: [ - Translate { - y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset + notificationItem.drawerAddedOffset } - ] + } DragHandler { id: dragHandler xAxis.enabled: false - yAxis.enabled: popupNotifications.currentPopupIndex == notificationPopup.popupIndex && !notificationPopup.popupDrawerOpened + yAxis.enabled: popupDelegate.index == 0 && !popupDelegate.popupDrawerOpened target: null property real lastOffset: 0 - property real startDragOffset: 0 property real startPosition: 0 - property bool startActive: false + property bool isStartActive: false onTranslationChanged: { - if (notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove") { - return; - } - if (startActive) { + if (popupDelegate.index < 0) return; + if (isStartActive) { startDragOffset = notificationPopup.dragOffset; startPosition = translation.y; - startActive = false; + isStartActive = false; } lastOffset = notificationPopup.dragOffset; - notificationPopup.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 0); + popupDelegate.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 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)) { + isStartActive = active; + popupDelegate.preventDismissTimeout = true; + if (!active && !(slideOutAnimation.running || fadeOutAnimation.running)) { + if ((lastOffset - popupDelegate.dragOffset > 1.0 && popupDelegate.dragOffset < 0) || (popupDelegate.closedOffset * 0.5 > popupDelegate.dragOffset)) { // this code is called when the notification is swiped or dragged to the top. - notificationPopup.closedWithSwipe = true; - notificationPopup.closePopup(popupIndex); + popupDelegate.closedWithSwipe = true; + popupDelegate.closePopup(); return; } - dragOffsetAn.running = true; - if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) { + dragOffsetAnimation.running = true; + if (popupDelegate.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < popupDelegate.dragOffset) { // this code is called when the notification is swiped or dragged down. } - notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand); + popupDelegate.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand); } else { - dragOffsetAn.running = false; + dragOffsetAnimation.running = false; } } } - - MouseArea { - // capture taps were the notifications are grouping together to open the popup notification drawer - id: item - anchors.left: parent.left - anchors.right: parent.right - anchors.top: notificationItem.bottom - - height: Kirigami.Units.gridUnit * 2 - - enabled: !notificationPopup.popupDrawerOpened && (notificationPopup.popupCount - popupNotifications.currentPopupIndex > 1) - - onReleased: { - notificationPopup.openPopupDrawer(); - notificationPopup.updateTouchArea(); - notificationPopup.setKeyboardFocus(); - } - } } + diff --git a/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml b/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml index f0fc8ca15..0e8f68efc 100644 --- a/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml +++ b/components/mobileshell/qml/popups/notifications/NotificationPopupManager.qml @@ -1,26 +1,20 @@ /* - * SPDX-FileCopyrightText: 2024 Micah Stanley <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later + SPDX-FileCopyrightText: 2024 Micah Stanley <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later */ -import QtQuick 2.15 +import QtQuick import QtQuick.Layouts 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.layershell 1.0 as LayerShell +import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.notificationmanager as NotificationManager import org.kde.plasma.clock -import QtQuick.Controls as Controls -import org.kde.plasma.components 3.0 as PlasmaComponents -import org.kde.taskmanager 0.1 as TaskManager - +import org.kde.layershell 1.0 as LayerShell /** * This sets up and manages the notification popups @@ -28,240 +22,287 @@ import org.kde.taskmanager 0.1 as TaskManager Window { id: notificationPopupManager - 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) - property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone + width: Screen.width + height: Screen.height + visible: false + color: isPopupDrawerOpen && visible ? backgroundColor : "transparent" - LayerShell.Window.scope: "notification" LayerShell.Window.anchors: LayerShell.Window.AnchorTop | LayerShell.Window.AnchorHorizontalCenter LayerShell.Window.layer: LayerShell.Window.LayerOverlay LayerShell.Window.exclusionZone: -1 LayerShell.Window.keyboardInteractivity: keyboardInteractivity - // This toggles whether to show all the active popup notifications at ones in a list - property bool popupDrawerOpened: false + Kirigami.Theme.colorSet: Kirigami.Theme.Complementary + Kirigami.Theme.inherit: false property var notificationModelType + property var popupNotificationsModel property QtObject notificationSettings - property QtObject popupNotificationsModel property QtObject tasksModel property Clock clockSource property bool inhibited - Kirigami.Theme.colorSet: Kirigami.Theme.Complementary - Kirigami.Theme.inherit: false + property int lastNotificationCount: 0 + property int activeRemovalAnimations: 0 - readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05) - color: popupDrawerOpened && visible ? backgroundColor : "transparent" - Behavior on color { - ColorAnimation { - duration: Kirigami.Units.veryLongDuration * 1.5 - easing.type: Easing.OutExpo + // when the notification model reaches zero we wait for all the removal animations to complete first before hiding the window + onActiveRemovalAnimationsChanged: { + if (activeRemovalAnimations === 0 && lastNotificationCount === 0) { + notificationPopupManager.visible = false; } } - width: longestLength - height: longestLength + // due to the count property on the ListView not updating when the window is not visible, + // we need to get the count vaule from the model itself so we can update the window visibilty state + // NOTE: we call for the row count value directly as binding to the normal count vaule from the model seems to lags behide a number + Connections { + target: popupNotificationsModel - signal timeChanged + function onRowsInserted(parent, first, last) { + notificationPopupManager.handleModelChange(); + } - Component.onCompleted: ShellUtil.setInputTransparent(notificationPopupManager, true) + function onRowsRemoved(parent, first, last) { + notificationPopupManager.handleModelChange(); + } - Binding { - target: MobileShellState.ShellDBusClient - property: "isNotificationPopupDrawerOpen" - value: popupDrawerOpened + function onModelReset() { + notificationPopupManager.handleModelChange(); + } } - // hide on timeout to give time to finish animations - Timer { - id: hideTimeout - interval: Kirigami.Units.veryLongDuration * 1.5 - repeat: false - onTriggered: if (notifications.count == 0) notificationPopupManager.visible = false; - } + function handleModelChange() { + if (!popupNotificationsModel) return; - // Update the window touch region to encapsulate the notification area or the whole screen depending on the 'popupDrawerOpened' state - function updateTouchArea() { - ShellUtil.setInputTransparent(notificationPopupManager, false); - if (popupDrawerOpened) { - ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0)); - } else { - // get the height of the popup directly to ensure we get the latest version - let popupHeight = Kirigami.Units.gridUnit * 6; - let currentPopup = notifications.objectAt(notifications.currentPopupIndex); - if (currentPopup) { - popupHeight = currentPopup.popupHeight; - } else { - console.warn("popupNotification: could not retrieve current popup height - falling back to a default value") - } + let currentCount = popupNotificationsModel.rowCount(); - 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 (currentCount === 0) { + ShellUtil.setInputTransparent(notificationPopupManager, true); + notificationPopupManager.isPopupDrawerOpen = false; + } else if (currentCount === 1 && lastNotificationCount === 0) { + notificationPopupManager.visible = true; } + + notificationPopupManager.updateTouchArea(); + + lastNotificationCount = currentCount; } - // parent the popup notifications inside a Flickable so that they can be scrollable when the drawer state is active - Flickable { - id: flickable - width: notificationPopupManager.width - height: Screen.height - contentHeight: notifications.fullHeight + notificationPopupManager.openOffset - boundsBehavior: Flickable.DragAndOvershootBounds - bottomMargin: Kirigami.Units.gridUnit * 6 + property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone + property bool isPopupDrawerOpen: false // this toggles whether to show all the active popup notifications at ones in a list + property real popupDrawerAnimationValue: notificationPopupManager.isPopupDrawerOpen ? 1 : 0 // animate notifications entering and exiting the drawer + property var currentNotification: notifications.count > 0 ? notifications.itemAtIndex(0) : null - interactive: notificationPopupManager.popupDrawerOpened + readonly property int popupWidth: Math.min(Kirigami.Units.gridUnit * 20, Screen.width - Kirigami.Units.gridUnit * 2) + readonly property real openOffset: (Kirigami.Units.smallSpacing * 3) + Kirigami.Units.gridUnit + readonly property color backgroundColor: Qt.darker(Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.95), 1.05) - onDragEnded: flickable.checkDismiss(); - onFlickEnded: flickable.checkDismiss(); - onDragStarted: { - notifications.recalculateHeight(); - atBeginning = flickable.atYBeginning; - atEnd = flickable.atYEnd; + signal timeChanged + + onWidthChanged: { + if (visible) { + notificationPopupManager.updateTouchArea(); } - onFlickStarted: { - notifications.recalculateHeight(); - atBeginning = flickable.atYBeginning; - atEnd = flickable.atYEnd; + } + + onCurrentNotificationChanged: { + if (currentNotification) { + updateTouchArea(); } + } - property bool atBeginning: false - property bool atEnd: false + Component.onCompleted: ShellUtil.setInputTransparent(notificationPopupManager, true) - function checkDismiss() { - let dismissFromTop = atBeginning && flickable.verticalOvershoot < -Kirigami.Units.gridUnit; - let dismissFromBottom = atEnd && flickable.verticalOvershoot > Kirigami.Units.gridUnit; - if (dismissFromTop || dismissFromBottom) { - flickable.dismiss(); - } - } + Binding { + target: MobileShellState.ShellDBusClient + property: "isNotificationPopupDrawerOpen" + value: notificationPopupManager.isPopupDrawerOpen + } - function dismiss() { - notificationPopupManager.popupDrawerOpened = false; - notificationPopupManager.updateTouchArea(); - resetContentY.running = true; + Behavior on popupDrawerAnimationValue { + NumberAnimation { + duration: Kirigami.Units.veryLongDuration * 1.5 + easing.type: Easing.OutQuint } + } - NumberAnimation on contentY { - id: resetContentY - running: false - to: 0 + Behavior on color { + ColorAnimation { duration: Kirigami.Units.veryLongDuration * 1.5 easing.type: Easing.OutExpo } + } - MouseArea { - // capture taps behind the notifications to close the drawer - id: item - anchors.left: parent.left - anchors.right: parent.right - width: notificationPopupManager.width - height: Math.max(notifications.fullHeight, Screen.height) - - onReleased: flickable.dismiss(); - - Instantiator { - id: notifications - model: popupNotificationsModel - - // get the height, drag offset, and idx of the current popup notifition and make it easily accessible by all popup notifications - property int currentPopupHeight: (count > 0 && currentPopupIndex < count && objectAt(currentPopupIndex)) ? objectAt(currentPopupIndex).popupHeight : 0; - property int currentDragOffset: 0 - property int currentPopupIndex: 0 - - // calculate the full height of all the notifications combine for scrolling purposes - property int fullHeight: 0 - onCountChanged: { - if (count == 0) { - ShellUtil.setInputTransparent(notificationPopupManager, true); - hideTimeout.restart(); - notificationPopupManager.popupDrawerOpened = false; - fullHeight = 0; - return; - } - notificationPopupManager.visible = true; - notifications.recalculateHeight(); - } + NumberAnimation { + id: popupDrawerCloseAnimation + target: notifications + property: "contentY" + to: notifications.originY - notifications.topMargin + duration: Kirigami.Units.veryLongDuration * 1 + easing.type: Easing.OutExpo + } - function recalculateHeight() { - let findHeight = 0 - for (var i = 0; i < count; i++) { - findHeight += notifications.objectAt(i).popupHeight + Kirigami.Units.gridUnit; - } - fullHeight = findHeight; - } + // Update the window touch region to encapsulate the notification area or the whole screen depending on the 'isPopupDrawerOpen' state + function updateTouchArea() { + notificationPopupManager.currentNotification = Qt.binding(() => notifications.count > 0 ? notifications.itemAtIndex(0) : null); - delegate: NotificationPopup { - id: popup + if (!(notifications.count > 0)) { + ShellUtil.setInputTransparent(notificationPopupManager, true); + return; + } - anchors.horizontalCenter: parent.horizontalCenter - z: notifications.count - index + ShellUtil.setInputTransparent(notificationPopupManager, false); + if (!currentNotification) return; - popupWidth: notificationPopupManager.popupWidth - openOffset: notificationPopupManager.openOffset + if (isPopupDrawerOpen) { + // reset the touch layout to use the entire screen dimensions + ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(0, 0, 0, 0)); + } else { + let currentNotificationMappedToView = currentNotification.mapToItem(notifications, 0, 0); + ShellUtil.setInputRegion( + notificationPopupManager, + Qt.rect( + currentNotificationMappedToView.x, + currentNotificationMappedToView.y, + currentNotification.width, + currentNotification.height + Kirigami.Units.gridUnit * 1.5 + ) + ); + } + } - keyboardInteractivity: notificationPopupManager.keyboardInteractivity - popupNotifications: notifications - popupIndex: index + ListView { + id: notifications + anchors.fill: parent + topMargin: notificationPopupManager.openOffset + bottomMargin: Kirigami.Units.gridUnit * 4 + spacing: 0 - popupDrawerOpened: notificationPopupManager.popupDrawerOpened + model: popupNotificationsModel + interactive: notificationPopupManager.isPopupDrawerOpen - popupModel: model - notificationsModel: popupNotificationsModel - notificationsModelType: notificationModelType - timeDataSource: clockSource + cacheBuffer: Math.max(notifications.height * 0.25, ((currentNotification ? currentNotification.height : Kirigami.Units.gridUnit * 3) * 3) - notifications.height) + displayMarginEnd: Math.max(notifications.height, ((currentNotification ? currentNotification.height : Kirigami.Units.gridUnit * 3) * 3) - notifications.height) - timeout: model.timeout + // internal tracking states for gesture thresholds + property bool isAtBeginning: false + property bool isAtEnd: false - onUpdateTouchArea: notificationPopupManager.updateTouchArea() + // event handlers + onDragStarted: { + isAtBeginning = notifications.atYBeginning; + isAtEnd = notifications.atYEnd; + } + onFlickStarted: { + isAtBeginning = notifications.atYBeginning; + isAtEnd = notifications.atYEnd; + } + onDragEnded: notifications.checkDismiss() + onFlickEnded: notifications.checkDismiss() - onSetInputTransparent: ShellUtil.setInputTransparent(notificationPopupManager, true) + onOriginYChanged: { + resetContentVerticalAnimation.to = notifications.originY - notifications.topMargin; + if (resetContentVerticalAnimation.running) { + resetContentVerticalAnimation.restart(); + } + updateTouchArea(); + } - onOpenPopupDrawer: notificationPopupManager.popupDrawerOpened = true + NumberAnimation on contentY { + id: resetContentVerticalAnimation + running: false + to: notifications.originY - notifications.topMargin + duration: Kirigami.Units.veryLongDuration * 1.5 + easing.type: Easing.OutExpo + onFinished: updateTouchArea() + } - onSetKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityOnDemand + // capture taps behind the notifications to close the drawer + TapHandler { + onTapped: notifications.dismiss() + } - onRemoveKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone + // helper functions + function checkDismiss() { + let dismissFromTop = isAtBeginning && notifications.verticalOvershoot < -Kirigami.Units.gridUnit; + let dismissFromBottom = isAtEnd && notifications.verticalOvershoot > Kirigami.Units.gridUnit; + if (dismissFromTop || dismissFromBottom) { + notifications.dismiss(); + } + } - defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0) + function dismiss() { + if (!notificationPopupManager.isPopupDrawerOpen) return; - dismissTimeout: !notificationSettings.permanentJobPopups - && model.type === NotificationManager.Notifications.JobType - && model.jobState !== NotificationManager.Notifications.JobStateStopped - ? defaultTimeout : 0 + notificationPopupManager.isPopupDrawerOpen = false; + notificationPopupManager.updateTouchArea(); + resetContentVerticalAnimation.restart(); + } - onDismissClicked: model.dismissed = true + delegate: NotificationPopup { + id: notificationPopup + width: notificationPopupManager.popupWidth + z: notifications.count - index - onExpired: { - if (model.resident) { - // When resident, only mark it as expired so the popup disappears - // but don't actually invalidate the notification - model.expired = true; - } else { - if (notificationModelType === NotificationsModelType.WatchedNotificationsModel) { - popupNotificationsModel.expire(model.notificationId); - } else if (notificationModelType === NotificationsModelType.NotificationsModel) { - popupNotificationsModel.expire(popupNotificationsModel.index(index, 0)); - } - } + transform: [ + Translate { + x: (notificationPopupManager.width - notificationPopupManager.popupWidth) * 0.5 + } + ] + + popupManager: notificationPopupManager + keyboardInteractivity: notificationPopupManager.keyboardInteractivity + popupDrawerOpened: notificationPopupManager.isPopupDrawerOpen + notificationsModel: popupNotificationsModel + notificationsModelType: notificationModelType + timeDataSource: clockSource + notificationCount: notifications.count + topPopupOffset: notificationPopupManager.openOffset + popupDrawerAnimationValue: notificationPopupManager.popupDrawerAnimationValue + timeout: model.timeout + + currentPopupHeight: (notifications.count > 0 && notificationPopup.index > 0 && currentNotification) ? currentNotification.cardHeight : Kirigami.Units.gridUnit * 6 + currentPopupDragOffset: (notifications.count > 0 && index > 0 && currentNotification) ? currentNotification.dragOffset : 0 + offsetFromCurrentPopup: notifications.originY - notificationPopup.y + + defaultTimeout: notificationSettings.popupTimeout + (model.urls && model.urls.length > 0 ? 5000 : 0) + dismissTimeout: !notificationSettings.permanentJobPopups + && model.type === NotificationManager.Notifications.JobType + && model.jobState !== NotificationManager.Notifications.JobStateStopped + ? defaultTimeout : 0 + + onUpdateTouchArea: notificationPopupManager.updateTouchArea() + onOpenPopupDrawer: notificationPopupManager.isPopupDrawerOpen = true + onSetKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityOnDemand + onRemoveKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone + onDismissClicked: model.dismissed = true + + onExpired: { + if (model.resident) { + // When resident, only mark it as expired so the popup disappears + // but don't actually invalidate the notification + model.expired = true; + } else { + if (notificationModelType === NotificationsModelType.WatchedNotificationsModel) { + popupNotificationsModel.expire(model.notificationId); + } else if (notificationModelType === NotificationsModelType.NotificationsModel) { + popupNotificationsModel.expire(popupNotificationsModel.index(index, 0)); } + } + } - Component.onCompleted: { - if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) { - // Register apps that were seen spawning a popup so they can be configured later - // Apps with notifyrc can already be configured anyway - if (!model.notifyRcName) { - notificationSettings.registerKnownApplication(model.desktopEntry); - notificationSettings.save(); - } - } - - // Tell the model that we're handling the timeout now - popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0)); - - item.children.push(this); + Component.onCompleted: { + if (model.type === NotificationManager.Notifications.NotificationType && model.desktopEntry) { + // Register apps that were seen spawning a popup so they can be configured later + // Apps with notifyrc can already be configured anyway + if (!model.notifyRcName) { + notificationSettings.registerKnownApplication(model.desktopEntry); + notificationSettings.save(); } } + + // Tell the model that we're handling the timeout now + popupNotificationsModel.stopTimeout(popupNotificationsModel.index(index, 0)); } } } diff --git a/components/mobileshell/qml/popups/notifications/NotificationPopupProvider.qml b/components/mobileshell/qml/popups/notifications/NotificationPopupProvider.qml index 1c32757a0..2515bfc36 100644 --- a/components/mobileshell/qml/popups/notifications/NotificationPopupProvider.qml +++ b/components/mobileshell/qml/popups/notifications/NotificationPopupProvider.qml @@ -1,14 +1,12 @@ /* - * SPDX-FileCopyrightText: 2024 Micah Stanley <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later + SPDX-FileCopyrightText: 2024 Micah Stanley <[email protected]> + + SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick import QtQuick.Layouts -import org.kde.plasma.private.mobileshell.shellsettingsplugin as ShellSettings -import org.kde.plasma.private.mobileshell.state as MobileShellState import org.kde.plasma.private.mobileshell as MobileShell import org.kde.notificationmanager as NotificationManager @@ -28,7 +26,7 @@ QtObject { onNotificationsInhibitedUntilChanged: notificationProvider.checkInhibition() } - property QtObject popupNotificationsModel: NotificationManager.Notifications { + property var popupNotificationsModel: NotificationManager.Notifications { showExpired: false showDismissed: false blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications diff --git a/components/mobileshell/qml/widgets/notifications/NotificationCard.qml b/components/mobileshell/qml/widgets/notifications/NotificationCard.qml index 6c0e4a39d..1a332560b 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationCard.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationCard.qml @@ -19,6 +19,11 @@ Item { */ default property Item contentItem + /** + * The height of the main card. This value is synced to the animated height if 'animateHeight' is set to true. + */ + readonly property real cardHeight: mainCard.implicitHeight + /** * The panel background type for this notification. */ @@ -39,6 +44,11 @@ Item { */ property bool inLockScreen: false + /** + * Whether to animate the changes in the notification card height. + */ + property bool animateHeight: false + /** * The current notification popup height. */ @@ -79,7 +89,6 @@ Item { contentItem.parent = contentParent; contentItem.anchors.fill = contentParent; contentItem.anchors.margins = Kirigami.Units.largeSpacing; - contentParent.children.push(contentItem); } implicitHeight: contentParent.implicitHeight @@ -113,7 +122,7 @@ Item { implicitHeight: inPopupDrawer ? currentPopupHeight : contentParent.implicitHeight Behavior on implicitHeight { NumberAnimation { - duration: Kirigami.Units.veryLongDuration + duration: root.animateHeight ? Kirigami.Units.veryLongDuration : 0 easing.type: Easing.OutExpo } } @@ -135,7 +144,7 @@ Item { } } - background: Item + background: Item {} contentItem: Item { implicitWidth: parent.width diff --git a/components/mobileshell/qml/widgets/notifications/NotificationItem.qml b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml index 3c7078199..b2beb103b 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationItem.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationItem.qml @@ -24,7 +24,10 @@ BaseNotificationItem { id: notificationItem implicitHeight: mainCard.implicitHeight + mainCard.anchors.topMargin + notificationHeading.height + readonly property real cardHeight: mainCard.cardHeight + property bool inLockScreen: false + property bool animateHeight: false property int panelType: MobileShell.PanelBackground.PanelType.Drawer property var cardColorScheme: Kirigami.Theme.View @@ -62,6 +65,7 @@ BaseNotificationItem { onDismissRequested: notificationItem.close(); inLockScreen: notificationItem.inLockScreen panelType: notificationItem.panelType + animateHeight: notificationItem.animateHeight onDragStart: notificationItem.dragStart() onDragEnd: notificationItem.dragEnd() diff --git a/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml b/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml index 0e769074f..52eebab09 100644 --- a/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml +++ b/components/mobileshell/qml/widgets/notifications/NotificationPopupItem.qml @@ -24,7 +24,10 @@ BaseNotificationItem { id: notificationItem implicitHeight: mainCard.implicitHeight + readonly property real cardHeight: mainCard.cardHeight + property bool inPopupDrawer: false + property bool animateHeight: false property int currentPopupHeight: 0 property real remainingTimeProgress: 1 property bool closeTimerRunning: false @@ -46,6 +49,7 @@ BaseNotificationItem { popupNotification: true inPopupDrawer: notificationItem.inPopupDrawer + animateHeight: notificationItem.animateHeight currentPopupHeight: notificationItem.currentPopupHeight remainingTimeProgress: notificationItem.remainingTimeProgress closeTimerRunning: notificationItem.closeTimerRunning