Files
Panama/config/dot/quickshell/modules/dock/DockItem.qml
T

169 lines
5.4 KiB
QML

// One dock icon: the app, its running-window dots, hover scale and click
// behaviour. Reproduces Dash-to-Dock's item: 48px icon, up to four dots
// underneath, grow-on-hover, click to focus / cycle / launch.
import Quickshell
import Quickshell.Hyprland
import Quickshell.Widgets
import QtQuick
import qs.config
Item {
id: root
// { entry: DesktopEntry|null, windows: [HyprlandToplevel], appId: string }
// Built by DockBody so this file stays presentational.
required property var app
readonly property DesktopEntry entry: app && app.entry ? app.entry : null
readonly property var windows: app && app.windows ? app.windows : []
readonly property string label: entry ? entry.name : (app && app.appId ? app.appId : "")
readonly property bool running: windows.length > 0
readonly property bool hovered: mouse.containsMouse
// Emitted so DockBody can drive the single shared tooltip.
signal entered
signal exited
// The icon may grow past the cell on hover; the cell itself stays a fixed
// size so the row doesn't reflow.
implicitWidth: Theme.dockIconSize
implicitHeight: Theme.dockIconSize + dots.height + 4
// Desktop entries usually carry a freedesktop icon *name*, but some ship an
// absolute path. iconPath() only understands names, so branch on it. The
// `true` argument makes a missing icon return "" instead of a placeholder
// that renders as a black square in some themes.
readonly property string iconSource: {
const name = entry && entry.icon ? entry.icon : (app && app.appId ? app.appId : "");
if (!name)
return "";
if (name.startsWith("/"))
return "file://" + name;
return Quickshell.iconPath(name, true);
}
Item {
id: iconSlot
width: Theme.dockIconSize
height: Theme.dockIconSize
anchors.horizontalCenter: parent.horizontalCenter
y: 0
// Grow upward out of the dock, the way GNOME's dash does, so the icon
// never overlaps its own running dots.
transformOrigin: Item.Bottom
scale: root.hovered ? 1.15 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.durFast
easing.type: Easing.OutBack
easing.overshoot: 1.6
}
}
IconImage {
anchors.fill: parent
visible: root.iconSource !== ""
source: root.iconSource
asynchronous: true
mipmap: true
}
// Last resort for an app with no resolvable icon: an initial, which is
// still recognisable, unlike the theme's broken-icon placeholder.
Rectangle {
anchors.fill: parent
visible: root.iconSource === ""
radius: Theme.cardRadius
border.width: 0
color: Theme.alpha(Theme.fg, Theme.activeAlpha)
Text {
anchors.centerIn: parent
text: root.label ? root.label.charAt(0).toUpperCase() : "?"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Math.round(Theme.dockIconSize * 0.5)
}
}
}
// Running indicators. Dash-to-Dock showed one dot per window up to four.
Row {
id: dots
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
spacing: 3
height: 4
Repeater {
model: Math.min(root.windows.length, 4)
Rectangle {
width: 4
height: 4
radius: 2
border.width: 0 // QTBUG-137166: transparent rounded rects punch corners
color: Theme.accent
}
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onEntered: root.entered()
onExited: root.exited()
onClicked: mev => {
// Middle click always starts a new instance, as in GNOME.
if (mev.button === Qt.MiddleButton) {
root.launch();
return;
}
if (root.running)
root.focusNext();
else
root.launch();
}
}
function launch(): void {
if (root.entry)
root.entry.execute();
}
// Clicking a running app cycles through its windows, matching GNOME's dash.
function focusNext(): void {
const wins = root.windows;
if (wins.length === 0)
return;
let next = wins[0];
for (let i = 0; i < wins.length; i++) {
if (wins[i].activated) {
next = wins[(i + 1) % wins.length];
break;
}
}
if (!next)
return;
// Quickshell reports the address without the 0x prefix Hyprland's
// window selector expects; normalise so either form works.
if (next.address) {
const addr = next.address.startsWith("0x") ? next.address : "0x" + next.address;
Hyprland.dispatch(`hl.dsp.focus({ window = "address:${addr}" })`);
} else if (next.wayland) {
// No address yet (the toplevel is still being reported) — the
// wlr handle can still raise it.
next.wayland.activate();
}
}
}