From 54fdf043c94fef17f2ded993603e5a4523f654ce Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Mon, 21 Apr 2025 23:29:31 +0200 Subject: [PATCH] notification popups --- .config/quickshell/GlobalStates.qml | 8 ++ .config/quickshell/modules/bar/Workspaces.qml | 43 +++---- .../quickshell/modules/common/Appearance.qml | 1 + .../common/widgets/CircularProgress.qml | 17 ++- .../common/widgets/NotificationWidget.qml | 45 ++++++- .../notificationPopup/NotificationPopup.qml | 120 ++++++++++++++++++ .../OnScreenDisplayBrightness.qml | 2 - .../onScreenDisplay/OnScreenDisplayVolume.qml | 2 - .../modules/sidebarRight/SidebarRight.qml | 9 +- .../notifications/NotificationList.qml | 22 ++-- .config/quickshell/services/Notifications.qml | 15 ++- .config/quickshell/shell.qml | 2 + 12 files changed, 240 insertions(+), 46 deletions(-) create mode 100644 .config/quickshell/GlobalStates.qml create mode 100644 .config/quickshell/modules/notificationPopup/NotificationPopup.qml diff --git a/.config/quickshell/GlobalStates.qml b/.config/quickshell/GlobalStates.qml new file mode 100644 index 00000000..e50f7b7b --- /dev/null +++ b/.config/quickshell/GlobalStates.qml @@ -0,0 +1,8 @@ +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + property int sidebarRightOpenCount: 0 +} \ No newline at end of file diff --git a/.config/quickshell/modules/bar/Workspaces.qml b/.config/quickshell/modules/bar/Workspaces.qml index ef4356ef..8e69b94f 100644 --- a/.config/quickshell/modules/bar/Workspaces.qml +++ b/.config/quickshell/modules/bar/Workspaces.qml @@ -163,32 +163,31 @@ Item { Layout.fillHeight: true onPressed: Hyprland.dispatch(`workspace ${index+1}`) width: workspaceButtonWidth - - contentItem: StyledText { - z: 3 - property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspacesShown + index + 1 - - anchors.fill: parent - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - font.pixelSize: Appearance.font.pixelSize.small - text: `${workspaceValue}` - elide: Text.ElideRight - color: (monitor.activeWorkspace?.id == workspaceValue) ? Appearance.m3colors.m3onPrimary : (workspaceOccupied[index] ? Appearance.colors.colOnLayer1 : Appearance.colors.colOnLayer1Inactive) - - Behavior on color { - ColorAnimation { - duration: Appearance.animation.elementDecel.duration - easing.type: Appearance.animation.elementDecel.type - } - - } - - } background: Item { implicitWidth: workspaceButtonWidth implicitHeight: workspaceButtonWidth + StyledText { + z: 3 + property int workspaceValue: workspaceGroup * ConfigOptions.bar.workspacesShown + index + 1 + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10")) + text: `${workspaceValue}` + elide: Text.ElideRight + color: (monitor.activeWorkspace?.id == workspaceValue) ? Appearance.m3colors.m3onPrimary : (workspaceOccupied[index] ? Appearance.colors.colOnLayer1 : Appearance.colors.colOnLayer1Inactive) + + Behavior on color { + ColorAnimation { + duration: Appearance.animation.elementDecel.duration + easing.type: Appearance.animation.elementDecel.type + } + + } + + } } diff --git a/.config/quickshell/modules/common/Appearance.qml b/.config/quickshell/modules/common/Appearance.qml index 048fbbf2..5a0f521d 100644 --- a/.config/quickshell/modules/common/Appearance.qml +++ b/.config/quickshell/modules/common/Appearance.qml @@ -195,6 +195,7 @@ Singleton { property int barCenterSideModuleWidth: 360 property int barPreferredSideSectionWidth: 400 property int sidebarWidth: 450 + property int hyprlandGapsOut: 5 property int elevationMargin: 7 property int fabShadowRadius: 5 diff --git a/.config/quickshell/modules/common/widgets/CircularProgress.qml b/.config/quickshell/modules/common/widgets/CircularProgress.qml index 8fc6a83b..480098db 100644 --- a/.config/quickshell/modules/common/widgets/CircularProgress.qml +++ b/.config/quickshell/modules/common/widgets/CircularProgress.qml @@ -1,7 +1,8 @@ // From https://github.com/rafzby/circular-progressbar // License: LGPL-3.0 - A copy can be found in `licenses` folder of repo // Modified so it looks like in Material 3: https://m3.material.io/components/progress-indicators/specs -import QtQuick 2.9 +import QtQuick +import "root:/modules/common" Item { id: root @@ -9,15 +10,19 @@ Item { property int size: 30 property int lineWidth: 2 property real value: 0 - property color primaryColor: "#70585D" - property color secondaryColor: "#FFF8F7" + property color primaryColor: Appearance.m3colors.m3onSecondaryContainer + property color secondaryColor: Appearance.m3colors.m3secondaryContainer property real gapAngle: Math.PI / 10 property bool fill: false property int fillOverflow: 2 property int animationDuration: 1000 + property var easingType: Easing.OutCubic width: size height: size + + signal animationFinished(); + onValueChanged: { canvas.degree = value * 360; } @@ -62,12 +67,12 @@ Item { // Secondary ctx.beginPath(); - ctx.arc(x, y, radius, progressAngle + gapAngle, startAngle - gapAngle); + ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle); ctx.strokeStyle = root.secondaryColor; ctx.stroke(); // Primary (value indication) - var endAngle = (progressAngle === startAngle) ? startAngle + epsilon : progressAngle; + var endAngle = progressAngle + (value > 0 ? 0 : epsilon); ctx.beginPath(); ctx.arc(x, y, radius, startAngle, endAngle); ctx.strokeStyle = root.primaryColor; @@ -77,7 +82,7 @@ Item { Behavior on degree { NumberAnimation { duration: root.animationDuration - easing.type: Easing.OutCubic + easing.type: root.easingType } } diff --git a/.config/quickshell/modules/common/widgets/NotificationWidget.qml b/.config/quickshell/modules/common/widgets/NotificationWidget.qml index 4ae3ba9c..bef591a9 100644 --- a/.config/quickshell/modules/common/widgets/NotificationWidget.qml +++ b/.config/quickshell/modules/common/widgets/NotificationWidget.qml @@ -13,13 +13,15 @@ import "./notification_utils.js" as NotificationUtils Item { id: root property var notificationObject + property bool popup: false property bool expanded: false property bool enableAnimation: true property int notificationListSpacing: 5 property bool ready: false + property int defaultTimeoutValue: 5000 Layout.fillWidth: true - clip: true + clip: !popup Process { id: closeSidebarProcess @@ -42,6 +44,16 @@ Item { Component.onCompleted: { root.ready = true + if (popup) timeoutTimer.start() + } + + Timer { + id: timeoutTimer + interval: notificationObject.expireTimeout ?? root.defaultTimeoutValue + repeat: false + onTriggered: { + Notifications.timeoutNotification(notificationObject.id); + } } function destroyWithAnimation(delay = 0) { @@ -104,7 +116,7 @@ Item { root.toggleExpanded() } - // Flick right to dismiss + // Flick right to dismiss/discard property real startX: 0 property real dragStartThreshold: 10 property real dragConfirmThreshold: 70 @@ -124,7 +136,7 @@ Item { } onDragStartedChanged: () => { // Prevent drag focus being shifted to parent flickable - root.parent.parent.parent.interactive = !dragStarted + if (root.parent.parent.parent.interactive !== undefined) root.parent.parent.parent.interactive = !dragStarted root.enableAnimation = !dragStarted } onReleased: (mouse) => { @@ -196,6 +208,18 @@ Item { } } } + + DropShadow { + visible: popup + id: notificationShadow + anchors.fill: notificationBackground + source: notificationBackground + radius: 5 + samples: radius * 2 + 1 + color: Appearance.colors.colShadow + verticalOffset: 2 + horizontalOffset: 0 + } } @@ -334,6 +358,21 @@ Item { elide: Text.ElideRight } + CircularProgress { + id: notificationProgress + visible: popup + Layout.alignment: Qt.AlignVCenter + lineWidth: 2 + value: popup ? 1 : 0 + size: 20 + animationDuration: notificationObject.expireTimeout ?? root.defaultTimeoutValue + easingType: Easing.Linear + + Component.onCompleted: { + value = 0 + } + } + StyledText { // Time id: notificationTimeText Layout.fillWidth: false diff --git a/.config/quickshell/modules/notificationPopup/NotificationPopup.qml b/.config/quickshell/modules/notificationPopup/NotificationPopup.qml new file mode 100644 index 00000000..cfd5537d --- /dev/null +++ b/.config/quickshell/modules/notificationPopup/NotificationPopup.qml @@ -0,0 +1,120 @@ +import "root:/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +Scope { + id: screenCorners + readonly property Toplevel activeWindow: ToplevelManager.activeToplevel + + Variants { + model: Quickshell.screens + + PanelWindow { + id: root + visible: true + + property var modelData + property Component notifComponent: NotificationWidget {} + property list notificationWidgetList: [] + + screen: modelData + WlrLayershell.namespace: "quickshell:notificationPopup" + WlrLayershell.layer: WlrLayer.Overlay + exclusiveZone: 0 + + anchors { + top: true + right: true + bottom: true + } + + mask: Region { + item: columnLayout + } + + color: "transparent" + width: Appearance.sizes.notificationPopupWidth + height: columnLayout.implicitHeight + + // Signal handlers to add/remove notifications + Connections { + target: Notifications + function onNotify(notification) { + if (GlobalStates.sidebarRightOpenCount > 0) { + return + } + // notificationRepeater.model = [notification, ...notificationRepeater.model] + const notif = root.notifComponent.createObject(columnLayout, { + notificationObject: notification, + popup: true + }); + notificationWidgetList.unshift(notif) + + // Remove stuff from t he column, add back + for (let i = 0; i < notificationWidgetList.length; i++) { + if (notificationWidgetList[i].parent === columnLayout) { + notificationWidgetList[i].parent = null; + } + } + + // Add notification widgets to the column + for (let i = 0; i < notificationWidgetList.length; i++) { + if (notificationWidgetList[i].parent === null) { + notificationWidgetList[i].parent = columnLayout; + } + } + } + function onDiscard(id) { + for (let i = notificationWidgetList.length - 1; i >= 0; i--) { + const widget = notificationWidgetList[i]; + if (widget && widget.notificationObject && widget.notificationObject.id === id) { + widget.destroyWithAnimation(); + notificationWidgetList.splice(i, 1); + } + } + } + function onTimeout(id) { + for (let i = notificationWidgetList.length - 1; i >= 0; i--) { + const widget = notificationWidgetList[i]; + if (widget && widget.notificationObject && widget.notificationObject.id === id) { + widget.destroyWithAnimation(); + notificationWidgetList.splice(i, 1); + } + } + } + function onDiscardAll() { + for (let i = notificationWidgetList.length - 1; i >= 0; i--) { + const widget = notificationWidgetList[i]; + if (widget && widget.notificationObject) { + widget.destroyWithAnimation(); + } + } + notificationWidgetList = []; + } + } + + ColumnLayout { // Scrollable window content + id: columnLayout + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width - Appearance.sizes.hyprlandGapsOut * 2 + spacing: 0 // The widgets themselves have margins for spacing + + Item { + implicitHeight: 1 + implicitWidth: 1 + } + + // Notifications are added by the above signal handlers + } + + } + + } + +} diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml index 1c692264..fcdb778f 100644 --- a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml +++ b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayBrightness.qml @@ -57,8 +57,6 @@ Scope { anchors { top: true - left: true - right: true } mask: Region { item: osdValuesWrapper diff --git a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml index f15f8acc..dd7d237d 100644 --- a/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml +++ b/.config/quickshell/modules/onScreenDisplay/OnScreenDisplayVolume.qml @@ -57,8 +57,6 @@ Scope { anchors { top: true - left: true - right: true } mask: Region { item: osdValuesWrapper diff --git a/.config/quickshell/modules/sidebarRight/SidebarRight.qml b/.config/quickshell/modules/sidebarRight/SidebarRight.qml index 576015ab..50cd24bb 100644 --- a/.config/quickshell/modules/sidebarRight/SidebarRight.qml +++ b/.config/quickshell/modules/sidebarRight/SidebarRight.qml @@ -1,6 +1,7 @@ +import "root:/" +import "root:/services" import "root:/modules/common" import "root:/modules/common/widgets" -import "root:/services" import "./quickToggles/" import QtQuick import QtQuick.Controls @@ -25,6 +26,10 @@ Scope { visible: false focusable: true + onVisibleChanged: { + GlobalStates.sidebarRightOpenCount += visible ? 1 : -1 + } + property var modelData screen: modelData @@ -191,6 +196,7 @@ Scope { let panelWindow = sidebarVariants.instances[i]; if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { panelWindow.visible = !panelWindow.visible; + if(panelWindow.visible) Notifications.timeoutAll(); } } } @@ -209,6 +215,7 @@ Scope { let panelWindow = sidebarVariants.instances[i]; if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { panelWindow.visible = true; + if(panelWindow.visible) Notifications.timeoutAll(); } } } diff --git a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml index c04015b0..a0e1bfb9 100644 --- a/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml +++ b/.config/quickshell/modules/sidebarRight/notifications/NotificationList.qml @@ -22,6 +22,7 @@ Item { notificationWidgetList.push(notif) }) } + function onNotify(notification) { // notificationRepeater.model = [notification, ...notificationRepeater.model] const notif = root.notifComponent.createObject(columnLayout, { notificationObject: notification }); @@ -41,6 +42,7 @@ Item { } } } + function onDiscard(id) { for (let i = notificationWidgetList.length - 1; i >= 0; i--) { const widget = notificationWidgetList[i]; @@ -50,6 +52,16 @@ Item { } } } + + function onDiscardAll() { + for (let i = notificationWidgetList.length - 1; i >= 0; i--) { + const widget = notificationWidgetList[i]; + if (widget && widget.notificationObject) { + widget.destroyWithAnimation(); + } + } + notificationWidgetList = []; + } } Flickable { // Scrollable window @@ -145,15 +157,7 @@ Item { buttonIcon: "clear_all" buttonText: "Clear" onClicked: () => { - for (let i = notificationWidgetList.length - 1; i >= 0; i--) { - // for (let i = 0; i < notificationWidgetList.length; i++) { - const widget = notificationWidgetList[i]; - if (widget && widget.notificationObject) { - widget.destroyWithAnimation(); - } - } - notificationWidgetList = []; - Notifications.discardAll() + Notifications.discardAllNotifications() } } } diff --git a/.config/quickshell/services/Notifications.qml b/.config/quickshell/services/Notifications.qml index 2aa17c2b..d04233b8 100644 --- a/.config/quickshell/services/Notifications.qml +++ b/.config/quickshell/services/Notifications.qml @@ -18,6 +18,8 @@ Singleton { signal initDone(); signal notify(notification: var); signal discard(id: var); + signal discardAll(); + signal timeout(id: var); NotificationServer { id: notifServer @@ -69,13 +71,24 @@ Singleton { root.discard(id); } - function discardAll() { + function discardAllNotifications() { root.list = [] triggerListChange() notifFileView.setText(JSON.stringify(root.list, null, 2)) notifServer.trackedNotifications.values.forEach((notif) => { notif.dismiss() }) + root.discardAll(); + } + + function timeoutNotification(id) { + root.timeout(id); + } + + function timeoutAll() { + root.list.forEach((notif) => { + root.timeout(notif.id); + }) } function attemptInvokeAction(id, notifIdentifier) { diff --git a/.config/quickshell/shell.qml b/.config/quickshell/shell.qml index 68f1f047..002dad7d 100644 --- a/.config/quickshell/shell.qml +++ b/.config/quickshell/shell.qml @@ -1,6 +1,7 @@ //@ pragma UseQApplication import "./modules/bar/" +import "./modules/notificationPopup/" import "./modules/onScreenDisplay/" import "./modules/screenCorners/" import "./modules/session/" @@ -13,6 +14,7 @@ import Quickshell ShellRoot { Bar {} + NotificationPopup {} OnScreenDisplayBrightness {} OnScreenDisplayVolume {} ReloadPopup {}