[network/kdeconnect-kde] /: Added the Notification Panel to Desktop application

Albert Vaca Cintora <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit cc417f5783812547f7653a06ceb7aafdb82c950f by Albert Vaca Cintora, on behalf of Shreyas Dutta.
Committed on 26/07/2026 at 20:39.
Pushed by albertvaka into branch 'master'.

Added the Notification Panel to Desktop application

M  +1    -0    app/CMakeLists.txt
M  +8    -0    app/qml/DevicePage.qml
A  +302  -0    app/qml/NotificationPanel.qml     [License: GPL(3+eV) GPL(v3.0) GPL(v2.0)]
M  +12   -0    declarativeplugin/kdeconnectdeclarativeplugin.h

https://invent.kde.org/network/kdeconnect-kde/-/commit/cc417f5783812547f7653a06ceb7aafdb82c950f

diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt
index aa2f253df..d9f565845 100644
--- a/app/CMakeLists.txt
+++ b/app/CMakeLists.txt
@@ -41,6 +41,7 @@ ecm_target_qml_sources(kdeconnect-app SOURCES
     qml/PluginSettings.qml
     qml/Settings.qml
     qml/WelcomePage.qml
+    qml/NotificationPanel.qml
 )
 
 target_link_libraries(kdeconnect-app PRIVATE Qt::Quick Qt::QuickControls2 Qt::Widgets KF6::CoreAddons KF6::I18n KF6::ColorScheme KF6::Crash KF6::ItemModels KF6::DBusAddons KF6::WindowSystem)
diff --git a/app/qml/DevicePage.qml b/app/qml/DevicePage.qml
index 95e3807fd..c5c4a9c51 100644
--- a/app/qml/DevicePage.qml
+++ b/app/qml/DevicePage.qml
@@ -145,6 +145,14 @@ Kirigami.ScrollablePage {
                 section: "control"
                 device: root.currentDevice
             },
