quickshell: notification click/action jumps to the app's window

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-08-03 10:34:15 +01:00
parent 5aea7fab27
commit 3d5ecb7b98

View file

@ -1480,7 +1480,10 @@ in
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: { modelData.invoke(); _nc.actionInvoked(); }
// Invoking an action ("View", "Reply")
// makes the app want focus, but it can't
// take it itself see focusNotifApp.
onClicked: { modelData.invoke(); bar.focusNotifApp(_nc.notif); _nc.actionInvoked(); }
}
}
}
@ -3217,6 +3220,37 @@ in
if (a.startsWith("/")) return "file://" + a;
return "";
}
// Jump to the window that sent a notification. Toplevel.activate()
// goes over wlr-foreign-toplevel the same path a taskbar uses
// so Hyprland follows it across workspaces. The app raising
// itself does not work here: misc:focus_on_activate is off, so
// its own activation request is dropped on purpose.
function focusNotifApp(notif) {
if (!notif) return;
const want = [(notif.desktopEntry || "").replace(/\.desktop$/, ""), notif.appName]
.filter(s => s !== "").map(s => s.toLowerCase());
if (want.length === 0) return;
const tls = ToplevelManager.toplevels.values;
// Exact appId first: appName is a display name ("Vesktop"),
// appId the wayland id ("vesktop"), and they often differ by
// more than case, hence the substring pass.
for (const t of tls)
if (want.includes((t.appId || "").toLowerCase())) { t.activate(); return; }
for (const t of tls) {
const id = (t.appId || "").toLowerCase();
if (id !== "" && want.some(w => id.includes(w) || w.includes(id))) { t.activate(); return; }
}
}
// Clicking a notification body. Apps hide "open the right chat"
// behind the default action, so fire that too when they send one.
function activateNotif(notif) {
if (!notif) return;
for (const a of notif.actions)
if (a.identifier === "default") { a.invoke(); break; }
focusNotifApp(notif);
}
function toggleDropdown(dd, setupFn) {
if (dd.visible && !dd.closing) {
dd.animateClose();
@ -6045,6 +6079,18 @@ in
width: parent.width
height: Math.max(notifPreview.visible ? 48 : 0, ncBody.height)
// First child, so the action chips and the
// dismiss button still take their own clicks.
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
bar.activateNotif(notifItem.modelData);
notifItem.modelData.dismiss();
bar.closeAllDropdowns();
}
}
// Image preview (album art, screenshot thumb)
ClippingRectangle {
id: notifPreview
@ -6325,6 +6371,18 @@ in
property string previewSource: bar.notifPreviewSource(toastItem.currentNotif)
// First child, so the action chips and the dismiss
// button still take their own clicks.
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
bar.activateNotif(toastItem.currentNotif);
if (toastItem.currentNotif) toastItem.currentNotif.dismiss();
toastItem.dismiss();
}
}
// Image preview (screenshot thumb, album art)
ClippingRectangle {
id: toastPreview