// The dock's app menu: its open windows, then what the .desktop file offers, // then what the dock itself can do with the app. The shell-owned configuration // route is deliberately last so it never displaces an app action. import Quickshell import Quickshell.Hyprland import QtQuick import qs.config import qs.modules.bar import qs.services import qs.widgets PopupWindow { id: root property Item anchorItem: null // The whole dock entry: { entry, windows, appId, pinned }. Narrowing this // to a desktop entry on the way in is what used to stop the menu offering // anything about the app's actual windows, or knowing whether it is pinned. property var app: null readonly property DesktopEntry entry: root.app && root.app.entry ? root.app.entry : null readonly property var windows: root.app && root.app.windows ? root.app.windows : [] readonly property bool pinned: root.app ? root.app.pinned === true : false anchor.item: root.anchorItem anchor.edges: Edges.Top | Edges.Left anchor.gravity: Edges.Top | Edges.Right anchor.margins.bottom: 8 implicitWidth: Math.min(360, Math.max(menu.implicitWidth + Theme.popoverPadding * 2, 240)) implicitHeight: menu.implicitHeight + Theme.popoverPadding * 2 color: "transparent" grabFocus: true // Whether the dock has asked for the menu. Visibility is that AND a live // anchor, rather than the request alone: the dock icon a menu is anchored // to is a delegate, and a delegate dies when its app's last window closes. // An anchored PopupWindow whose anchor has gone stays mapped with a null // anchor -- a menu hanging over the desktop, attached to nothing, holding // the dock revealed behind it. property bool requested: false visible: root.requested && root.anchorItem !== null onAnchorItemChanged: if (root.anchorItem === null) root.requested = false // Read by the Dock's interaction watchdog. A menu with the pointer on it is // a menu being read, not a stuck flag -- and the pointer being here means // it is not on the dock, which is the only other thing the dock can see. readonly property bool hovered: menuPointer.hovered // Long window titles are the one thing here that can be arbitrarily wide, // and a menu as wide as a browser tab's title is not a menu. function shortTitle(toplevel: var): string { const title = String(toplevel?.title ?? "").trim(); if (!title) return root.app && root.app.appId ? root.app.appId : "Untitled window"; return title.length > 42 ? title.slice(0, 41) + "…" : title; } function addressOf(toplevel: var): string { const raw = String(toplevel?.address ?? ""); if (!raw) return ""; return raw.startsWith("0x") ? raw : "0x" + raw; } function focusToplevel(toplevel: var): void { if (!toplevel) return; if (toplevel.workspace) toplevel.workspace.activate(); const address = root.addressOf(toplevel); if (address) Hyprland.dispatch(`hl.dsp.focus({ window = "address:${address}" })`); else if (toplevel.wayland) toplevel.wayland.activate(); } // Every window, not the focused one: "Quit" on a dock icon means the app, // which is what the icon stands for. function quit(): void { for (const toplevel of root.windows) { const address = root.addressOf(toplevel); if (address) Hyprland.dispatch(`hl.dsp.window.close({ window = "address:${address}" })`); } } // Always the whole array through DesktopPreferences, which is the only // thing the Settings page and the dock agree on. function pin(): void { if (!root.entry) return; const stored = Settings.dockPinned; if (stored.indexOf(root.entry.id) >= 0) return; DesktopPreferences.set("dockPinned", stored.concat([root.entry.id])); } function unpin(): void { const id = root.app && root.app.appId ? root.app.appId : ""; if (!id) return; DesktopPreferences.set("dockPinned", Settings.dockPinned.filter(other => other !== id)); } Rectangle { anchors.fill: parent radius: Theme.popoverRadius color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha) border.width: 1 border.color: Theme.alpha(Theme.fg, 0.08) HoverHandler { id: menuPointer } PrismEdge { anchors.top: parent.top anchors.topMargin: 1 anchors.left: parent.left anchors.right: parent.right inset: parent.radius } Column { id: menu anchors.fill: parent anchors.margins: Theme.popoverPadding spacing: 2 // ── The app's own windows ─────────────────────────────────────── Repeater { id: openWindows model: root.windows delegate: TrayMenuRow { required property var modelData width: parent.width label: root.shortTitle(modelData) onActivated: { root.focusToplevel(modelData); root.requested = false; } } } Rectangle { width: parent.width height: 1 anchors.margins: 3 visible: openWindows.count > 0 border.width: 0 color: Theme.alpha(Theme.fg, 0.1) } // ── What the .desktop file offers ─────────────────────────────── Repeater { id: applicationActions model: root.entry ? root.entry.actions : [] delegate: TrayMenuRow { required property var modelData width: parent.width label: modelData.name onActivated: { modelData.execute(); root.requested = false; } } } Rectangle { width: parent.width height: 1 anchors.margins: 3 visible: applicationActions.count > 0 border.width: 0 color: Theme.alpha(Theme.fg, 0.1) } // ── What the dock can do with it ──────────────────────────────── TrayMenuRow { width: parent.width label: "New window" // A running application with no desktop entry cannot be // launched again -- there is nothing that says how. rowEnabled: root.entry !== null onActivated: { if (root.entry) root.entry.execute(); root.requested = false; } } TrayMenuRow { width: parent.width label: root.pinned ? "Unpin from dock" : "Pin to dock" // Unpinning needs only the id the pin was stored under; // pinning needs an entry to name, and an app whose id resolves // to nothing would be pinned as a hole. rowEnabled: root.pinned || root.entry !== null onActivated: { if (root.pinned) root.unpin(); else root.pin(); root.requested = false; } } TrayMenuRow { width: parent.width label: root.windows.length > 1 ? "Quit all windows" : "Quit" rowEnabled: root.windows.length > 0 onActivated: { root.quit(); root.requested = false; } } Rectangle { width: parent.width height: 1 anchors.margins: 3 border.width: 0 color: Theme.alpha(Theme.fg, 0.1) } TrayMenuRow { width: parent.width label: "Dock settings" onActivated: { ShellState.openSettings("dock"); root.requested = false; } } } } }