notifications: Implement popup notifications

This merge request implements a more mobile optimized solution for popup notification.
- 

The current controls are:
- Swipe up to move the notification to the notification center.
- Swipe left/right to dismiss the notification entirely.
- If multiple popup notifications are grouped together, tap on the bottom area to view them in a expanded view.

What still needs to be done:
- ~~For notification without a default action, tapping on them should probably open up the associated app.~~ Note: I think I will add this in a separate merge request as it probably should be the case regardless if the notification is a popup
- ~~Swiping down on a notification currently does nothing. Maybe we should map this to a notification action?~~ Note: I have some ideas I will try later, though for now, I will leave this action blank
- ~~The expanded view of notifications should be able to be dismissed by swiping up/down on the top/bottom of the list.~~ Note: Added
- Investigate further into how to remove the current desktop popup notifications.
- ~~Code clean up.~~ Note: The code is at least a bit better

Single popup notification:

![notification_1](/uploads/63d12be6da1dd2676de17940dcadbdfa/notification_1.gif)

Multiple popup notifications:

![notification_2](/uploads/907a14b772f66f46040c28342f4dcf02/notification_2.gif)

Multiple popup notifications in the expanded view:

![notification_3](/uploads/9a7cd09a6bb8a0f7ee70e5bcf7c29c6b/notification_3.gif)

Any feedback would be greatly appreciated.
This commit is contained in:
Micah Stanley 2024-11-07 16:13:06 +00:00 committed by Devin Lin
parent e66d88a754
commit eba6074161
18 changed files with 1372 additions and 23 deletions

View file

@ -54,6 +54,7 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED
Quick
Gui
Sensors
WaylandClient
)
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS

View file

