diff --git a/settings/quickshell.nix b/settings/quickshell.nix index 17a09c3..7a2ab1d 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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