This commit is contained in:
2026-03-02 23:03:39 +02:00
parent f189e304a8
commit d536d75591
11 changed files with 276 additions and 58 deletions

View File

@@ -44,6 +44,13 @@ FloatingWindow {
font.pixelSize: 32 font.pixelSize: 32
} }
Text {
text: Quickshell.iconPath("67", "steam")
color: "#ff00ff"
font.family: "Determination Mono"
font.pixelSize: 32
}
Repeater { Repeater {
model: Bluetooth.devices model: Bluetooth.devices
Item { Item {

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

View File

@@ -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

View File

@@ -37,15 +37,33 @@ Item {
readonly property bool isExpanded: forceExpanded || contextOpen readonly property bool isExpanded: forceExpanded || contextOpen
function resolveIconSource(iconName) { function resolveIconSource(iconName) {
const fallbackName = "deltarune-soul";
const normalized = String(iconName || "").trim(); const normalized = String(iconName || "").trim();
if (normalized.length === 0) if (normalized.length === 0)
return ""; return String(Quickshell.iconPath(fallbackName, true) || "");
if (normalized.indexOf("/") >= 0 || normalized.indexOf("file://") === 0) if (normalized.indexOf("/") >= 0 || normalized.indexOf("file://") === 0)
return normalized; 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: { readonly property string appIconSource: {
if (preferSoulFallbackIcon)
return "";
const candidates = []; const candidates = [];
const desktopEntry = notifDesktopEntry.trim(); const desktopEntry = notifDesktopEntry.trim();
const appName = notifAppName.trim(); const appName = notifAppName.trim();
@@ -79,7 +97,9 @@ Item {
readonly property string customImageSource: notifImage readonly property string customImageSource: notifImage
readonly property string primaryIconSource: customImageSource.length > 0 ? customImageSource : appIconSource 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 int iconBoxSize: (theme ? theme.iconBox : 28) + 12
readonly property var notifActions: { readonly property var notifActions: {
@@ -114,22 +134,16 @@ Item {
return items; return items;
} }
function urgencyTimeoutMs() { function autoCloseTimeoutMs() {
const fallbackTimeoutMs = 3000;
if (!notifObject) if (!notifObject)
return theme.fallbackNormalTimeoutMs; return fallbackTimeoutMs;
const rawTimeout = Number(notifObject.expireTimeout); const specifiedTimeoutMs = Number(notifObject.expireTimeout);
if (Number.isFinite(rawTimeout) && rawTimeout > 0) { if (Number.isFinite(specifiedTimeoutMs) && specifiedTimeoutMs > 0)
const normalizedTimeout = rawTimeout > 100 ? Math.round(rawTimeout) : Math.round(rawTimeout * 1000); return Math.max(1, Math.round(specifiedTimeoutMs));
return Math.max(theme ? theme.minimumTimeoutMs : 2000, normalizedTimeout);
}
const urgency = notifObject.urgency; return fallbackTimeoutMs;
if (urgency === NotificationUrgency.Critical)
return theme.fallbackCriticalTimeoutMs;
if (urgency === NotificationUrgency.Low)
return theme.fallbackLowTimeoutMs;
return theme.fallbackNormalTimeoutMs;
} }
function relativeTime() { function relativeTime() {
@@ -166,10 +180,16 @@ Item {
} }
function restartTimeout() { function restartTimeout() {
timeoutTimer.stop();
if (closing || isExpanded || peekMode || hovered) if (closing || isExpanded || peekMode || hovered)
return; return;
timeoutTimer.interval = urgencyTimeoutMs();
timeoutTimer.restart(); const timeoutMs = autoCloseTimeoutMs();
if (!(timeoutMs > 0))
return;
timeoutTimer.interval = timeoutMs;
timeoutTimer.start();
} }
property string closeReason: "" property string closeReason: ""
@@ -232,7 +252,7 @@ Item {
Timer { Timer {
id: timeoutTimer id: timeoutTimer
interval: root.urgencyTimeoutMs() interval: root.autoCloseTimeoutMs()
repeat: false repeat: false
running: false running: false
onTriggered: root.beginClose("timeout") onTriggered: root.beginClose("timeout")
@@ -281,13 +301,15 @@ Item {
IconImage { IconImage {
anchors.fill: parent anchors.fill: parent
source: root.primaryIconSource source: root.primaryIconSource
visible: root.primaryIconSource.length > 0 visible: root.primaryIconReady
asynchronous: true asynchronous: true
onSourceChanged: root.primaryIconReady = false
onStatusChanged: root.primaryIconReady = status === Image.Ready
} }
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
visible: root.primaryIconSource.length === 0 visible: !root.primaryIconReady
color: "#000000" color: "#000000"
border.width: 0 border.width: 0
border.color: "transparent" border.color: "transparent"
@@ -300,7 +322,7 @@ Item {
width: parent.width - 2 width: parent.width - 2
height: parent.height - 2 height: parent.height - 2
source: "../Topbar/topbar/soul_small.png" source: "../Topbar/topbar/soul_small.png"
visible: root.primaryIconSource.length === 0 visible: !root.primaryIconReady
smooth: false smooth: false
antialiasing: false antialiasing: false
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
@@ -314,6 +336,8 @@ Item {
x: parent.width - width x: parent.width - width
y: parent.height - height y: parent.height - height
asynchronous: true asynchronous: true
onSourceChanged: root.badgeIconReady = false
onStatusChanged: root.badgeIconReady = status === Image.Ready
} }
} }

View File

@@ -15,14 +15,39 @@ QtObject {
} }
function indexOfId(notificationId) { function indexOfId(notificationId) {
const wantedId = String(notificationId);
for (let i = 0; i < notifications.count; i++) { for (let i = 0; i < notifications.count; i++) {
const row = notifications.get(i); const row = notifications.get(i);
if (row.rowNotificationId === notificationId) if (String(row.rowNotificationId) === wantedId)
return i; return i;
} }
return -1; 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) { function normalizedString(value) {
if (value === undefined || value === null) if (value === undefined || value === null)
return ""; return "";
@@ -36,6 +61,65 @@ QtObject {
return value === undefined || value === null ? "" : String(value); 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) { function groupKeyFor(notificationObject) {
const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase(); const desktopEntry = normalizedString(hintString(notificationObject, "desktop-entry")).trim().toLowerCase();
if (desktopEntry.length > 0) if (desktopEntry.length > 0)
@@ -123,6 +207,37 @@ QtObject {
setGroupExpanded(groupKey, !groupIsExpanded(groupKey)); 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) { function addNotification(notificationObject) {
if (notificationObject.lastGeneration) { if (notificationObject.lastGeneration) {
notificationObject.dismiss(); notificationObject.dismiss();
@@ -130,48 +245,62 @@ QtObject {
} }
notificationObject.tracked = true; notificationObject.tracked = true;
const groupKey = groupKeyFor(notificationObject);
const idCopy = notificationObject.id; const idCopy = notificationObject.id;
const objectCopy = notificationObject; const objectCopy = notificationObject;
notificationObject.closed.connect(function () { // Guard against reconnecting signals when the same QObject is seen again.
const rowIndex = indexOfId(idCopy); // Replacement is strict freedesktop id semantics:
if (rowIndex >= 0 && notifications.get(rowIndex).rowObject === objectCopy) { // `id=$(notify-send -p ...)`, then `notify-send -r "$id" ...`.
notifications.remove(rowIndex); if (!notificationObject.__dqModelWired) {
rebuildGroups(); notificationObject.__dqModelWired = true;
}
});
const nowMs = Date.now(); notificationObject.closed.connect(function () {
const groupKey = groupKeyFor(notificationObject); 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); const existingIndex = indexOfId(notificationObject.id);
if (existingIndex >= 0) { if (existingIndex >= 0 && notifications.get(existingIndex).rowObject === notificationObject) {
notifications.set(existingIndex, { const currentRow = notifications.get(existingIndex);
rowNotificationId: notificationObject.id, notifications.set(existingIndex, rowDataFor(notificationObject, currentRow.rowCreatedAt));
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")
});
rebuildGroups(); rebuildGroups();
return; return;
} }
notifications.append({ removeRowsById(notificationObject.id, notificationObject);
rowNotificationId: notificationObject.id, // Quickshell currently doesn't expose client replaces_id in QML, so keep
rowObject: notificationObject, // notify-send/dunstify entries collapsed to newest to match -r user intent.
rowVersion: nextVersion(), removeOldNotifyToolRows(notificationObject);
rowCreatedAt: nowMs, // Fallback replacement key uses id only.
rowGroupKey: groupKey, removeRowsByFallbackReplacementKey(notificationObject);
rowAppName: normalizedString(notificationObject.appName),
rowAppIcon: normalizedString(notificationObject.appIcon), notifications.insert(0, rowDataFor(notificationObject));
rowImage: normalizedString(notificationObject.image), // setGroupExpanded(groupKey, true);
rowDesktopEntry: hintString(notificationObject, "desktop-entry")
});
rebuildGroups(); rebuildGroups();
} }
@@ -191,8 +320,16 @@ QtObject {
} }
} }
notifications.remove(rowIndex); const remainingIndex = indexOfId(notificationId);
rebuildGroups(); 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 { property NotificationServer server: NotificationServer {

View File

@@ -232,6 +232,15 @@ Item {
manager.closePowerMenu(); 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) { function handleKey(key) {
switch (key) { switch (key) {
case Qt.Key_Up: case Qt.Key_Up:
@@ -343,6 +352,7 @@ Item {
visible: !root.inSubmenu && index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows visible: !root.inSubmenu && index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows
property var trayItem: modelData property var trayItem: modelData
readonly property string trayIconSource: root.resolveTrayIconSource(trayItem ? trayItem.icon : "")
Text { Text {
x: root.textStartX x: root.textStartX
@@ -366,7 +376,7 @@ Item {
x: 182 x: 182
y: 8 + 14 y: 8 + 14
implicitSize: root.iconSize implicitSize: root.iconSize
source: trayItem ? trayItem.icon : "" source: trayIconSource
asynchronous: true asynchronous: true
} }