@ -23,6 +23,7 @@ set_source_files_properties(qml/dataproviders/AudioInfo.qml PROPERTIES QT_QML_SI
set_source_files_properties(qml/dataproviders/BatteryInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/BluetoothInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/dataproviders/SignalStrengthInfo.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/notificationpopup/NotificationPopupProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
set_source_files_properties(qml/volumeosd/VolumeOSDProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
# Include qml and js files within ./qml/
@ -41,6 +42,7 @@ target_link_libraries(mobileshellplugin
Qt::Qml
Qt::Gui
Qt::Quick
Qt::WaylandClientPrivate
KF6::KIOGui
Plasma::Plasma
KF6::I18n

View file

@ -0,0 +1,535 @@
/*
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick
import QtQuick.Layouts
import QtQuick.Window
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
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.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support
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.gridUnit : 0
property real aboveNotificationFullOffset: 0
property int aboveNotificationHeight: 0
// 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: {
updateNotificationPopups();
visible = true;
}
}
// 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()
}
// 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
}
// 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
visible: false
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
signal expired()
signal dismissClicked()
signal updateTouchArea()
signal setInputTransparent()
signal openPopupDrawer()
signal setKeyboardFocus()
signal removeKeyboardFocus()
// 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
}
}
// 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
}
}
// 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 wiht new height
// also update the allowed touch area for the main window
onPopupHeightChanged: {
if (popupIndex + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = popupHeight;
}
if (popupNotifications.currentPopupIndex == notificationPopup.popupIndex && notificationItem.state == "open") {
notificationPopup.updateTouchArea();
}
}
// if the offset position need in the expanded drawer changes, update the notification below wiht new offset
onFullOpenOffsetChanged: {
if (popupIndex + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = fullOpenOffset;
}
}
// if the notification is being draged and is the current one
// update 'currentDragOffset' so all notifications can easily access this value
onDragOffsetChanged: {
if (popupNotifications.currentPopupIndex == notificationPopup.popupIndex) {
popupNotifications.currentDragOffset = dragOffset;
}
}
// if a new notification is added, update the above notification values need for the expanded drawer
onPopupCountChanged: {
if (popupIndex + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = popupHeight;
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = fullOpenOffset;
}
}
// update the current popup index value if the index ever changes.
onPopupIndexChanged: {
if (!isClosing && !inPopupDrawer && !waiting) {
popupNotifications.currentPopupIndex = popupIndex;
}
}
// if the action drawer opens, it is best to dismiss all popup notifications
onIsActionDrawerOpenChanged: checkActionDrawerOpened()
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
property bool preventDismissTimeout: true
property int timeout
property int dismissTimeout
property int defaultTimeout: 5000
readonly property int effectiveTimeout: {
if (timeout === -1) {
return defaultTimeout;
}
if (dismissTimeout) {
return dismissTimeout;
}
return model.timeout;
}
Component.onCompleted: checkActionDrawerOpened()
// check if the action drawer is opened, and if so, close the popup with scale
function checkActionDrawerOpened() {
if (isActionDrawerOpen) {
if (popupNotifications.currentPopupIndex == notificationPopup.popupIndex) {
notificationPopup.setInputTransparent();
}
notificationPopup.expired();
keyboardInteractivity = LayerShell.Window.KeyboardInteractivityNone;
notificationItem.state = "closeWithScale";
}
}
// 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();
}
}
popupNotifications.objectAt(0).showNotificationPopup();
visible = true;
}
function showNotificationPopup() {
if (isClosing) {
closePopup();
return;
}
if (notificationItem.state != "open") {
preventDismissTimeout = true;
}
waiting = false;
inPopupDrawer = false;
popupNotifications.currentPopupIndex = popupIndex;
visible = true;
openPopup();
updateTouchArea();
}
function moveToPopupDrawer() {
if (isClosing) {
return;
}
waiting = false;
inPopupDrawer = true;
if (notificationPopup.popupDrawerOpened && notificationItem.state != "inDrawerClosed" && notificationItem.state != "open") {
notificationItem.offset = openOffset;
notificationItem.scale = 0.75;
notificationItem.opacity = 0.0;
}
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.opacity = 0.0;
}
notificationItem.state = "open";
notificationPopup.removeKeyboardFocus();
}
// if the notification ever expires, close it and move on to the next one in the list.
property bool isExpired: model.expired
onIsExpiredChanged: closePopup()
// this closes the popup notification with the relvent animation while updating the popup below to show, if any exist
function closePopup() {
notificationPopup.removeKeyboardFocus();
notificationPopup.setInputTransparent();
if (popupIndex + 1 < popupCount) {
popupNotifications.objectAt(popupIndex + 1).aboveNotificationHeight = 0;
popupNotifications.objectAt(popupIndex + 1).aboveNotificationFullOffset = 0;
}
if (popupCount > 1) {
let nextNotificationIdx = popupIndex + (popupIndex < 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;
}
}
}
if (isExpired) {
notificationItem.close();
return;
}
notificationItem.state = "closeWithMove";
}
function calculateResistance(value : double, threshold : int) : double {
if (value > threshold) {
return threshold + Math.pow(value - threshold + 1, Math.max(0.8 - (value - threshold) / ((longestLength - threshold) * 15), 0.35));
} else {
return value;
}
}
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
timeSource: notificationPopup.timeDataSource
currentPopupHeight: popupNotifications.currentPopupHeight
remainingTimeProgress: notificationPopup.remainingTimeProgress
closeTimerRunning: hideTimer.running
onDragStart: preventDismissTimeout = true
onDragEnd: preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand)
onTakeFocus: {
notificationPopup.setKeyboardFocus();
preventDismissTimeout = true;
}
onDismissRequested: closePopup()
property real offset: closedOffset
property real scale: 1.0
property real drawerScale: 1 - Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) * 0.075
Behavior on drawerScale {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
}
}
property real drawerAddedOffset: Kirigami.Units.gridUnit * 0.5 * Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1)
Behavior on drawerAddedOffset {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
}
}
property real drawerOpacity: (Math.max(notificationPopup.popupIndex - popupNotifications.currentPopupIndex, 1) > 2) ? 0 : 1
Behavior on drawerOpacity {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
}
}
opacity: 1.0
state: ""
states: [
State {
name: "open"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset
}
PropertyChanges {
target: notificationItem; scale: 1.0
}
PropertyChanges {
target: notificationItem; opacity: 1.0
}
},
State {
name: "closeWithMove"
PropertyChanges {
target: notificationItem; offset: notificationPopup.closedOffset
}
PropertyChanges {
target: notificationItem; scale: 1.0
}
PropertyChanges {
target: notificationItem; opacity: 1.0
}
},
State {
name: "closeWithScale"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset
}
PropertyChanges {
target: notificationItem; scale: 0.75
}
PropertyChanges {
target: notificationItem; opacity: 0.0
}
},
State {
name: "inDrawerClosed"
PropertyChanges {
target: notificationItem; offset: notificationPopup.openOffset + (notificationPopup.popupDrawerOpened ? 0 : drawerAddedOffset)
}
PropertyChanges {
target: notificationItem; scale: notificationPopup.popupDrawerOpened ? 1 : drawerScale
}
PropertyChanges {
target: notificationItem; opacity: notificationPopup.popupDrawerOpened ? 1 : drawerOpacity
}
}
]
transitions: Transition {
SequentialAnimation {
ParallelAnimation {
PropertyAnimation {
properties: "offset"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
}
PropertyAnimation {
properties: "scale"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
}
PropertyAnimation {
properties: "opacity"; easing.type: Easing.OutExpo; duration: Kirigami.Units.veryLongDuration * 1.5
}
}
ScriptAction {
script: {
if (notificationItem.state == "open") {
preventDismissTimeout = false;
notificationPopup.updateTouchArea();
} else if (notificationItem.state == "closeWithMove" || notificationItem.state == "closeWithScale") {
preventDismissTimeout = true;
if (dismissTimeout) {
notificationPopup.dismissClicked();
} else if (!isActionDrawerOpen) {
notificationPopup.expired();
}
}
}
}
}
}
transform: [
Scale {
origin.x: Math.round(notificationPopup.popupWidth / 2)
origin.y: notificationPopup.scaleOriginY
xScale: notificationItem.scale
yScale: notificationItem.scale
}
]
}
transform: [
Translate {
y: notificationItem.offset + notificationPopup.fullOffsetAn + notificationPopup.dragOffset + notificationPopup.currentDragOffset
}
]
DragHandler {
id: dragHandler
xAxis.enabled: false
yAxis.enabled: popupNotifications.currentPopupIndex == notificationPopup.popupIndex && !notificationPopup.popupDrawerOpened
target: null
property real lastOffset: 0
property real startDragOffset: 0
property real startPosition: 0
property bool startActive: false
onTranslationChanged: {
if (notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove") {
return;
}
if (startActive) {
startDragOffset = notificationPopup.dragOffset;
startPosition = translation.y;
startActive = false;
}
lastOffset = notificationPopup.dragOffset;
notificationPopup.dragOffset = calculateResistance(startDragOffset + (translation.y - startPosition), 0);
}
onActiveChanged: {
startActive = active;
notificationPopup.preventDismissTimeout = true;
if (!active && !(notificationItem.state == "closeWithScale" || notificationItem.state == "closeWithMove")) {
dragOffsetAn.running = true;
if ((lastOffset - notificationPopup.dragOffset > 1.0 && notificationPopup.dragOffset < 0) || (-(notificationPopup.openOffset - notificationPopup.closedOffset) / 4 > notificationPopup.dragOffset)) {
// this code is called when the notifition is swiped or draged to the top.
notificationPopup.closePopup();
return;
} else if (notificationPopup.dragOffset - lastOffset > 1.0 || Kirigami.Units.gridUnit * 3 < notificationPopup.dragOffset) {
// this code is called when the notifition is swiped or draged down.
}
notificationPopup.preventDismissTimeout = (keyboardInteractivity == LayerShell.Window.KeyboardInteractivityOnDemand);
} else {
dragOffsetAn.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();
}
}
}

View file

@ -0,0 +1,250 @@
/*
* 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 2.20 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.notificationmanager as NotificationManager
import org.kde.plasma.plasma5support 2.0 as P5Support
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)
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 QtObject timeSource
property bool inhibited
Kirigami.Theme.colorSet: Kirigami.Theme.View
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)
// 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 lastest 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")
}
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).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);
notificationPopupManager.visible = false;
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
anchors.horizontalCenter: parent.horizontalCenter
z: notifications.count - index
popupWidth: notificationPopupManager.popupWidth
openOffset: notificationPopupManager.openOffset
keyboardInteractivity: notificationPopupManager.keyboardInteractivity
popupNotifications: notifications
popupIndex: index
popupDrawerOpened: notificationPopupManager.popupDrawerOpened
popupModel: model
notificationsModel: popupNotificationsModel
notificationsModelType: notificationModelType
timeDataSource: timeSource
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 {
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);
}
}
}
}
}
}

View file

@ -0,0 +1,143 @@
/*
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
*
* 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
import org.kde.plasma.plasma5support 2.0 as P5Support
import org.kde.taskmanager 0.1 as TaskManager
/**
* This sets up and manages the notification popup settings
*/
QtObject {
id: notificationProvider
property var notificationModelType: MobileShell.NotificationsModelType.NotificationsModel
property QtObject notificationSettings: NotificationManager.Settings {
onNotificationsInhibitedUntilChanged: notificationProvider.checkInhibition()
}
property QtObject popupNotificationsModel: NotificationManager.Notifications {
showExpired: false
showDismissed: false
blacklistedDesktopEntries: notificationSettings.popupBlacklistedApplications
blacklistedNotifyRcNames: notificationSettings.popupBlacklistedServices
whitelistedDesktopEntries: []
whitelistedNotifyRcNames: []
showJobs: notificationSettings.jobsInNotifications
sortMode: NotificationManager.Notifications.SortByTypeAndUrgency
sortOrder: Qt.DescendingOrder
groupMode: NotificationManager.Notifications.GroupDisabled
urgencies: {
var urgencies = 0;
// Critical always except in do not disturb mode when disabled in settings
if (!notificationProvider.inhibited || notificationSettings.criticalPopupsInDoNotDisturbMode) {
urgencies |= NotificationManager.Notifications.CriticalUrgency;
}
// Normal only when not in do not disturb mode
if (!notificationProvider.inhibited) {
urgencies |= NotificationManager.Notifications.NormalUrgency;
}
// Low only when enabled in settings and not in do not disturb mode
if (!notificationProvider.inhibited && notificationSettings.lowPriorityPopups) {
urgencies |=NotificationManager.Notifications.LowUrgency;
}
return urgencies;
}
}
property bool inhibited: false
onInhibitedChanged: {
var pa = pulseAudio.item;
if (!pa) {
return;
}
var stream = pa.notificationStream;
if (!stream) {
return;
}
if (inhibited) {
// Only remember that we muted if previously not muted.
if (!stream.muted) {
notificationSettings.notificationSoundsInhibited = true;
stream.mute();
}
} else {
// Only unmute if we previously muted it.
if (notificationSettings.notificationSoundsInhibited) {
stream.unmute();
}
notificationSettings.notificationSoundsInhibited = false;
}
notificationSettings.save();
}
function checkInhibition() {
notificationProvider.inhibited = Qt.binding(function() {
var inhibited = false;
if (!NotificationManager.Server.valid) {
return false;
}
var inhibitedUntil = notificationSettings.notificationsInhibitedUntil;
if (!isNaN(inhibitedUntil.getTime())) {
inhibited |= (Date.now() < inhibitedUntil.getTime());
}
if (notificationSettings.notificationsInhibitedByApplication) {
inhibited |= true;
}
if (notificationSettings.inhibitNotificationsWhenScreensMirrored) {
inhibited |= notificationSettings.screensMirrored;
}
return inhibited;
});
}
property QtObject tasksModel: TaskManager.TasksModel {
groupMode: TaskManager.TasksModel.GroupApplications
groupInline: false
}
property QtObject timeSource: P5Support.DataSource {
engine: "time"
connectedSources: ["Local"]
interval: 60000 // 1 min
intervalAlignment: P5Support.Types.AlignToMinute
onDataChanged: {
checkInhibition();
npm.timeChanged();
}
}
property var npm: NotificationPopupManager {
notificationModelType: notificationProvider.notificationModelType
notificationSettings: notificationProvider.notificationSettings
popupNotificationsModel: notificationProvider.popupNotificationsModel
timeSource: notificationProvider.timeSource
inhibited: notificationProvider.inhibited
tasksModel: notificationProvider.tasksModel
}
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2024 Micah Stanley <stanleymicah@proton.me>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import QtQuick
import org.kde.plasma.private.mobileshell as MobileShell
pragma Singleton
/**
* This wraps the NotificationPopupProvider component so that we can avoid loading side
* effects from imports (since this is a singleton and initialized immediately on import).
*/
Loader {
id: root
sourceComponent: Component {
MobileShell.NotificationPopupProvider {}
}
function load() {
root.active = true;
}
}

View file

@ -19,9 +19,9 @@ import org.kde.coreaddons 1.0 as KCoreAddons
Item {
id: notificationItem
required property var notificationsModel
property var notificationsModel
required property int notificationsModelType
property int notificationsModelType
/**
* Whether the notification is allowed to invoke any action, or if it should instead

View file

@ -21,8 +21,8 @@ PlasmaComponents.Label {
maximumLineCount: 3
elide: Text.ElideRight
wrapMode: Text.Wrap
textFormat: TextEdit.RichText
wrapMode: Text.WordWrap
textFormat: TextEdit.PlainText
}

View file

@ -3,6 +3,7 @@
import QtQuick
import QtQuick.Effects
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import org.kde.kirigami 2.12 as Kirigami
@ -14,6 +15,16 @@ Item {
default property Item contentItem
property bool popupNotification: false
property bool inPopupDrawer: false
property int currentPopupHeight: 0
property real remainingTimeProgress: 1
property bool closeTimerRunning: false
property bool tapEnabled: false
property bool swipeGestureEnabled: false
@ -23,6 +34,8 @@ Item {
signal tapped()
signal dismissRequested()
signal configureClicked() // TODO implement settings button
signal dragStart()
signal dragEnd()
onContentItemChanged: {
contentItem.parent = contentParent;
@ -52,7 +65,7 @@ Item {
blurMax: 16
shadowEnabled: true
shadowVerticalOffset: 1
shadowOpacity: 0.5
shadowOpacity: popupNotification ? 0.85 : 0.5
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
}
@ -66,7 +79,7 @@ Item {
color: {
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, 0.3)
return Qt.rgba(darkerBackgroundColor.r, darkerBackgroundColor.g, darkerBackgroundColor.b, popupNotification ? 0.5 : 0.3)
}
radius: Kirigami.Units.cornerRadius
}
@ -82,18 +95,65 @@ Item {
anchors.rightMargin: root.dragOffset < 0 ? -root.dragOffset : 0
anchors.top: parent.top
implicitHeight: contentParent.implicitHeight
implicitHeight: inPopupDrawer ? currentPopupHeight : contentParent.implicitHeight
Behavior on implicitHeight {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
}
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(Kirigami.Theme.backgroundColor.r * 0.95, Kirigami.Theme.backgroundColor.g * 0.95, Kirigami.Theme.backgroundColor.b * 0.95, (root.tapEnabled && mouseArea.pressed) ? 0.95 : 0.85)
color: popupNotification ? Qt.lighter(Kirigami.Theme.backgroundColor, 1.5) : Qt.rgba(Kirigami.Theme.backgroundColor.r * 0.95, Kirigami.Theme.backgroundColor.g * 0.95, Kirigami.Theme.backgroundColor.b * 0.95, (root.tapEnabled && mouseArea.pressed) ? 0.95 : 0.85)
opacity: popupNotification ? 0.85 : 1
radius: Kirigami.Units.cornerRadius
layer.enabled: true
layer.enabled: popupNotification ? false : true
layer.effect: MultiEffect {
brightness: 0.075
}
}
ProgressBar {
id: progress
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
width: root.width
height: 2
value: remainingTimeProgress
opacity: closeTimerRunning ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.longDuration
easing.type: Easing.OutQuad
}
}
background: Item
contentItem: Item {
implicitWidth: parent.width
height: parent.height
clip: true
Rectangle {
width: Math.min(progress.visualPosition * (parent.width + root.dragOffset), parent.width)
height: Math.max(Kirigami.Units.cornerRadius * 2, parent.height)
topLeftRadius: Kirigami.Units.cornerRadius
topRightRadius: Kirigami.Units.cornerRadius
color: Kirigami.ColorUtils.linearInterpolation(Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.8)
}
Rectangle {
width: Math.min(progress.visualPosition * (parent.width + root.dragOffset), parent.width - Kirigami.Units.cornerRadius)
height: Math.max(Kirigami.Units.cornerRadius * 2, parent.height)
topLeftRadius: Kirigami.Units.cornerRadius
color: Kirigami.ColorUtils.linearInterpolation (Kirigami.Theme.highlightColor, Kirigami.Theme.backgroundColor, 0.8)
}
}
}
// clip
layer.enabled: true
@ -124,6 +184,7 @@ Item {
id: dragHandler
enabled: root.swipeGestureEnabled
yAxis.enabled: false
xAxis.enabled: !inPopupDrawer
property real startDragOffset: 0
property real startPosition: 0
@ -143,6 +204,7 @@ Item {
startActive = active;
if (!active) { // release event
root.dragEnd()
let threshold = Kirigami.Units.gridUnit * 5; // drag threshold
if (root.dragOffset > threshold) {
dragAnim.to = root.width;
@ -152,6 +214,8 @@ Item {
dragAnim.to = 0;
}
dragAnim.restart();
} else {
root.dragStart()
}
}
}

View file

@ -15,10 +15,13 @@ Item {
id: actionContainer
required property BaseNotificationItem notification
property bool popupNotification: false
implicitHeight: Math.max(actionFlow.implicitHeight, replyLoader.height)
visible: actionRepeater.count > 0
signal takeFocus()
Flow {
id: actionFlow
width: parent.width
@ -63,6 +66,8 @@ Item {
flat: false
text: modelData.label || ""
visible: !(notificationItem.hasReplyAction && actionContainer.popupNotification)
onClicked: {
if (modelData.actionName === "inline-reply") {
replyLoader.beginReply();
@ -81,11 +86,13 @@ Item {
height: active ? item.implicitHeight : 0
// When there is only one action and it is a reply action, show text field right away
active: false
active: notificationItem.hasReplyAction && actionContainer.popupNotification
visible: active
opacity: active ? 1 : 0
x: active ? 0 : parent.width
property bool replying: false
Behavior on x {
NumberAnimation {
duration: Kirigami.Units.longDuration
@ -100,7 +107,9 @@ Item {
}
function beginReply() {
active = true
actionContainer.takeFocus();
active = true;
replying = true;
replyLoader.item.activate();
}
@ -110,7 +119,7 @@ Item {
buttonText: notificationItem.replySubmitButtonText
onReplied: notificationItem.replied(text)
replying: replyLoader.active
replying: replyLoader.replying
onBeginReplyRequested: replyLoader.beginReply()
}
}

View file

@ -26,6 +26,10 @@ BaseNotificationItem {
property bool inLockscreen: false
signal dragStart()
signal dragEnd()
signal takeFocus()
// notification heading for groups with one element
NotificationGroupHeader {
id: notificationHeading
@ -57,16 +61,19 @@ BaseNotificationItem {
tapEnabled: notificationItem.hasDefaultAction
onTapped: notificationItem.actionInvoked("default");
swipeGestureEnabled: notificationItem.closable
onDismissRequested: notificationItem.close()
onDismissRequested: notificationItem.close();
onDragStart: notificationItem.dragStart()
onDragEnd: notificationItem.dragEnd()
ColumnLayout {
id: column
spacing: 0
// notification summary row
RowLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignRight
Layout.fillWidth: summaryLabel.visible
Layout.bottomMargin: Kirigami.Units.smallSpacing
// notification summary
@ -107,7 +114,7 @@ BaseNotificationItem {
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: column.width - iconContainer.width - Kirigami.Units.smallSpacing
text: notificationItem.body
text: ShellUtil.toPlainText(notificationItem.body)
}
// notification icon
@ -165,6 +172,7 @@ BaseNotificationItem {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
notification: notificationItem
onTakeFocus: notificationItem.takeFocus()
}
// thumbnails

View file

@ -0,0 +1,234 @@
/*
* SPDX-FileCopyrightText: 2021 Devin Lin <devin@kde.org>
* SPDX-FileCopyrightText: 2018-2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
import QtQuick 2.15
import QtQuick.Layouts 1.1
import QtQuick.Window 2.2
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.notificationmanager as NotificationManager
import org.kde.kirigami 2.12 as Kirigami
import org.kde.coreaddons 1.0 as KCoreAddons
// notification properties are in BaseNotificationItem
BaseNotificationItem {
id: notificationItem
implicitHeight: mainCard.implicitHeight
property bool inPopupDrawer: false
property int currentPopupHeight: 0
property real remainingTimeProgress: 1
property bool closeTimerRunning: false
property bool inLockscreen: false
signal dragStart()
signal dragEnd()
signal takeFocus()
signal dismissRequested()
// notification
NotificationCard {
id: mainCard
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
popupNotification: true
inPopupDrawer: notificationItem.inPopupDrawer
currentPopupHeight: notificationItem.currentPopupHeight
remainingTimeProgress: notificationItem.remainingTimeProgress
closeTimerRunning: notificationItem.closeTimerRunning
tapEnabled: notificationItem.hasDefaultAction
onTapped: notificationItem.actionInvoked("default");
swipeGestureEnabled: notificationItem.closable
onDismissRequested: {
model.resident = false;
notificationItem.dismissRequested();
notificationItem.close();
}
onDragStart: notificationItem.dragStart()
onDragEnd: notificationItem.dragEnd()
ColumnLayout {
id: column
spacing: 0
opacity: notificationItem.inPopupDrawer ? 0 : 1
Behavior on opacity {
NumberAnimation {
duration: Kirigami.Units.veryLongDuration
easing.type: Easing.OutExpo
}
}
// notification summary row
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.fillWidth: true
Layout.bottomMargin: Kirigami.Units.smallSpacing
Kirigami.Icon {
id: applicationIconItem
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
source: notificationItem.applicationIconSource
visible: valid
}
PlasmaComponents.Label {
id: applicationNameLabel
Layout.leftMargin: Kirigami.Units.smallSpacing
Layout.fillWidth: true
color: Kirigami.Theme.textColor
elide: Text.ElideLeft
font.pointSize: Kirigami.Theme.defaultFont.pointSize
text: notificationItem.applicationName + (notificationItem.originName ? " · " + notificationItem.originName : "")
}
// notification timestamp
NotificationTimeText {
Layout.rightMargin: Kirigami.Units.smallSpacing
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
notificationType: notificationItem.notificationType
jobState: notificationItem.jobState
jobDetails: notificationItem.jobDetails
time: notificationItem.time
timeSource: notificationItem.timeSource
}
}
// notification contents
RowLayout {
Layout.fillWidth: true
Layout.bottomMargin: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
Layout.alignment: Qt.AlignTop
ColumnLayout {
Layout.alignment: Qt.AlignTop
// notification summary
PlasmaComponents.Label {
id: summaryLabel
Layout.fillWidth: true
textFormat: Text.PlainText
maximumLineCount: 3
wrapMode: Text.WordWrap
elide: Text.ElideRight
font.pointSize: Kirigami.Theme.defaultFont.pointSize
text: MobileShell.NotificationsUtils.determineNotificationHeadingText(notificationItem)
visible: text !== ""
font.weight: Font.DemiBold
}
// notification text
NotificationBodyLabel {
id: bodyLabel
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
Layout.preferredWidth: column.width - iconContainer.width - Kirigami.Units.smallSpacing
text: ShellUtil.toPlainText(notificationItem.body)
}
}
// notification icon
Item {
id: iconContainer
Layout.fillHeight: true
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
Layout.topMargin: Kirigami.Units.smallSpacing
Layout.bottomMargin: Kirigami.Units.smallSpacing
visible: iconItem.shouldBeShown
Kirigami.Icon {
id: iconItem
// don't show two identical icons
readonly property bool shouldBeShown: valid && source != notificationItem.applicationIconSource
anchors.fill: parent
smooth: true
// don't show a generic "info" icon since this is a notification already
source: notificationItem.icon !== "dialog-information" ? notificationItem.icon : ""
visible: shouldBeShown
}
}
}
// Job progress reporting
Loader {
id: jobLoader
Layout.fillWidth: true
Layout.preferredHeight: item ? item.implicitHeight : 0
active: notificationItem.notificationType === NotificationManager.Notifications.JobType
visible: active
sourceComponent: NotificationJobItem {
iconContainerItem: iconContainer
jobState: notificationItem.jobState
jobError: notificationItem.jobError
percentage: notificationItem.percentage
suspendable: notificationItem.suspendable
killable: notificationItem.killable
jobDetails: notificationItem.jobDetails
onSuspendJobClicked: notificationItem.suspendJobClicked()
onResumeJobClicked: notificationItem.resumeJobClicked()
onKillJobClicked: notificationItem.killJobClicked()
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked(action)
}
}
// notification actions
NotificationFooterActions {
Layout.fillWidth: true
Layout.topMargin: Kirigami.Units.smallSpacing
notification: notificationItem
onTakeFocus: notificationItem.takeFocus()
popupNotification: true
}
// thumbnails
Loader {
id: thumbnailStripLoader
Layout.topMargin: Kirigami.Units.largeSpacing
Layout.fillWidth: true
active: notificationItem.urls.length > 0
visible: active
asynchronous: true
sourceComponent: ThumbnailStrip {
leftPadding: -thumbnailStripLoader.Layout.leftMargin
rightPadding: -thumbnailStripLoader.Layout.rightMargin
topPadding: -notificationItem.thumbnailTopPadding
bottomPadding: -thumbnailStripLoader.Layout.bottomMargin
urls: notificationItem.urls
onOpenUrl: notificationItem.openUrl(url)
onFileActionInvoked: notificationItem.fileActionInvoked(action)
}
}
}
}
}

View file

@ -22,12 +22,15 @@
#include <QFile>
#include <QProcess>
#include <QTextDocumentFragment>
#include <QtWaylandClient/private/qwaylandwindow_p.h>
#define FORMAT24H "HH:mm:ss"
ShellUtil::ShellUtil(QObject *parent)
: QObject{parent}
, m_localeConfig{KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig)}
{
ShellUtil::ShellUtil(QObject *parent) : QObject{parent}, m_localeConfig {
KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig) } {
}
void ShellUtil::stackItemBefore(QQuickItem *item1, QQuickItem *item2)
@ -102,3 +105,44 @@ void ShellUtil::setInputTransparent(QQuickWindow *window, bool transparent) {
}
}
void ShellUtil::setInputRegion(QWindow *window, const QRect &region) {
auto waylandWindow = dynamic_cast<QtWaylandClient::QWaylandWindow *>(window->handle());
if (!waylandWindow) {
qWarning() << "Failed to retrieve Wayland window handle.";
return;
}
auto waylandDisplay = dynamic_cast<QtWaylandClient::QWaylandDisplay *>(waylandWindow->display());
if (!waylandDisplay) {
qWarning() << "Failed to retrieve Wayland display.";
return;
}
wl_compositor *compositorResource = static_cast<wl_compositor *>(waylandDisplay->compositor()->object());
if (!compositorResource) {
qWarning() << "Failed to retrieve compositor.";
return;
}
wl_surface *surface = waylandWindow->wlSurface();
if (!surface) {
qWarning() << "Failed to retrieve Wayland surface.";
return;
}
if (region.isEmpty()) {
wl_surface_set_input_region(surface, nullptr);
} else {
wl_region *inputRegion = wl_compositor_create_region(compositorResource);
wl_region_add(inputRegion, region.x(), region.y(), region.width(), region.height());
wl_surface_set_input_region(surface, inputRegion);
wl_region_destroy(inputRegion);
}
wl_surface_commit(surface);
}
QString ShellUtil::toPlainText(QString htmlString) {
return QTextDocumentFragment::fromHtml(htmlString).toPlainText();
}

View file

@ -71,6 +71,18 @@ public:
*/
Q_INVOKABLE void setInputTransparent(QQuickWindow *window, bool transparent);
/**
* Sets a region where inputs will get registered on a window.
* Inputs outside the region will pass through to the surface below.
* Set this to empty to fill the whole window again.
*/
Q_INVOKABLE void setInputRegion(QWindow *window, const QRect &region);
/**
* Converts rich text to plain text.
*/
Q_INVOKABLE QString toPlainText(QString htmlString);
Q_SIGNALS:
void isSystem24HourFormatChanged();

View file

@ -95,6 +95,8 @@ ContainmentItem {
// HACK: we need to initialize the DBus server somewhere, it might as well be here...
// initialize the volume osd, and volume keys
MobileShell.VolumeOSDProviderLoader.load();
// initialize notification popups
MobileShell.NotificationPopupProviderLoader.load();
}
MobileShell.StartupFeedbackPanelFill {

View file

@ -3,5 +3,4 @@
const panel = new Panel("org.kde.plasma.mobile.panel");
panel.location = "top";
panel.addWidget("org.kde.plasma.notifications");
panel.height = 1.25 * gridUnit; // HACK: supposed to be gridUnit + smallSpacing, but it doesn't seem to give the correct number

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2024 Micah <stanleymicah@proton.me>
// SPDX-License-Identifier: GPL-2.0-or-later
/**
* Configuration updates for Plasma 6.3
* - Mobile popup notifications were added, remove the old ones.
*/
let allPanels = panels();
for (var i = 0; i < allPanels.length; i++) {
if (allPanels[i].type === "org.kde.plasma.mobile.panel") {
let allWidgetIds = allPanels[i].widgetIds;
for (var w = 0; w < allWidgetIds.length; w++) {
let widget = allPanels[i].widgetById(allWidgetIds[w]);
if (widget.type === "org.kde.plasma.notifications") {
widget.remove();
}
}
}
}

View file

@ -67,7 +67,7 @@ void LowUrgencyNotificationTest::sendNotification(QCoreApplication &app)
notification->setIconName(QStringLiteral("notification-inactive"));
notification->setTitle("Low Urgency Notification");
notification->setText("This is not very important...");
notification->setUrgency(KNotification::CriticalUrgency);
notification->setUrgency(KNotification::LowUrgency);
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
notification->sendEvent();
@ -80,7 +80,7 @@ void HighUrgencyNotificationTest::sendNotification(QCoreApplication &app)
notification->setIconName(QStringLiteral("notification-active"));
notification->setTitle("Urgent Notification");
notification->setText("This is very urgent! AAAAAA");
notification->setUrgency(KNotification::CriticalUrgency);
notification->setUrgency(KNotification::HighUrgency);
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
notification->sendEvent();