mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 14:23:09 +00:00
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:  Multiple popup notifications:  Multiple popup notifications in the expanded view:  Any feedback would be greatly appreciated.
This commit is contained in:
parent
e66d88a754
commit
eba6074161
18 changed files with 1372 additions and 23 deletions
|
|
@ -54,6 +54,7 @@ find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED
|
||||||
Quick
|
Quick
|
||||||
Gui
|
Gui
|
||||||
Sensors
|
Sensors
|
||||||
|
WaylandClient
|
||||||
)
|
)
|
||||||
|
|
||||||
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
find_package(KF6 ${KF6_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
|
|
|
||||||
|
|
@ -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/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/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/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)
|
set_source_files_properties(qml/volumeosd/VolumeOSDProviderLoader.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE)
|
||||||
|
|
||||||
# Include qml and js files within ./qml/
|
# Include qml and js files within ./qml/
|
||||||
|
|
@ -41,6 +42,7 @@ target_link_libraries(mobileshellplugin
|
||||||
Qt::Qml
|
Qt::Qml
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
|
Qt::WaylandClientPrivate
|
||||||
KF6::KIOGui
|
KF6::KIOGui
|
||||||
Plasma::Plasma
|
Plasma::Plasma
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -19,9 +19,9 @@ import org.kde.coreaddons 1.0 as KCoreAddons
|
||||||
Item {
|
Item {
|
||||||
id: notificationItem
|
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
|
* Whether the notification is allowed to invoke any action, or if it should instead
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ PlasmaComponents.Label {
|
||||||
|
|
||||||
maximumLineCount: 3
|
maximumLineCount: 3
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.WordWrap
|
||||||
textFormat: TextEdit.RichText
|
textFormat: TextEdit.PlainText
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import QtQuick.Controls
|
||||||
import Qt5Compat.GraphicalEffects
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
import org.kde.kirigami 2.12 as Kirigami
|
import org.kde.kirigami 2.12 as Kirigami
|
||||||
|
|
@ -14,6 +15,16 @@ Item {
|
||||||
|
|
||||||
default property Item contentItem
|
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 tapEnabled: false
|
||||||
|
|
||||||
property bool swipeGestureEnabled: false
|
property bool swipeGestureEnabled: false
|
||||||
|
|
@ -23,6 +34,8 @@ Item {
|
||||||
signal tapped()
|
signal tapped()
|
||||||
signal dismissRequested()
|
signal dismissRequested()
|
||||||
signal configureClicked() // TODO implement settings button
|
signal configureClicked() // TODO implement settings button
|
||||||
|
signal dragStart()
|
||||||
|
signal dragEnd()
|
||||||
|
|
||||||
onContentItemChanged: {
|
onContentItemChanged: {
|
||||||
contentItem.parent = contentParent;
|
contentItem.parent = contentParent;
|
||||||
|
|
@ -52,7 +65,7 @@ Item {
|
||||||
blurMax: 16
|
blurMax: 16
|
||||||
shadowEnabled: true
|
shadowEnabled: true
|
||||||
shadowVerticalOffset: 1
|
shadowVerticalOffset: 1
|
||||||
shadowOpacity: 0.5
|
shadowOpacity: popupNotification ? 0.85 : 0.5
|
||||||
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
|
shadowColor: Qt.lighter(Kirigami.Theme.backgroundColor, 0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +79,7 @@ Item {
|
||||||
|
|
||||||
color: {
|
color: {
|
||||||
let darkerBackgroundColor = Qt.darker(Kirigami.Theme.backgroundColor, 1.3);
|
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
|
radius: Kirigami.Units.cornerRadius
|
||||||
}
|
}
|
||||||
|
|
@ -82,18 +95,65 @@ Item {
|
||||||
anchors.rightMargin: root.dragOffset < 0 ? -root.dragOffset : 0
|
anchors.rightMargin: root.dragOffset < 0 ? -root.dragOffset : 0
|
||||||
anchors.top: parent.top
|
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 {
|
Rectangle {
|
||||||
anchors.fill: parent
|
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
|
radius: Kirigami.Units.cornerRadius
|
||||||
layer.enabled: true
|
layer.enabled: popupNotification ? false : true
|
||||||
layer.effect: MultiEffect {
|
layer.effect: MultiEffect {
|
||||||
brightness: 0.075
|
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
|
// clip
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
|
|
||||||
|
|
@ -124,6 +184,7 @@ Item {
|
||||||
id: dragHandler
|
id: dragHandler
|
||||||
enabled: root.swipeGestureEnabled
|
enabled: root.swipeGestureEnabled
|
||||||
yAxis.enabled: false
|
yAxis.enabled: false
|
||||||
|
xAxis.enabled: !inPopupDrawer
|
||||||
|
|
||||||
property real startDragOffset: 0
|
property real startDragOffset: 0
|
||||||
property real startPosition: 0
|
property real startPosition: 0
|
||||||
|
|
@ -143,6 +204,7 @@ Item {
|
||||||
startActive = active;
|
startActive = active;
|
||||||
|
|
||||||
if (!active) { // release event
|
if (!active) { // release event
|
||||||
|
root.dragEnd()
|
||||||
let threshold = Kirigami.Units.gridUnit * 5; // drag threshold
|
let threshold = Kirigami.Units.gridUnit * 5; // drag threshold
|
||||||
if (root.dragOffset > threshold) {
|
if (root.dragOffset > threshold) {
|
||||||
dragAnim.to = root.width;
|
dragAnim.to = root.width;
|
||||||
|
|
@ -152,6 +214,8 @@ Item {
|
||||||
dragAnim.to = 0;
|
dragAnim.to = 0;
|
||||||
}
|
}
|
||||||
dragAnim.restart();
|
dragAnim.restart();
|
||||||
|
} else {
|
||||||
|
root.dragStart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@ Item {
|
||||||
id: actionContainer
|
id: actionContainer
|
||||||
|
|
||||||
required property BaseNotificationItem notification
|
required property BaseNotificationItem notification
|
||||||
|
property bool popupNotification: false
|
||||||
|
|
||||||
implicitHeight: Math.max(actionFlow.implicitHeight, replyLoader.height)
|
implicitHeight: Math.max(actionFlow.implicitHeight, replyLoader.height)
|
||||||
visible: actionRepeater.count > 0
|
visible: actionRepeater.count > 0
|
||||||
|
|
||||||
|
signal takeFocus()
|
||||||
|
|
||||||
Flow {
|
Flow {
|
||||||
id: actionFlow
|
id: actionFlow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
@ -63,6 +66,8 @@ Item {
|
||||||
flat: false
|
flat: false
|
||||||
text: modelData.label || ""
|
text: modelData.label || ""
|
||||||
|
|
||||||
|
visible: !(notificationItem.hasReplyAction && actionContainer.popupNotification)
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (modelData.actionName === "inline-reply") {
|
if (modelData.actionName === "inline-reply") {
|
||||||
replyLoader.beginReply();
|
replyLoader.beginReply();
|
||||||
|
|
@ -81,11 +86,13 @@ Item {
|
||||||
height: active ? item.implicitHeight : 0
|
height: active ? item.implicitHeight : 0
|
||||||
|
|
||||||
// When there is only one action and it is a reply action, show text field right away
|
// 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
|
visible: active
|
||||||
opacity: active ? 1 : 0
|
opacity: active ? 1 : 0
|
||||||
x: active ? 0 : parent.width
|
x: active ? 0 : parent.width
|
||||||
|
|
||||||
|
property bool replying: false
|
||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Kirigami.Units.longDuration
|
duration: Kirigami.Units.longDuration
|
||||||
|
|
@ -100,7 +107,9 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginReply() {
|
function beginReply() {
|
||||||
active = true
|
actionContainer.takeFocus();
|
||||||
|
active = true;
|
||||||
|
replying = true;
|
||||||
replyLoader.item.activate();
|
replyLoader.item.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,7 +119,7 @@ Item {
|
||||||
buttonText: notificationItem.replySubmitButtonText
|
buttonText: notificationItem.replySubmitButtonText
|
||||||
onReplied: notificationItem.replied(text)
|
onReplied: notificationItem.replied(text)
|
||||||
|
|
||||||
replying: replyLoader.active
|
replying: replyLoader.replying
|
||||||
onBeginReplyRequested: replyLoader.beginReply()
|
onBeginReplyRequested: replyLoader.beginReply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,10 @@ BaseNotificationItem {
|
||||||
|
|
||||||
property bool inLockscreen: false
|
property bool inLockscreen: false
|
||||||
|
|
||||||
|
signal dragStart()
|
||||||
|
signal dragEnd()
|
||||||
|
signal takeFocus()
|
||||||
|
|
||||||
// notification heading for groups with one element
|
// notification heading for groups with one element
|
||||||
NotificationGroupHeader {
|
NotificationGroupHeader {
|
||||||
id: notificationHeading
|
id: notificationHeading
|
||||||
|
|
@ -57,16 +61,19 @@ BaseNotificationItem {
|
||||||
tapEnabled: notificationItem.hasDefaultAction
|
tapEnabled: notificationItem.hasDefaultAction
|
||||||
onTapped: notificationItem.actionInvoked("default");
|
onTapped: notificationItem.actionInvoked("default");
|
||||||
swipeGestureEnabled: notificationItem.closable
|
swipeGestureEnabled: notificationItem.closable
|
||||||
onDismissRequested: notificationItem.close()
|
onDismissRequested: notificationItem.close();
|
||||||
|
|
||||||
|
onDragStart: notificationItem.dragStart()
|
||||||
|
onDragEnd: notificationItem.dragEnd()
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: column
|
id: column
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
|
|
||||||
// notification summary row
|
// notification summary row
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.alignment: Qt.AlignRight
|
||||||
|
Layout.fillWidth: summaryLabel.visible
|
||||||
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
Layout.bottomMargin: Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
// notification summary
|
// notification summary
|
||||||
|
|
@ -107,7 +114,7 @@ BaseNotificationItem {
|
||||||
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
|
||||||
Layout.preferredWidth: column.width - iconContainer.width - Kirigami.Units.smallSpacing
|
Layout.preferredWidth: column.width - iconContainer.width - Kirigami.Units.smallSpacing
|
||||||
|
|
||||||
text: notificationItem.body
|
text: ShellUtil.toPlainText(notificationItem.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// notification icon
|
// notification icon
|
||||||
|
|
@ -165,6 +172,7 @@ BaseNotificationItem {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.topMargin: Kirigami.Units.smallSpacing
|
Layout.topMargin: Kirigami.Units.smallSpacing
|
||||||
notification: notificationItem
|
notification: notificationItem
|
||||||
|
onTakeFocus: notificationItem.takeFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
// thumbnails
|
// thumbnails
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,12 +22,15 @@
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
|
||||||
|
#include <QTextDocumentFragment>
|
||||||
|
#include <QtWaylandClient/private/qwaylandwindow_p.h>
|
||||||
|
|
||||||
|
|
||||||
#define FORMAT24H "HH:mm:ss"
|
#define FORMAT24H "HH:mm:ss"
|
||||||
|
|
||||||
ShellUtil::ShellUtil(QObject *parent)
|
|
||||||
: QObject{parent}
|
ShellUtil::ShellUtil(QObject *parent) : QObject{parent}, m_localeConfig {
|
||||||
, m_localeConfig{KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig)}
|
KSharedConfig::openConfig(QStringLiteral("kdeglobals"), KConfig::SimpleConfig) } {
|
||||||
{
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ShellUtil::stackItemBefore(QQuickItem *item1, QQuickItem *item2)
|
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 ®ion) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,18 @@ public:
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void setInputTransparent(QQuickWindow *window, bool transparent);
|
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 ®ion);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts rich text to plain text.
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE QString toPlainText(QString htmlString);
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void isSystem24HourFormatChanged();
|
void isSystem24HourFormatChanged();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,8 @@ ContainmentItem {
|
||||||
// HACK: we need to initialize the DBus server somewhere, it might as well be here...
|
// HACK: we need to initialize the DBus server somewhere, it might as well be here...
|
||||||
// initialize the volume osd, and volume keys
|
// initialize the volume osd, and volume keys
|
||||||
MobileShell.VolumeOSDProviderLoader.load();
|
MobileShell.VolumeOSDProviderLoader.load();
|
||||||
|
// initialize notification popups
|
||||||
|
MobileShell.NotificationPopupProviderLoader.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
MobileShell.StartupFeedbackPanelFill {
|
MobileShell.StartupFeedbackPanelFill {
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,4 @@
|
||||||
|
|
||||||
const panel = new Panel("org.kde.plasma.mobile.panel");
|
const panel = new Panel("org.kde.plasma.mobile.panel");
|
||||||
panel.location = "top";
|
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
|
panel.height = 1.25 * gridUnit; // HACK: supposed to be gridUnit + smallSpacing, but it doesn't seem to give the correct number
|
||||||
|
|
|
||||||
20
shell/contents/updates/6_3_update.js
Normal file
20
shell/contents/updates/6_3_update.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ void LowUrgencyNotificationTest::sendNotification(QCoreApplication &app)
|
||||||
notification->setIconName(QStringLiteral("notification-inactive"));
|
notification->setIconName(QStringLiteral("notification-inactive"));
|
||||||
notification->setTitle("Low Urgency Notification");
|
notification->setTitle("Low Urgency Notification");
|
||||||
notification->setText("This is not very important...");
|
notification->setText("This is not very important...");
|
||||||
notification->setUrgency(KNotification::CriticalUrgency);
|
notification->setUrgency(KNotification::LowUrgency);
|
||||||
|
|
||||||
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
|
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
|
||||||
notification->sendEvent();
|
notification->sendEvent();
|
||||||
|
|
@ -80,7 +80,7 @@ void HighUrgencyNotificationTest::sendNotification(QCoreApplication &app)
|
||||||
notification->setIconName(QStringLiteral("notification-active"));
|
notification->setIconName(QStringLiteral("notification-active"));
|
||||||
notification->setTitle("Urgent Notification");
|
notification->setTitle("Urgent Notification");
|
||||||
notification->setText("This is very urgent! AAAAAA");
|
notification->setText("This is very urgent! AAAAAA");
|
||||||
notification->setUrgency(KNotification::CriticalUrgency);
|
notification->setUrgency(KNotification::HighUrgency);
|
||||||
|
|
||||||
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
|
connect(notification, &KNotification::closed, &app, QCoreApplication::quit);
|
||||||
notification->sendEvent();
|
notification->sendEvent();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue