From d536d755911e8030bb1f41fe98965d48ecdc89a7 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 2 Mar 2026 23:03:39 +0200 Subject: [PATCH] fix --- Apps/QsDebugger/shell.qml | 7 + DeltaruneIcons/16x16/apps/deltarune-soul.png | Bin 0 -> 4189 bytes DeltaruneIcons/24x24/apps/deltarune-soul.png | Bin 0 -> 4189 bytes DeltaruneIcons/32x32/apps/deltarune-soul.png | Bin 0 -> 4189 bytes DeltaruneIcons/48x48/apps/deltarune-soul.png | Bin 0 -> 5414 bytes DeltaruneIcons/64x64/apps/deltarune-soul.png | Bin 0 -> 5414 bytes DeltaruneIcons/icon-theme.cache | Bin 0 -> 216 bytes DeltaruneIcons/index.theme | 40 ++++ Shell/Notifications/NotificationCard.qml | 68 ++++-- Shell/Notifications/NotificationModel.qml | 207 +++++++++++++++---- Shell/Windows/PowerMenu/PowerMenuApp.qml | 12 +- 11 files changed, 276 insertions(+), 58 deletions(-) create mode 100644 DeltaruneIcons/16x16/apps/deltarune-soul.png create mode 100644 DeltaruneIcons/24x24/apps/deltarune-soul.png create mode 100644 DeltaruneIcons/32x32/apps/deltarune-soul.png create mode 100644 DeltaruneIcons/48x48/apps/deltarune-soul.png create mode 100644 DeltaruneIcons/64x64/apps/deltarune-soul.png create mode 100644 DeltaruneIcons/icon-theme.cache create mode 100644 DeltaruneIcons/index.theme diff --git a/Apps/QsDebugger/shell.qml b/Apps/QsDebugger/shell.qml index 78fb300..8a6e945 100644 --- a/Apps/QsDebugger/shell.qml +++ b/Apps/QsDebugger/shell.qml @@ -44,6 +44,13 @@ FloatingWindow { font.pixelSize: 32 } + Text { + text: Quickshell.iconPath("67", "steam") + color: "#ff00ff" + font.family: "Determination Mono" + font.pixelSize: 32 + } + Repeater { model: Bluetooth.devices Item { diff --git a/DeltaruneIcons/16x16/apps/deltarune-soul.png b/DeltaruneIcons/16x16/apps/deltarune-soul.png new file mode 100644 index 0000000000000000000000000000000000000000..b953cf81990d3aa8b22b95e853cdd328d2c75e3e GIT binary patch literal 4189 zcmeAS@N?(olHy`uVBq!ia0y~yU{GLSV36lvV_;zX82Dd`fq{XsILO_J@#aaLdIkmt zmUKs7M+SzC{oH>NSwX6kJ%W507^>757#dm_7=AG@Ff_biU???UV0e|lz+g3lfkC`r z&aOZk1_lPs0*}aI1_nK45N51cYG1~{z<@E1q|8549QNdMpr0g%g(F)_s~vU(UBn~z~~FnVZ&0L-<>GNSwX6kJ%W507^>757#dm_7=AG@Ff_biU???UV0e|lz+g3lfkC`r z&aOZk1_lPs0*}aI1_nK45N51cYG1~{z<@E1q|8549QNdMpr0g%g(F)_s~vU(UBn~z~~FnVZ&0L-<>GNSwX6kJ%W507^>757#dm_7=AG@Ff_biU???UV0e|lz+g3lfkC`r z&aOZk1_lPs0*}aI1_nK45N51cYG1~{z<@E1q|8549QNdMpr0g%g(F)_s~vU(UBn~z~~FnVZ&0L-<>GJ4mJh`#%b&;_!t-%7>k44ofvPP)Tw7+U|>mi z^mSxl*x1kgCy|wbfk7eJBgmJ5p-PQ`p`nF=;THn~L&FOOhEf9thF1v;3|2E37{m+a z>>21hNDp0|QtLBn~nZ#ayr;hydwe0udk#5&&Tg3%~{;s{pA2W0)X@3YhgU z1t8TJ=7I##F%!A~Ha^%om;|yFATgNLAU4?N=n6o55C$nA&N`TCkPZ+AS%D#jE&x)9 zj)}64C`I5Pz-J}MjbKF}H(*l;QUH>JD*|zl6~YvN#NmpN6@Vl_7+E2R55l@W5ck>KrslWkQf7D2Eg=SH3%k&%|LVm zU~1t8p=-jW0hcZBO#h}>OJKHwXnd{$ z$z#K$m~VjM<}c?1huWJh3QV{;fO4na4QYH^QlFu8n+Lx^09ap@a$ zAvEeBTu}uR9}OW`NZ_IeT?nC+)G$|5Qn10yAeKfiScgUkq5FJGKAcuOo`njxgN@xNAZdH*p literal 0 HcmV?d00001 diff --git a/DeltaruneIcons/64x64/apps/deltarune-soul.png b/DeltaruneIcons/64x64/apps/deltarune-soul.png new file mode 100644 index 0000000000000000000000000000000000000000..136d6afb12157a683312f6cf1bf33c20153ac10f GIT binary patch literal 5414 zcmeAS@N?(olHy`uVBq!ia0y~yU{C>J4mJh`#%b&;_!t-%7>k44ofvPP)Tw7+U|>mi z^mSxl*x1kgCy|wbfk7eJBgmJ5p-PQ`p`nF=;THn~L&FOOhEf9thF1v;3|2E37{m+a z>>21hNDp0|QtLBn~nZ#ayr;hydwe0udk#5&&Tg3%~{;s{pA2W0)X@3YhgU z1t8TJ=7I##F%!A~Ha^%om;|yFATgNLAU4?N=n6o55C$nA&N`TCkPZ+AS%D#jE&x)9 zj)}64C`I5Pz-J}MjbKF}H(*l;QUH>JD*|zl6~YvN#NmpN6@Vl_7+E2R55l@W5ck>KrslWkQf7D2Eg=SH3%k&%|LVm zU~1t8p=-jW0hcZBO#h}>OJKHwXnd{$ z$z#K$m~VjM<}c?1huWJh3QV{;fO4na4QYH^QlFu8n+Lx^09ap@a$ zAvEeBTu}uR9}OW`NZ_IeT?nC+)G$|5Qn10yAeKfiScgUkq5FJGKAcuOo`njxgN@xNAZdH*p literal 0 HcmV?d00001 diff --git a/DeltaruneIcons/icon-theme.cache b/DeltaruneIcons/icon-theme.cache new file mode 100644 index 0000000000000000000000000000000000000000..8957bdcd7dabd5fbb8852cf4c60b612f211f9774 GIT binary patch literal 216 zcmZQzWB>sk1_p)}1_lQ1|5yM61A`3~VXzPb1A_+x14BegYEDUFQE6VPZgGBT4#-Fb zRt6RZ7BGt$N;5%eMkvhyr&$>o80IiAFsxu;VA#UIz;J+pf#HmyS%smQequpEF$05< dNrjOKlx=KOVQd6tn^;tsSU}lkCKYBdH2^KpMuY$W literal 0 HcmV?d00001 diff --git a/DeltaruneIcons/index.theme b/DeltaruneIcons/index.theme new file mode 100644 index 0000000..6837919 --- /dev/null +++ b/DeltaruneIcons/index.theme @@ -0,0 +1,40 @@ +[Icon Theme] +Name=DeltaruneIcons +Comment=Deltarune soul fallback icons +Inherits=Arashi,breeze,Adwaita,AdwaitaLegacy,hicolor + +DesktopDefault=48 +DesktopSizes=16,24,32,48,64 +ToolbarDefault=24 +ToolbarSizes=16,24,32,48 +SmallDefault=16 +SmallSizes=16,24,32 +PanelDefault=24 +PanelSizes=16,24,32,48 + +Directories=16x16/apps,24x24/apps,32x32/apps,48x48/apps,64x64/apps + +[16x16/apps] +Size=16 +Context=Applications +Type=Fixed + +[32x32/apps] +Size=32 +Context=Applications +Type=Fixed + +[48x48/apps] +Size=48 +Context=Applications +Type=Fixed + +[24x24/apps] +Size=24 +Context=Applications +Type=Fixed + +[64x64/apps] +Size=64 +Context=Applications +Type=Fixed diff --git a/Shell/Notifications/NotificationCard.qml b/Shell/Notifications/NotificationCard.qml index 32ff0ad..c9a0a94 100644 --- a/Shell/Notifications/NotificationCard.qml +++ b/Shell/Notifications/NotificationCard.qml @@ -37,15 +37,33 @@ Item { readonly property bool isExpanded: forceExpanded || contextOpen function resolveIconSource(iconName) { + const fallbackName = "deltarune-soul"; const normalized = String(iconName || "").trim(); if (normalized.length === 0) - return ""; + return String(Quickshell.iconPath(fallbackName, true) || ""); if (normalized.indexOf("/") >= 0 || normalized.indexOf("file://") === 0) return normalized; - return String(Quickshell.iconPath(normalized) || ""); + + const primaryPath = String(Quickshell.iconPath(normalized, true) || ""); + if (primaryPath.length > 0) + return primaryPath; + + return String(Quickshell.iconPath(fallbackName, true) || ""); } + function isNotifyToolName(value) { + const normalized = String(value || "").trim().toLowerCase(); + if (normalized.length === 0) + return false; + return normalized === "notify-send" || normalized === "dunstify" || normalized === "notify-send.desktop" || normalized === "dunstify.desktop"; + } + + readonly property bool preferSoulFallbackIcon: isNotifyToolName(notifAppName) || isNotifyToolName(notifDesktopEntry) + readonly property string appIconSource: { + if (preferSoulFallbackIcon) + return ""; + const candidates = []; const desktopEntry = notifDesktopEntry.trim(); const appName = notifAppName.trim(); @@ -79,7 +97,9 @@ Item { readonly property string customImageSource: notifImage readonly property string primaryIconSource: customImageSource.length > 0 ? customImageSource : appIconSource - readonly property bool showBadgeIcon: customImageSource.length > 0 && appIconSource.length > 0 + property bool primaryIconReady: false + property bool badgeIconReady: false + readonly property bool showBadgeIcon: customImageSource.length > 0 && appIconSource.length > 0 && badgeIconReady readonly property int iconBoxSize: (theme ? theme.iconBox : 28) + 12 readonly property var notifActions: { @@ -114,22 +134,16 @@ Item { return items; } - function urgencyTimeoutMs() { + function autoCloseTimeoutMs() { + const fallbackTimeoutMs = 3000; if (!notifObject) - return theme.fallbackNormalTimeoutMs; + return fallbackTimeoutMs; - const rawTimeout = Number(notifObject.expireTimeout); - if (Number.isFinite(rawTimeout) && rawTimeout > 0) { - const normalizedTimeout = rawTimeout > 100 ? Math.round(rawTimeout) : Math.round(rawTimeout * 1000); - return Math.max(theme ? theme.minimumTimeoutMs : 2000, normalizedTimeout); - } + const specifiedTimeoutMs = Number(notifObject.expireTimeout); + if (Number.isFinite(specifiedTimeoutMs) && specifiedTimeoutMs > 0) + return Math.max(1, Math.round(specifiedTimeoutMs)); - const urgency = notifObject.urgency; - if (urgency === NotificationUrgency.Critical) - return theme.fallbackCriticalTimeoutMs; - if (urgency === NotificationUrgency.Low) - return theme.fallbackLowTimeoutMs; - return theme.fallbackNormalTimeoutMs; + return fallbackTimeoutMs; } function relativeTime() { @@ -166,10 +180,16 @@ Item { } function restartTimeout() { + timeoutTimer.stop(); if (closing || isExpanded || peekMode || hovered) return; - timeoutTimer.interval = urgencyTimeoutMs(); - timeoutTimer.restart(); + + const timeoutMs = autoCloseTimeoutMs(); + if (!(timeoutMs > 0)) + return; + + timeoutTimer.interval = timeoutMs; + timeoutTimer.start(); } property string closeReason: "" @@ -232,7 +252,7 @@ Item { Timer { id: timeoutTimer - interval: root.urgencyTimeoutMs() + interval: root.autoCloseTimeoutMs() repeat: false running: false onTriggered: root.beginClose("timeout") @@ -281,13 +301,15 @@ Item { IconImage { anchors.fill: parent source: root.primaryIconSource - visible: root.primaryIconSource.length > 0 + visible: root.primaryIconReady asynchronous: true + onSourceChanged: root.primaryIconReady = false + onStatusChanged: root.primaryIconReady = status === Image.Ready } Rectangle { anchors.fill: parent - visible: root.primaryIconSource.length === 0 + visible: !root.primaryIconReady color: "#000000" border.width: 0 border.color: "transparent" @@ -300,7 +322,7 @@ Item { width: parent.width - 2 height: parent.height - 2 source: "../Topbar/topbar/soul_small.png" - visible: root.primaryIconSource.length === 0 + visible: !root.primaryIconReady smooth: false antialiasing: false fillMode: Image.PreserveAspectFit @@ -314,6 +336,8 @@ Item { x: parent.width - width y: parent.height - height asynchronous: true + onSourceChanged: root.badgeIconReady = false + onStatusChanged: root.badgeIconReady = status === Image.Ready } } diff --git a/Shell/Notifications/NotificationModel.qml b/Shell/Notifications/NotificationModel.qml index d82bee4..619495a 100644 --- a/Shell/Notifications/NotificationModel.qml +++ b/Shell/Notifications/NotificationModel.qml @@ -15,14 +15,39 @@ QtObject { } function indexOfId(notificationId) { + const wantedId = String(notificationId); for (let i = 0; i < notifications.count; i++) { const row = notifications.get(i); - if (row.rowNotificationId === notificationId) + if (String(row.rowNotificationId) === wantedId) return i; } return -1; } + function indexOfIdAndObject(notificationId, notificationObject) { + const wantedId = String(notificationId); + for (let i = 0; i < notifications.count; i++) { + const row = notifications.get(i); + if (String(row.rowNotificationId) === wantedId && row.rowObject === notificationObject) + return i; + } + return -1; + } + + function removeRowsById(notificationId, replacementObject) { + const wantedId = String(notificationId); + for (let i = notifications.count - 1; i >= 0; i--) { + const row = notifications.get(i); + if (String(row.rowNotificationId) !== wantedId) + continue; + + if (row.rowObject && row.rowObject !== replacementObject) + row.rowObject.dismiss(); + + notifications.remove(i); + } + } + function normalizedString(value) { if (value === undefined || value === null) return ""; @@ -36,6 +61,65 @@ QtObject { return value === undefined || value === null ? "" : String(value); } + function isNotifyToolName(value) { + const normalized = String(value || "").trim().toLowerCase(); + if (normalized.length === 0) + return false; + return normalized === "notify-send" || normalized === "dunstify" || normalized === "notify-send.desktop" || normalized === "dunstify.desktop"; + } + + function isNotifyToolNotification(notificationObject) { + if (!notificationObject) + return false; + return isNotifyToolName(notificationObject.appName) || isNotifyToolName(hintString(notificationObject, "desktop-entry")); + } + + function removeOldNotifyToolRows(replacementObject) { + if (!isNotifyToolNotification(replacementObject)) + return; + + for (let i = notifications.count - 1; i >= 0; i--) { + const row = notifications.get(i); + const rowObject = row.rowObject; + if (!rowObject || rowObject === replacementObject) + continue; + if (!isNotifyToolNotification(rowObject)) + continue; + + rowObject.dismiss(); + notifications.remove(i); + } + } + + function replacementFallbackKeyFor(notificationObject) { + if (!notificationObject) + return ""; + + const idValue = Number(notificationObject.id); + if (Number.isFinite(idValue) && idValue > 0) + return "id:" + String(Math.trunc(idValue)); + + return ""; + } + + function removeRowsByFallbackReplacementKey(replacementObject) { + const fallbackKey = replacementFallbackKeyFor(replacementObject); + if (fallbackKey.length === 0) + return; + + for (let i = notifications.count - 1; i >= 0; i--) { + const row = notifications.get(i); + if (row.rowObject === replacementObject) + continue; + if (String(row.rowReplacementFallbackKey || "") !== fallbackKey) + continue; + + if (row.rowObject) + row.rowObject.dismiss(); + notifications.remove(i); + } + } + function groupKeyFor(notificationObject) { const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase(); if (desktopEntry.length > 0) @@ -123,6 +207,37 @@ QtObject { setGroupExpanded(groupKey, !groupIsExpanded(groupKey)); } + function rowDataFor(notificationObject, createdAtMsOverride) { + return { + rowNotificationId: notificationObject.id, + rowObject: notificationObject, + rowVersion: nextVersion(), + rowCreatedAt: createdAtMsOverride !== undefined ? createdAtMsOverride : Date.now(), + rowGroupKey: groupKeyFor(notificationObject), + rowAppName: normalizedString(notificationObject.appName), + rowAppIcon: normalizedString(notificationObject.appIcon), + rowImage: normalizedString(notificationObject.image), + rowDesktopEntry: hintString(notificationObject, "desktop-entry"), + rowReplacementFallbackKey: replacementFallbackKeyFor(notificationObject) + }; + } + + function bumpNotificationToTop(notificationObject) { + const rowIndex = indexOfIdAndObject(notificationObject.id, notificationObject); + if (rowIndex < 0) + return; + + if (rowIndex === 0) { + notifications.set(0, rowDataFor(notificationObject)); + rebuildGroups(); + return; + } + + removeRowsById(notificationObject.id, notificationObject); + notifications.insert(0, rowDataFor(notificationObject)); + rebuildGroups(); + } + function addNotification(notificationObject) { if (notificationObject.lastGeneration) { notificationObject.dismiss(); @@ -130,48 +245,62 @@ QtObject { } notificationObject.tracked = true; + const groupKey = groupKeyFor(notificationObject); const idCopy = notificationObject.id; const objectCopy = notificationObject; - notificationObject.closed.connect(function () { - const rowIndex = indexOfId(idCopy); - if (rowIndex >= 0 && notifications.get(rowIndex).rowObject === objectCopy) { - notifications.remove(rowIndex); - rebuildGroups(); - } - }); + // Guard against reconnecting signals when the same QObject is seen again. + // Replacement is strict freedesktop id semantics: + // `id=$(notify-send -p ...)`, then `notify-send -r "$id" ...`. + if (!notificationObject.__dqModelWired) { + notificationObject.__dqModelWired = true; - const nowMs = Date.now(); - const groupKey = groupKeyFor(notificationObject); + notificationObject.closed.connect(function () { + let rowIndex = indexOfIdAndObject(idCopy, objectCopy); + if (rowIndex < 0) { + const idOnlyIndex = indexOfId(idCopy); + if (idOnlyIndex >= 0 && notifications.get(idOnlyIndex).rowObject === objectCopy) + rowIndex = idOnlyIndex; + } + + if (rowIndex >= 0) { + notifications.remove(rowIndex); + setGroupExpanded(groupKey, false); + rebuildGroups(); + } + }); + + const onUpdated = function () { + root.bumpNotificationToTop(objectCopy); + }; + notificationObject.expireTimeoutChanged.connect(onUpdated); + notificationObject.appNameChanged.connect(onUpdated); + notificationObject.appIconChanged.connect(onUpdated); + notificationObject.summaryChanged.connect(onUpdated); + notificationObject.bodyChanged.connect(onUpdated); + notificationObject.actionsChanged.connect(onUpdated); + notificationObject.desktopEntryChanged.connect(onUpdated); + notificationObject.imageChanged.connect(onUpdated); + notificationObject.hintsChanged.connect(onUpdated); + } const existingIndex = indexOfId(notificationObject.id); - if (existingIndex >= 0) { - notifications.set(existingIndex, { - rowNotificationId: notificationObject.id, - rowObject: notificationObject, - rowVersion: nextVersion(), - rowCreatedAt: nowMs, - rowGroupKey: groupKey, - rowAppName: normalizedString(notificationObject.appName), - rowAppIcon: normalizedString(notificationObject.appIcon), - rowImage: normalizedString(notificationObject.image), - rowDesktopEntry: hintString(notificationObject, "desktop-entry") - }); + if (existingIndex >= 0 && notifications.get(existingIndex).rowObject === notificationObject) { + const currentRow = notifications.get(existingIndex); + notifications.set(existingIndex, rowDataFor(notificationObject, currentRow.rowCreatedAt)); rebuildGroups(); return; } - notifications.append({ - rowNotificationId: notificationObject.id, - rowObject: notificationObject, - rowVersion: nextVersion(), - rowCreatedAt: nowMs, - rowGroupKey: groupKey, - rowAppName: normalizedString(notificationObject.appName), - rowAppIcon: normalizedString(notificationObject.appIcon), - rowImage: normalizedString(notificationObject.image), - rowDesktopEntry: hintString(notificationObject, "desktop-entry") - }); + removeRowsById(notificationObject.id, notificationObject); + // Quickshell currently doesn't expose client replaces_id in QML, so keep + // notify-send/dunstify entries collapsed to newest to match -r user intent. + removeOldNotifyToolRows(notificationObject); + // Fallback replacement key uses id only. + removeRowsByFallbackReplacementKey(notificationObject); + + notifications.insert(0, rowDataFor(notificationObject)); + // setGroupExpanded(groupKey, true); rebuildGroups(); } @@ -191,8 +320,16 @@ QtObject { } } - notifications.remove(rowIndex); - rebuildGroups(); + const remainingIndex = indexOfId(notificationId); + if (remainingIndex >= 0) { + const remainingRow = notifications.get(remainingIndex); + // Avoid removing a newly inserted replacement row with the same id. + if (!notifObject || remainingRow.rowObject === notifObject) { + notifications.remove(remainingIndex); + // setGroupExpanded(groupKey, true); + rebuildGroups(); + } + } } property NotificationServer server: NotificationServer { diff --git a/Shell/Windows/PowerMenu/PowerMenuApp.qml b/Shell/Windows/PowerMenu/PowerMenuApp.qml index 50afb2c..fb2961b 100644 --- a/Shell/Windows/PowerMenu/PowerMenuApp.qml +++ b/Shell/Windows/PowerMenu/PowerMenuApp.qml @@ -232,6 +232,15 @@ Item { manager.closePowerMenu(); } + function resolveTrayIconSource(iconValue) { + const normalized = String(iconValue || "").trim(); + if (normalized.length === 0) + return ""; + if (normalized.indexOf("/") >= 0 || normalized.indexOf("file://") === 0) + return normalized; + return String(Quickshell.iconPath(normalized, true) || ""); + } + function handleKey(key) { switch (key) { case Qt.Key_Up: @@ -343,6 +352,7 @@ Item { visible: !root.inSubmenu && index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows property var trayItem: modelData + readonly property string trayIconSource: root.resolveTrayIconSource(trayItem ? trayItem.icon : "") Text { x: root.textStartX @@ -366,7 +376,7 @@ Item { x: 182 y: 8 + 14 implicitSize: root.iconSize - source: trayItem ? trayItem.icon : "" + source: trayIconSource asynchronous: true }