+            PluginItem {
+                name: i18nd("kdeconnect-app", "Notifications")
+                interfaceFactory: DeviceNotificationsDbusInterfaceFactory
+                component: "NotificationPanel.qml"
+                pluginName: "notifications"
+                section: "control"
+                device: root.currentDevice
+            },
             PluginItem {
                 name: i18nd("kdeconnect-app", "Remote input")
                 interfaceFactory: RemoteControlDbusInterfaceFactory
diff --git a/app/qml/NotificationPanel.qml b/app/qml/NotificationPanel.qml
new file mode 100644
index 000000000..2eb088f40
--- /dev/null
+++ b/app/qml/NotificationPanel.qml
@@ -0,0 +1,302 @@
+/*
+ * SPDX-FileCopyrightText: 2019 Nicolas Fella <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
+ */
+
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.kde.kdeconnect
+import org.kde.kdeconnect as KDEConnect
+import org.kde.kirigami as Kirigami
+import org.kde.kitemmodels as KItemModels
+
+Kirigami.ScrollablePage {
+    id: page
+
+    required property var pluginInterface
+    required property var device
+
+    title: i18nc("@title:window", "Active Notifications")
+    Component.onCompleted: {
+        console.log("device =", device.id());
+        console.log("pluginInterface =", pluginInterface);
+    }
+
+    Column {
+        Repeater {
+            id: notificationsView
+
+            model: KDEConnect.NotificationsModel {
+                id: notificationsModel
+
+                deviceId: device.id()
+            }
+
+            // Notification item
+            delegate: ItemDelegate {
+                id: listitem
+
+                required property int index
+                required property var model
+                // Control animation suppression (for instant collapsing)
+                property bool _suppressAnimations: false
+                property bool replying: false
+
+                hoverEnabled: true
+                width: parent.width
+                height: implicitHeight
+                clip: true
+                topPadding: index != 0 ? Kirigami.Units.mediumSpacing * 2 : 0
+                bottomPadding: index != notificationsModel.count - 1 ? Kirigami.Units.mediumSpacing * 2 : 0
+                leftPadding: LayoutMirroring.enabled ? 0 : Kirigami.Units.smallSpacing
+                rightPadding: LayoutMirroring.enabled ? Kirigami.Units.smallSpacing : 0
+                Layout.margins: 0
+                Kirigami.Theme.colorSet: Kirigami.Theme.View
+                Kirigami.Theme.inherit: false
+                enabled: true
+                onClicked: checked = !checked
+
+                // Dismiss with animation
+                SequentialAnimation {
+                    id: dismissAnimation
+
+                    NumberAnimation {
+                        target: listitem
+                        property: "x"
+                        to: listitem.LayoutMirroring.enabled ? -width : width
+                        duration: Kirigami.Units.shortDuration
+                    }
+
+                    ScriptAction {
+                        script: listitem.model.dbusInterface.dismiss()
+                    }
+
+                }
+
+                // Timer to delay collapse until list animation finishes
+                Timer {
+                    id: collapseDelayTimer
+
+                    interval: Kirigami.Units.shortDuration // Match the list collapse duration
+                    onTriggered: {
+                        listitem._suppressAnimations = true;
+                        listitem.checked = false;
+                        listitem._suppressAnimations = false;
+                    }
+                }
+
+                // Collapse if notification list is collapsed
+                // Watch for list expansion state changes
+                Connections {
+                    function onNotificationsExpandedChanged() {
+                        if (!root.notificationsExpanded)
+                            collapseDelayTimer.start();
+                        else
+                            collapseDelayTimer.stop();
+                    }
+
+                    target: root
+                }
+
+                Kirigami.Separator {
+                    anchors.bottom: parent.bottom
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    width: parent.width - deviceHeaderContainer.leftPadding - deviceHeaderContainer.rightPadding
+                    opacity: 0.5
+                    visible: index < notificationsModel.count - 1
+                }
+
+                Behavior on height {
+                    enabled: !listitem._suppressAnimations
+
+                    NumberAnimation {
+                        duration: Kirigami.Units.shortDuration
+                        easing.type: Easing.InOutQuad
+                    }
+
+                }
+
+                background: Item {
+                }
+
+                contentItem: ColumnLayout {
+                    spacing: Kirigami.Units.smallSpacing
+
+                    RowLayout {
+                        spacing: Kirigami.Units.largeSpacing
+                        Layout.alignment: Qt.AlignTop
+
+                        ColumnLayout {
+                            Layout.alignment: Qt.AlignTop
+
+                            RowLayout {
+                                id: notificationHead
+
+                                spacing: Kirigami.Units.largeSpacing
+                                Layout.alignment: Qt.AlignTop
+
+                                ColumnLayout {
+                                    // Couple together so Dismiss and Reply have enough room
+                                    spacing: Kirigami.Units.smallSpacing
+
+                                    // Notification and icon
+                                    RowLayout {
+                                        Layout.alignment: Qt.AlignVCenter
+
+                                        Kirigami.Icon {
+                                            id: notificationIcon
+
+                                            source: listitem.model.appIcon
+                                            Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
+                                            Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
+                                            Layout.alignment: Qt.AlignVCenter
+                                        }
+
+                                        Kirigami.Heading {
+                                            id: notificationAppName
+
+                                            text: listitem.model.appName
+                                            level: 5
+                                            elide: listitem.checked ? Text.ElideNone : Text.ElideRight
+                                            maximumLineCount: listitem.checked ? 0 : 1
+                                            wrapMode: Text.Wrap
+                                            Layout.alignment: Qt.AlignVCenter
+                                            Layout.fillWidth: true
+                                            color: Kirigami.ColorUtils.tintWithAlpha(Kirigami.Theme.backgroundColor, Kirigami.Theme.textColor, 0.85)
+                                        }
+
+                                    }
+
+                                    // Notification title
+                                    Kirigami.Heading {
+                                        id: notificationTitle
+
+                                        text: listitem.model.title
+                                        level: 2
+                                        type: Kirigami.Heading.Type.Primary
+                                        visible: text.length > 0
+                                        elide: listitem.checked ? Text.ElideNone : Text.ElideRight
+                                        maximumLineCount: listitem.checked ? 0 : 1
+                                        wrapMode: Text.Wrap
+                                        Layout.fillWidth: true
+                                    }
+
+                                }
+
+                                ToolButton {
+                                    id: replyButton
+
+                                    visible: listitem.model.repliable
+                                    enabled: listitem.model.repliable && !listitem.replying
+                                    icon.name: "mail-reply-sender"
+                                    icon.width: Kirigami.Units.iconSizes.smallMedium
+                                    icon.height: Kirigami.Units.iconSizes.smallMedium
+                                    ToolTip.text: i18n("Reply")
+                                    onClicked: {
+                                        listitem.replying = true;
+                                        replyTextField.forceActiveFocus();
+                                    }
+                                    Layout.alignment: Qt.AlignTop
+                                }
+
+                                ToolButton {
+                                    id: dismissButton
+
+                                    visible: notificationsModel.isAnyDimissable
+                                    enabled: listitem.model.dismissable
+                                    Layout.alignment: Qt.AlignTop
+                                    icon.name: "window-close"
+                                    icon.width: Kirigami.Units.iconSizes.smallMedium
+                                    icon.height: Kirigami.Units.iconSizes.smallMedium
+                                    ToolTip.text: i18n("Dismiss")
+                                    onClicked: dismissAnimation.start()
+                                }
+
+                            }
+
+                            Label {
+                                id: notificationNotitext
+
+                                text: listitem.model.notitext
+                                visible: text.length > 0
+                                elide: listitem.checked ? Text.ElideNone : Text.ElideRight
+                                maximumLineCount: listitem.checked ? 0 : 1
+                                wrapMode: Text.Wrap
+                                Layout.fillWidth: true
+                            }
+
+                        }
+
+                    }
+
+                    RowLayout {
+                        visible: listitem.replying
+                        spacing: Kirigami.Units.smallSpacing
+                        Layout.topMargin: Kirigami.Units.smallSpacing
+                        Layout.fillWidth: true
+
+                        ToolButton {
+                            id: replyCancelButton
+
+                            Layout.alignment: Qt.AlignBottom
+                            text: i18n("Cancel")
+                            padding: replyTextField.padding
+                            display: AbstractButton.IconOnly
+                            icon.name: "dialog-cancel"
+                            onClicked: {
+                                replyTextField.text = "";
+                                listitem.replying = false;
+                            }
+
+                            ToolTip {
+                                text: replyCancelButton.text
+                            }
+
+                        }
+
+                        TextArea {
+                            id: replyTextField
+
+                            placeholderText: i18nc("@info:placeholder", "Reply to %1…", listitem.model.appName)
+                            wrapMode: TextEdit.Wrap
+                            Layout.fillWidth: true
+                            Keys.onPressed: (event) => {
+                                if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
+                                    replySendButton.clicked();
+                                    event.accepted = true;
+                                }
+                                if (event.key === Qt.Key_Escape) {
+                                    replyCancelButton.clicked();
+                                    event.accepted = true;
+                                }
+                            }
+                        }
+
+                        ToolButton {
+                            id: replySendButton
+
+                            Layout.alignment: Qt.AlignBottom
+                            text: i18n("Send")
+                            padding: replyTextField.padding
+                            icon.name: LayoutMirroring.enabled ? "document-send-rtl" : "document-send"
+                            enabled: replyTextField.text !== ""
+                            onClicked: {
+                                listitem.model.dbusInterface.sendReply(replyTextField.text);
+                                replyTextField.text = "";
+                                listitem.replying = false;
+                            }
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+}
diff --git a/declarativeplugin/kdeconnectdeclarativeplugin.h b/declarativeplugin/kdeconnectdeclarativeplugin.h
index 1cd0f8a3c..c0ab263ec 100644
--- a/declarativeplugin/kdeconnectdeclarativeplugin.h
+++ b/declarativeplugin/kdeconnectdeclarativeplugin.h
@@ -229,6 +229,18 @@ class OpenConfigForeign
     QML_FOREIGN(OpenConfig)
 };
 
+class DeviceNotificationsDbusInterfaceFactory : public QObject
+{
+    Q_OBJECT
+    QML_ELEMENT
+    QML_SINGLETON
+public:
+    Q_INVOKABLE DeviceNotificationsDbusInterface *create(const QString &deviceId)
+    {
+        return new DeviceNotificationsDbusInterface(deviceId);
+    }
+};
+
 class DeviceDbusInterfaceFactory : public QObject
 {
     Q_OBJECT
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.