Make Shell a category, the bar legible, and the dock a real dock
Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling, Workspaces — the home for everything Quickshell draws. The settings- management cluster moves to System as Sync & Backup, Appearance's Shell tab dissolves, and 24-hour time finally lives on Date & Time, which always owned it. The bar gets what it never had: a way to survive the wallpaper. A second neutral text family (follow theme, or forced light or dark), a one-layer shadow under every glyph, and a gradient scrim for wallpapers nothing else survives — all off by default, pixel-identical until asked. Widgets earn toggles (weather, media, clipboard, calendar countdown), the vitals cluster stops leaving a dead pill behind, and Control Center's sections learn to step aside. The dock graduates from MVP: a context menu with window rows, pin, unpin, quit and new-window; scroll an icon to cycle its windows; drag to reorder on the dock itself; hover previews with one-shot captures; and "Add App to Dock" in the launcher. Three real bugs died en route — menus that slid away with the autohide, a readonly-property crash on every menu open, and a drag that drifted half a slot per icon on side docks. The pinned-apps editor in Settings becomes a drag strip. 166 contracts; the full suite is green except two live display and switcher tests that cannot run behind a locked session — re-verified on unlock. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
// The dock's app menu. Desktop-entry actions stay first; the shell-owned
|
||||
// configuration route is deliberately last so it never displaces app actions.
|
||||
// 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
|
||||
@@ -12,19 +14,84 @@ PopupWindow {
|
||||
id: root
|
||||
|
||||
property Item anchorItem: null
|
||||
property var entry: 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.max(menu.implicitWidth + Theme.popoverPadding * 2, 240)
|
||||
implicitWidth: Math.min(360, Math.max(menu.implicitWidth + Theme.popoverPadding * 2, 240))
|
||||
implicitHeight: menu.implicitHeight + Theme.popoverPadding * 2
|
||||
color: "transparent"
|
||||
visible: false
|
||||
grabFocus: true
|
||||
|
||||
// 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
|
||||
@@ -46,6 +113,33 @@ PopupWindow {
|
||||
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.visible = 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 : []
|
||||
@@ -71,11 +165,59 @@ PopupWindow {
|
||||
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.visible = 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.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
TrayMenuRow {
|
||||
width: parent.width
|
||||
label: root.windows.length > 1 ? "Quit all windows" : "Quit"
|
||||
rowEnabled: root.windows.length > 0
|
||||
onActivated: {
|
||||
root.quit();
|
||||
root.visible = 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("desktop");
|
||||
ShellState.openSettings("dock");
|
||||
root.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user