From c98690fa63d15698b2f30e8966c9373cfc5a2673 Mon Sep 17 00:00:00 2001 From: Kat Inskip Date: Sun, 7 Dec 2025 18:41:31 -0800 Subject: [PATCH] feat: notifications o: --- quickshell/Components/NotificationDisplay.qml | 296 ++++++++++++++++++ quickshell/Components/SystemTrayButton.qml | 5 +- quickshell/Components/SystemTrayWrapper.qml | 14 +- quickshell/DataSources/Notifications.qml | 38 +++ quickshell/Modules/Bar.qml | 1 + 5 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 quickshell/Components/NotificationDisplay.qml create mode 100644 quickshell/DataSources/Notifications.qml diff --git a/quickshell/Components/NotificationDisplay.qml b/quickshell/Components/NotificationDisplay.qml new file mode 100644 index 00000000..fc34d0bb --- /dev/null +++ b/quickshell/Components/NotificationDisplay.qml @@ -0,0 +1,296 @@ +import Quickshell +import Quickshell.Widgets +import Quickshell.Io +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import "root:/DataSources" +import "root:/Helpers" +import Quickshell.Services.Notifications + +Item { + id: root + Layout.alignment: Qt.AlignVCenter; + implicitWidth: 25 + implicitHeight: parent.height + Rectangle { + anchors.centerIn: parent + id: rootContainer + color: "transparent" + width: 30 + height: 30 + radius: 50 + Text { + id: rootIcon + text: "" + color: Stylix.base05 + anchors.centerIn: parent + } + } + function updateDisplay() { + if (Notifications.list.length > 0) { + rootContainer.color = Stylix.base08 + rootIcon.color = Stylix.base00 + } else { + rootContainer.color = "transparent" + rootIcon.color = Stylix.base05 + } + } + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: root.updateDisplay() + } + MouseArea { + id: ma + anchors.fill: parent + hoverEnabled: true + + onClicked: function(mouseEvent) { + var m = root.QsWindow.mapFromItem(ma, ma.width/2.0, ma.height/2.0); + var offset = notificationLoader.item.width / 2.0; + notificationLoader.item.clicky = m.x - offset; + notificationLoader.item.visible = !notificationLoader.item.visible + } + } + + LazyLoader { + id: notificationLoader + + loading: true + + PopupWindow { + property real clicky + id: wrapperPopup + visible: false + anchor.window: root.QsWindow.window + anchor.rect.y: parentWindow.height + anchor.rect.x: clicky + color: "transparent" + + implicitWidth: 400 + implicitHeight: 600 + + Rectangle { + anchors.fill: parent + color: Stylix.base01 + bottomLeftRadius: 5 + bottomRightRadius: 5 + ColumnLayout { + anchors.fill: parent + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 5 + Text { + Layout.preferredHeight: 26 + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignVCenter + text: "Notifications" + color: Stylix.base05 + font.pixelSize: 16 + } + Text { + Layout.preferredHeight: 26 + Layout.alignment: Qt.AlignVCenter + verticalAlignment: Text.AlignBottom + id: clear + text: "󱏧" + color: Stylix.base08 + font.pixelSize: 16 + ToolTip { + id: clearTooltip + visible: false + delay: 500 + timeout: 1000 + text: "Clear notifications" + } + MouseArea { + anchors.fill: parent + onClicked: { + if (Notifications.list.length >= 0) { + Notifications.clear() + root.updateDisplay() + } + } + } + + HoverHandler { + id: clearHover + onHoveredChanged: { + clearTooltip.visible = hovered + } + } + } + } + ListView { + id: notificationList + model: Notifications.list + spacing: 10 + ScrollBar.vertical: ScrollBar {} + Layout.alignment: Qt.AlignCenter + Layout.preferredWidth: parent.width + Layout.preferredHeight: parent.height + + delegate: Item { + required property Notification modelData + + height: 100 + width: 400//notificationList.width + + Rectangle { + id: indivNotif + anchors { + fill: parent + leftMargin: 5 + rightMargin: 5 + } + color: Stylix.base02 + ColumnLayout { + anchors { + fill: parent + leftMargin: 5 + rightMargin: 5 + } + RowLayout { + spacing: 5 + ClippingWrapperRectangle { + radius: 5 + Layout.minimumWidth: 0 + Layout.minimumHeight: 0 + Layout.maximumWidth: 60 + Layout.preferredWidth: 60 + Layout.preferredHeight: 60 + Layout.leftMargin: 5 + Layout.rightMargin: 5 + visible: modelData.image != "" + Image { + fillMode: Image.PreserveAspectCrop + Layout.minimumWidth: 0 + Layout.minimumHeight: 0 + Layout.preferredWidth: 60 + Layout.preferredHeight: 60 + source: modelData.image + } + } + ColumnLayout { + spacing: 5 + RowLayout { + spacing: 5 + IconImage { + function getIcon() { + console.log(modelData.appIcon) + if (modelData.appIcon != "") { + return Quickshell.iconPath(modelData.appIcon) + } else { + return iconForId(modelData.appName) + } + } + width: 24 + height: 24 + visible: modelData.appIcon != "" + source: Quickshell.iconPath(modelData.appIcon) + } + Text { + elide: Text.ElideRight + text: modelData.summary + color: Stylix.base05 + } + Text { + id: dismiss + text: "󱏩" + color: Stylix.base08 + font.pixelSize: 16 + + ToolTip { + id: dismissTooltip + visible: false + delay: 500 + timeout: 1000 + text: "Dismiss notification" + } + + HoverHandler { + id: dismissHover + onHoveredChanged: { + dismissTooltip.visible = hovered + } + } + + Layout.topMargin: 5 + Layout.rightMargin: 10 + + MouseArea { + anchors.fill: parent + onClicked: { + modelData.dismiss(); + if (Notifications.list.length <= 0) { + popup.visible = false; + } + } + } + } + } + Text { + font.pointSize: 10 + wrapMode: Text.WordWrap + elide: Text.ElideRight + text: modelData.body + color: Stylix.base05 + } + } + } + RowLayout { + Layout.minimumHeight: 0 + visible: modelData.actions != [] + spacing: 5 + Repeater { + model: modelData.actions + + Item { + required property NotificationAction actionData + + width: 400 + height: 30 + + anchors { + left: parent.left + leftMargin: 5 + top: parent.top + topMargin: 5 + } + + Rectangle { + anchors.fill: parent + color: Stylix.base02 + radius: 5 + + Text { + text: actionData.text + color: Stylix.base05 + font.pixelSize: 12 + + anchors { + left: parent.left + leftMargin: 10 + verticalCenter: parent.verticalCenter + } + } + + MouseArea { + anchors.fill: parent + onClicked: actionData.invoke() + } + } + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/quickshell/Components/SystemTrayButton.qml b/quickshell/Components/SystemTrayButton.qml index 82a189fe..c1e03578 100644 --- a/quickshell/Components/SystemTrayButton.qml +++ b/quickshell/Components/SystemTrayButton.qml @@ -12,9 +12,6 @@ Item { width: parent.width height: 30 - property real text_point_size: 12 - property real length: width / text_point_size - Rectangle { anchors { fill: parent @@ -33,7 +30,7 @@ Item { horizontalAlignment: Text.AlignHCenter text: modelData?.text ?? "" color: Stylix.base05 - font.pointSize: text_point_size + font.pointSize: 12 elide: Text.ElideRight } diff --git a/quickshell/Components/SystemTrayWrapper.qml b/quickshell/Components/SystemTrayWrapper.qml index 3fa6801b..f5f78a83 100644 --- a/quickshell/Components/SystemTrayWrapper.qml +++ b/quickshell/Components/SystemTrayWrapper.qml @@ -8,10 +8,11 @@ Item { Layout.alignment: Qt.AlignVCenter; implicitWidth: 25 implicitHeight: parent.height + property list textStates: ["", ""] Text { id: texty anchors.centerIn: parent - text: "" + text: textStates[0] color: Stylix.base05 } MouseArea { @@ -23,7 +24,9 @@ Item { var m = root.QsWindow.mapFromItem(ma, ma.width/2.0, ma.height/2.0); var offset = wrapperPopup.width / 2.0; wrapperPopup.clicky = m.x - offset; - wrapperPopup.visible = !wrapperPopup.visible + wrapperPopup.visible = !wrapperPopup.visible; + + texty.text = root.textStates[wrapperPopup.visible ? 1 : 0]; } } PopupWindow { @@ -32,11 +35,14 @@ Item { anchor.window: root.QsWindow.window anchor.rect.y: parentWindow.height anchor.rect.x: clicky - width: systray.width + 10 - height: systray.height + 10 + implicitWidth: systray.width + 10 + implicitHeight: systray.height + 10 + color: "transparent" Rectangle { anchors.fill: parent color: Stylix.base01 + bottomLeftRadius: 5 + bottomRightRadius: 5 SystemTray { id: systray } diff --git a/quickshell/DataSources/Notifications.qml b/quickshell/DataSources/Notifications.qml new file mode 100644 index 00000000..323e5d1a --- /dev/null +++ b/quickshell/DataSources/Notifications.qml @@ -0,0 +1,38 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications + +Singleton { + id: root + + NotificationServer { + id: notificationServer + imageSupported: true + bodySupported: true + bodyMarkupSupported: false + bodyImagesSupported: false + actionsSupported: true + onNotification: (notification) => { + notification.tracked = true; + root.notification(notification); + } + } + function clear(): void { + for (const notification of notificationServer.trackedNotifications.values) { + notification.tracked = false; + } + } + + // TODO: use signal + property list list: notificationServer.trackedNotifications.values.filter(notification => notification.tracked) + signal notification(Notification notification) + + IpcHandler { + target: "notifications" + function clear() { + root.clear() + } + } +} diff --git a/quickshell/Modules/Bar.qml b/quickshell/Modules/Bar.qml index 74cfd7eb..02a19c89 100644 --- a/quickshell/Modules/Bar.qml +++ b/quickshell/Modules/Bar.qml @@ -70,6 +70,7 @@ Scope { spacing: 15 + NotificationDisplay {} SystemTrayWrapper {} Clock {} DistroIcon {}