mirror of
https://invent.kde.org/marcoa/shift-shell.git
synced 2026-04-26 06:13: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
|
||||
Gui
|
||||
Sensors
|
||||
WaylandClient
|
||||
)
|
||||
|
||||
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/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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ PlasmaComponents.Label {
|
|||
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: TextEdit.RichText
|
||||
wrapMode: Text.WordWrap
|
||||
textFormat: TextEdit.PlainText
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <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 ®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);
|
||||
|
||||
/**
|
||||
* 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:
|
||||
void isSystem24HourFormatChanged();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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->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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue