Files
Panama/config/dot/quickshell/modules/dock/DockBody.qml
T
Gabriel Brown d96863b687 Convert British spellings to American across the repo
colour -> color, behaviour -> behavior, centre -> center, favourite ->
favorite, and about twenty other pairs, applied consistently across
comments, docs, error/UI copy, and a handful of QML identifiers that
used the British spelling as their actual name: SystemSettings'
serialiseValue/serialiseTable/normaliseGradient, Displays'
normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's
serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's
_normalise helper, and ShortcutCapture's cancelled signal (with its
onCancelled handler in ShortcutsPage.qml). Every call site and the two
tests that assert on the literal source text (settings-ownership and
settings-backup-live contracts) were updated in lockstep.

Left untouched: config/dot/espanso/match/packages/misspell-en/ is a
vendored third-party autocorrect dictionary -- its entries are typo
corrections, not our prose, and rewriting them would fight the
package's own purpose (and any future re-sync from upstream).

The already-American `favorites` property (Home page pinned
accessories) was never actually misspelled -- only nearby comments and
error strings said "favourites" -- so no data migration was needed
there.

Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
2026-08-19 08:07:55 -04:00

204 lines
7.2 KiB
QML

// The visible dock bar: the rounded translucent pill, its items, and the
// tooltip that floats above it.
//
// Kept separate from Dock.qml so it can be dropped straight into a
// FloatingWindow for testing — PanelWindow cannot map without layer shell.
import Quickshell
import Quickshell.Hyprland
import QtQuick
import qs.config
import qs.widgets
Rectangle {
id: root
// ── Model ───────────────────────────────────────────────────────────────
// One pass over the live toplevel list produces the whole dock: pinned
// apps first in Settings order, then anything else that is running.
// Entries are plain JS objects: { entry, windows, appId }.
readonly property var items: {
// DesktopEntries is scanned asynchronously at startup, and byId() is a
// plain method call that creates no binding dependency. Reading the
// model here is what makes the dock fill in once the scan lands.
if (DesktopEntries.applications.values.length === 0)
return [];
const groups = {}; // appId -> [HyprlandToplevel]
const order = []; // appIds in first-seen order
const toplevels = Hyprland.toplevels.values;
for (let i = 0; i < toplevels.length; i++) {
const t = toplevels[i];
// `wayland` is null until Hyprland reports the address, and it is
// the only live source of an app id — there is no `class` property.
const appId = t.wayland ? t.wayland.appId : "";
if (!appId)
continue;
if (!groups[appId]) {
groups[appId] = [];
order.push(appId);
}
groups[appId].push(t);
}
const out = [];
const claimed = {};
const pinned = Settings.dockPinned;
for (let i = 0; i < pinned.length; i++) {
const entry = DesktopEntries.byId(pinned[i]);
// A pin that no longer resolves is dropped rather than drawn as a
// broken icon.
if (!entry)
continue;
let windows = [];
for (let j = 0; j < order.length; j++) {
if (!claimed[order[j]] && root.matches(order[j], entry, pinned[i])) {
claimed[order[j]] = true;
windows = windows.concat(groups[order[j]]);
}
}
out.push({
entry: entry,
windows: windows,
appId: pinned[i]
});
}
for (let i = 0; i < order.length; i++) {
const appId = order[i];
if (claimed[appId])
continue;
out.push({
entry: DesktopEntries.heuristicLookup(appId),
windows: groups[appId],
appId: appId
});
}
return out;
}
// Wayland app ids and desktop entry ids agree often but not always
// (`org.gnome.Nautilus` vs `nautilus`, `Steam` vs `steam`), so compare the
// usual candidates case-insensitively and fall back to the last
// reverse-DNS segment.
function matches(appId: string, entry: var, pinId: string): bool {
const a = appId.toLowerCase();
if (a === pinId.toLowerCase())
return true;
if (entry.id && a === entry.id.toLowerCase())
return true;
if (entry.startupClass && a === entry.startupClass.toLowerCase())
return true;
const tail = a.split(".").pop();
return tail === pinId.toLowerCase().split(".").pop();
}
// ── Appearance ──────────────────────────────────────────────────────────
radius: Theme.dockRadius
color: Theme.alpha(Theme.bgPanel, Theme.dockAlpha)
border.width: 0 // QTBUG-137166: rounded translucent rects lose their corners
// The prism edge — see widgets/PrismEdge.qml.
PrismEdge {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
inset: parent.radius
}
implicitWidth: row.implicitWidth + Theme.dockPadding * 2
implicitHeight: row.implicitHeight + Theme.dockPadding * 2
// The item the tooltip is currently describing, or null.
property Item hoveredItem: null
Row {
id: row
anchors.centerIn: parent
spacing: Theme.dockGap
ShowAppsButton {
id: showApps
anchors.verticalCenter: parent.verticalCenter
onEntered: root.hoveredItem = showApps
onExited: if (root.hoveredItem === showApps)
root.hoveredItem = null
}
// Separator between the launcher and the apps, as in GNOME's dash.
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: Theme.dockIconSize * 0.7
border.width: 0
color: Theme.alpha(Theme.fg, 0.14)
}
Repeater {
model: root.items
DockItem {
id: dockItem
required property var modelData
app: modelData
anchors.verticalCenter: parent.verticalCenter
onEntered: root.hoveredItem = dockItem
onExited: if (root.hoveredItem === dockItem)
root.hoveredItem = null
}
}
}
// ── Tooltip ─────────────────────────────────────────────────────────────
// Styled like widgets/Popover.qml. Deliberately outside the dock's bounds
// (negative y): it renders fine there, and the dock's input mask does not
// cover it, so it can never swallow a click.
Rectangle {
id: tooltip
readonly property string text: root.hoveredItem ? root.hoveredItem.label : ""
visible: opacity > 0
opacity: root.hoveredItem && tipLabel.text ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.durFast
}
}
// Centered on the hovered item, clamped inside the dock. Reads the item's
// x directly (rather than mapToItem) so the binding re-evaluates when
// the row reflows.
x: {
if (!root.hoveredItem)
return 0;
const center = row.x + root.hoveredItem.x + root.hoveredItem.width / 2;
return Math.max(4, Math.min(root.width - width - 4, center - width / 2));
}
y: -height - 8
width: tipLabel.implicitWidth + Theme.popoverPadding * 2
height: tipLabel.implicitHeight + 8
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.08)
Text {
id: tipLabel
anchors.centerIn: parent
text: tooltip.text
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
}
}