mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-06-11 00:47:22 +00:00
Filter @other out of the history blacklist so senders without a\ndesktop entry still land in the drawer. Keep the convergence empty\nstate, dismiss path, and popup geometry aligned with the current\nframe and dock layout.
283 lines
13 KiB
QML
283 lines
13 KiB
QML
/*
|
|
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
|
|
*
|
|
* 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
|
|
readonly property real convergencePopupMargin: MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
|
|
readonly property real convergencePopupBottomInset: MobileShell.Constants.convergenceDockHeight + MobileShell.Constants.convergenceWorkspaceFrameThickness + Kirigami.Units.largeSpacing
|
|
readonly property int popupAnimationDuration: Math.round(MobileShell.Motion.duration(MobileShell.Motion.SpatialSlow) * 1.5)
|
|
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 {
|
|
MobileShell.MotionColorAnimation {
|
|
duration: notificationPopupManager.popupAnimationDuration
|
|
type: MobileShell.Motion.SpatialSlow
|
|
}
|
|
}
|
|
|
|
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: notificationPopupManager.popupAnimationDuration
|
|
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 - Kirigami.Units.gridUnit / 2;
|
|
let regionY = (currentPopup ? currentPopup.effectiveOpenOffset : Screen.height - notificationPopupManager.convergencePopupBottomInset - popupHeight) - Kirigami.Units.gridUnit / 2;
|
|
ShellUtil.setInputRegion(notificationPopupManager, Qt.rect(regionX, regionY, notificationPopupManager.popupWidth + Kirigami.Units.gridUnit, popupHeight + Kirigami.Units.gridUnit));
|
|
} 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;
|
|
}
|
|
|
|
MobileShell.MotionNumberAnimation on contentY {
|
|
id: resetContentY
|
|
running: false
|
|
to: 0
|
|
type: MobileShell.Motion.SpatialSlow
|
|
duration: notificationPopupManager.popupAnimationDuration
|
|
}
|
|
|
|
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
|
|
? (notificationPopupManager.width - width - notificationPopupManager.convergencePopupMargin)
|
|
: (notificationPopupManager.width - width) / 2
|
|
z: notifications.count - index
|
|
|
|
isConvergence: notificationPopupManager.isConvergence
|
|
popupWidth: notificationPopupManager.popupWidth
|
|
openOffset: notificationPopupManager.openOffset
|
|
convergenceBottomInset: notificationPopupManager.convergencePopupBottomInset
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|