/* * SPDX-FileCopyrightText: 2024 Micah Stanley * * SPDX-License-Identifier: GPL-2.0-or-later */ import QtQuick 2.15 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.plasma.private.mobileshell.shellsettingsplugin as ShellSettings import org.kde.layershell 1.0 as LayerShell 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 /** * This sets up and manages the notification popups */ 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) readonly property bool isConvergence: ShellSettings.Settings.convergenceModeEnabled // Margin between popup and screen edge in convergence mode; used in both // the delegate x position and the input-region calculation so they stay in sync. readonly property real convergencePopupMargin: Kirigami.Units.gridUnit * 2 property var keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone 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 property var notificationModelType 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 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 } } width: longestLength height: longestLength signal timeChanged Component.onCompleted: ShellUtil.setInputTransparent(notificationPopupManager, true) Binding { target: MobileShellState.ShellDBusClient property: "isNotificationPopupDrawerOpen" value: popupDrawerOpened } // 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; } // 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") } if (isConvergence) { let regionX = notificationPopupManager.width - notificationPopupManager.popupWidth - notificationPopupManager.convergencePopupMargin; let regionY = openOffset; 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))); } } } // 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 interactive: notificationPopupManager.popupDrawerOpened onDragEnded: flickable.checkDismiss(); onFlickEnded: flickable.checkDismiss(); onDragStarted: { notifications.recalculateHeight(); atBeginning = flickable.atYBeginning; atEnd = flickable.atYEnd; } onFlickStarted: { notifications.recalculateHeight(); atBeginning = flickable.atYBeginning; atEnd = flickable.atYEnd; } property bool atBeginning: false property bool atEnd: false function checkDismiss() { let dismissFromTop = atBeginning && flickable.verticalOvershoot < -Kirigami.Units.gridUnit; let dismissFromBottom = atEnd && flickable.verticalOvershoot > Kirigami.Units.gridUnit; if (dismissFromTop || dismissFromBottom) { flickable.dismiss(); } } function dismiss() { notificationPopupManager.popupDrawerOpened = false; notificationPopupManager.updateTouchArea(); resetContentY.running = true; } NumberAnimation on contentY { id: resetContentY running: false to: 0 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(); } function recalculateHeight() { let findHeight = 0 for (var i = 0; i < count; i++) { findHeight += notifications.objectAt(i).popupHeight + Kirigami.Units.gridUnit; } fullHeight = findHeight; } delegate: NotificationPopup { id: popup x: notificationPopupManager.isConvergence ? (parent.width - width - notificationPopupManager.convergencePopupMargin) : (parent.width - width) / 2 z: notifications.count - index isConvergence: notificationPopupManager.isConvergence popupWidth: notificationPopupManager.popupWidth openOffset: notificationPopupManager.openOffset keyboardInteractivity: notificationPopupManager.keyboardInteractivity popupNotifications: notifications popupIndex: index popupDrawerOpened: notificationPopupManager.popupDrawerOpened popupModel: model notificationsModel: popupNotificationsModel notificationsModelType: notificationModelType timeDataSource: clockSource timeout: model.timeout onUpdateTouchArea: notificationPopupManager.updateTouchArea() onSetInputTransparent: ShellUtil.setInputTransparent(notificationPopupManager, true) onOpenPopupDrawer: notificationPopupManager.popupDrawerOpened = true onSetKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityOnDemand onRemoveKeyboardFocus: notificationPopupManager.keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone 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 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); } } } } } }