From 3ce3fe19b11e30fd98d7eee06d56b90ae33b228d Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sat, 13 Sep 2025 14:56:55 +0200 Subject: ... --- accounts/gkleen@sif/shell/quickshell/Bar.qml | 4 +- .../shell/quickshell/NotificationDisplay.qml | 40 +++- .../quickshell/NotificationInhibitorWidget.qml | 213 +++++++++++++++++++++ .../quickshell/Services/NotificationManager.qml | 51 +++++ 4 files changed, 301 insertions(+), 7 deletions(-) (limited to 'accounts') diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml index dd0feb4b..426ed78c 100644 --- a/accounts/gkleen@sif/shell/quickshell/Bar.qml +++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml @@ -66,9 +66,9 @@ PanelWindow { anchors.verticalCenter: parent.verticalCenter spacing: 0 - WorktimeWidget { command: "time"; } + // WorktimeWidget { command: "time"; } - WorktimeWidget { command: "today"; } + // WorktimeWidget { command: "today"; } KeyboardLayout {} diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml index 589c36e5..a727b044 100644 --- a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml +++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml @@ -15,7 +15,7 @@ Scope { id: notifsRepeater model: ScriptModel { - values: NotificationManager.displayInhibited ? [] : [...NotificationManager.groups] + values: NotificationManager.displayInhibited ? [] : NotificationManager.groups } delegate: PanelWindow { @@ -48,6 +48,7 @@ Scope { readonly property real spaceAbove: { var res = 0; for (let i = 0; i < notifWindow.index; i++) { + (_ => {})(notifsRepeater.objectAt(i).modelData); res += notifsRepeater.objectAt(i).height + 8; } return res; @@ -60,7 +61,7 @@ Scope { color: "transparent" - implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + 16 + implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16 implicitWidth: 400 WrapperMouseArea { @@ -108,6 +109,8 @@ Scope { res += 1; if (notifActions.visible) res += 1; + if (notifTime.visible) + res += 1; return res; } @@ -167,7 +170,7 @@ Scope { Layout.column: 0 Layout.row: 0 Layout.fillHeight: true - Layout.rowSpan: notifBody.visible ? 2 : 1 + Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) } Text { @@ -176,6 +179,7 @@ Scope { visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" textFormat: Text.RichText + wrapMode: Text.Wrap font.pointSize: 10 font.family: "Fira Sans" @@ -187,6 +191,32 @@ Scope { Layout.columnSpan: notifCount.visible ? 2 : 1 } + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); + } + } + + visible: notifTime.text && notifTime.text !== "now" + text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: notifBody.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + Layout.columnSpan: 2 + } + RowLayout { id: notifActions @@ -196,9 +226,9 @@ Scope { uniformCellSizes: true width: 400 - 16 - Layout.row: notifBody.visible ? 2 : 1 + Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) Layout.column: 0 - Layout.columnSpan: notifImage.visible ? 3 : 2 + Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) Repeater { model: ScriptModel { diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml index 3dadbc69..1005182a 100644 --- a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml +++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml @@ -2,6 +2,9 @@ import Quickshell import QtQuick import Quickshell.Widgets import qs.Services +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Shapes Item { id: root @@ -44,4 +47,214 @@ Item { } } } + + Loader { + id: tooltipLoader + + active: false + + Connections { + target: widgetMouseArea + function onContainsMouseChanged() { + if (widgetMouseArea.containsMouse) + tooltipLoader.active = true; + } + } + + sourceComponent: PopupWindow { + id: tooltip + + property bool nextVisible: !NotificationManager.displayInhibited && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse) + + anchor { + item: widgetMouseArea + edges: Edges.Bottom | Edges.Left + } + visible: false + + onNextVisibleChanged: hangTimer.restart() + + Timer { + id: hangTimer + interval: tooltip.visible ? 100 : 500 + onTriggered: { + tooltip.visible = tooltip.nextVisible; + if (!tooltip.visible) + tooltipLoader.active = false; + } + } + + implicitWidth: 400 + implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16)) + color: "black" + + WrapperMouseArea { + id: tooltipMouseArea + + hoverEnabled: true + enabled: true + + anchors.fill: parent + + WrapperItem { + margin: 8 + + ScrollView { + id: scroll + + contentWidth: availableWidth + // ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + ColumnLayout { + id: historyLayout + anchors { + left: parent.left + right: parent.right + } + + spacing: 8 + + Repeater { + model: ScriptModel { + values: [...NotificationManager.history].reverse().map(o => o.notification) + } + + delegate: GridLayout { + id: notif + + Layout.fillWidth: true + Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8 + + required property var modelData + required property int index + + columnSpacing: 8 + rowSpacing: 8 + + columns: notifImage.visible ? 2 : 1 + rows: { + var res = 2; + if (notifBody.visible) + res += 1; + if (notifSep.visible) + res += 1; + return res; + } + + Shape { + id: notifSep + + visible: notif.index != 0 + + height: 2 + width: 400 - 32 + + ShapePath { + strokeWidth: 2 + strokeColor: "#20ffffff" + startX: 0; startY: 0; + PathLine { x: 400 - 32; y: 0; } + } + + Layout.row: 0 + Layout.column: 0 + Layout.columnSpan: notifImage.visible ? 2 : 1 + Layout.alignment: Qt.AlignHCenter + } + + Text { + id: notifSummary + + text: notif.modelData?.summary ?? "" + + font.pointSize: 10 + font.family: "Fira Sans" + font.italic: true + color: "white" + maximumLineCount: 1 + elide: Text.ElideRight + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 1 : 0 + Layout.column: notifImage.visible ? 1 : 0 + } + + Image { + id: notifImage + + visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false + + onStatusChanged: { + if (notifImage.status == Image.Error) + notifImage.visible = false; + } + + source: (notif.modelData?.image || notif.modelData?.appIcon) ?? "" + fillMode: Image.PreserveAspectFit + asynchronous: true + smooth: true + mipmap: true + + Layout.maximumWidth: 50 + Layout.column: 0 + Layout.row: notifSep.visible ? 1 : 0 + Layout.fillHeight: true + Layout.rowSpan: notifBody.visible ? 3 : 2 + } + + Text { + id: notifBody + + visible: notif.modelData?.body ?? false + text: notif.modelData?.body ?? "" + textFormat: Text.RichText + wrapMode: Text.Wrap + + font.pointSize: 10 + font.family: "Fira Sans" + color: "white" + + Layout.fillWidth: true + Layout.row: notifSep.visible ? 2 : 1 + Layout.column: notifImage.visible ? 1 : 0 + } + + Text { + id: notifTime + + Connections { + target: NotificationManager.clock + function onDateChanged() { + notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime); + } + } + + text: NotificationManager.formatTime(notif.modelData?.receivedTime) + + font.pointSize: 8 + font.family: "Fira Sans" + font.italic: true + color: "#555" + maximumLineCount: 1 + horizontalAlignment: Text.AlignRight + + Layout.fillWidth: true + Layout.row: { + var res = 1; + if (notifSep.visible) + res += 1; + if (notifBody.visible) + res += 1; + return res; + } + Layout.column: notifImage.visible ? 1 : 0 + } + } + } + } + } + } + } + } + } } diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml index 778cdc2a..2199ccdf 100644 --- a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml +++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml @@ -56,6 +56,8 @@ Singleton { { "test": { "appName": "Element" }, "group-by": [ "summary" ] } ]; + property var history: [] + Component { id: expirationTimer @@ -74,6 +76,45 @@ Singleton { } } + Component { + id: notificationLock + + RetainableLock {} + } + + readonly property SystemClock clock: SystemClock { + precision: SystemClock.Minutes + } + + function formatTime(time) { + const now = root.clock.date; + const diff = now - time; + const minutes = Math.ceil(diff / 60000); + const hours = Math.floor(minutes / 60); + + if (hours < 1) { + if (minutes < 1) + return "now"; + if (minutes == 1) + return "1 minute"; + return `${minutes} minutes`; + } + + const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) + const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) + const days = Math.floor((nowDate - timeDate) / (1000 * 86400)) + + const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm"); + + if (days === 0) + return timeStr; + if (days === 1) + return `yesterday ${timeStr}`; + + const dateStr = time.toLocaleTimeString(Qt.locale(), "YYYY-MM-DD"); + return `${dateStr} ${timeStr}`; + } + NotificationServer { id: server @@ -92,7 +133,17 @@ Singleton { Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); } + Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true }); + notification.receivedTime = root.clock.date; + notification.closed.connect((reason) => server.onNotificationClosed(notification, reason)); notification.tracked = true; } + + function onNotificationClosed(notification, reason) { + root.history.push({ + lock: notificationLock.createObject(root, { locked: true, object: notification }), + notification: notification + }); + } } } -- cgit v1.2.3