172 lines
5.6 KiB
QML
172 lines
5.6 KiB
QML
// "Add application to dock", reachable without opening Settings.
|
|
//
|
|
// The Settings page is the place to curate the whole dock -- reorder it, unpin
|
|
// things, change how it hides. Adding one application is a single decision made
|
|
// while looking at the dock, and routing it through a settings window means
|
|
// finding the page, then the card, then the box. This is that box, on its own.
|
|
//
|
|
// It embeds the same DockAppPicker the Dock page uses rather than growing a
|
|
// second search: the exclusion of already-pinned applications, the icon lookup
|
|
// and the "nothing until you type" behavior are all already there, and two
|
|
// copies of them would drift.
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
import qs.modules.settings
|
|
import qs.widgets
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
readonly property bool open: ShellState.dockPickerOpen
|
|
|
|
// The one place a pin is added from outside the Settings page. Returns
|
|
// false for an id nothing installs, so the IPC caller hears about a typo
|
|
// instead of the dock quietly gaining a hole -- DockBody drops a pin it
|
|
// cannot resolve, so a bad id is invisible at runtime.
|
|
function pin(id: string): bool {
|
|
const wanted = String(id ?? "").trim();
|
|
if (!wanted || !DesktopEntries.byId(wanted))
|
|
return false;
|
|
const stored = Settings.dockPinned;
|
|
if (stored.indexOf(wanted) >= 0)
|
|
return true;
|
|
DesktopPreferences.set("dockPinned", stored.concat([wanted]));
|
|
return true;
|
|
}
|
|
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
exclusiveZone: 0
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
// Blurred by the `^qs-popover` rule in hypr/rules.lua; the scrim is painted
|
|
// here rather than added to that rule, the same way the cheatsheet does it.
|
|
WlrLayershell.namespace: "qs-popover-dock-picker"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: root.open
|
|
? WlrKeyboardFocus.Exclusive
|
|
: WlrKeyboardFocus.None
|
|
|
|
// Stays mapped for the length of the close animation, or it vanishes
|
|
// instantly and only the opening is ever seen.
|
|
property bool mapped: false
|
|
visible: root.mapped
|
|
|
|
onOpenChanged: {
|
|
if (root.open) {
|
|
unmapTimer.stop();
|
|
root.mapped = true;
|
|
picker.grab();
|
|
} else {
|
|
unmapTimer.restart();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: unmapTimer
|
|
interval: Theme.durNormal
|
|
onTriggered: root.mapped = false
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
|
|
opacity: root.open ? 1 : 0
|
|
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: ShellState.close()
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: card
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
// High rather than centred: the list grows downwards as you type, and a
|
|
// centred card walks up the screen while you are reading it.
|
|
y: Math.round(parent.height * 0.18)
|
|
width: Math.min(root.width - 120, 520)
|
|
height: header.height + picker.implicitHeight + 56
|
|
|
|
radius: Theme.popoverRadius
|
|
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
|
|
border.width: 1
|
|
border.color: Theme.alpha(Theme.fg, 0.1)
|
|
|
|
opacity: root.open ? 1 : 0
|
|
scale: root.open ? 1 : 0.98
|
|
Behavior on opacity { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
Behavior on scale { NumberAnimation { duration: Theme.durNormal; easing.type: Easing.OutCubic } }
|
|
|
|
PrismEdge {
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
inset: Theme.popoverRadius
|
|
}
|
|
|
|
// Clicks on the card itself must not fall through to the scrim.
|
|
MouseArea { anchors.fill: parent }
|
|
|
|
Item {
|
|
id: header
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.margins: 22
|
|
height: title.implicitHeight
|
|
|
|
Text {
|
|
id: title
|
|
text: "Add application to dock"
|
|
color: Theme.fg
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.DemiBold
|
|
}
|
|
|
|
Text {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: title.verticalCenter
|
|
text: "Esc to close"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
|
|
DockAppPicker {
|
|
id: picker
|
|
|
|
anchors.top: header.bottom
|
|
anchors.topMargin: 16
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.leftMargin: 22
|
|
anchors.rightMargin: 22
|
|
|
|
pinned: Settings.dockPinned
|
|
onPicked: id => {
|
|
root.pin(id);
|
|
ShellState.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
// A Shortcut rather than Keys.onEscapePressed on an item: the search box
|
|
// owns Escape while it has focus (it clears the query first), and a key
|
|
// handler on an ancestor would never see the second press.
|
|
Shortcut {
|
|
sequence: "Escape"
|
|
enabled: root.open
|
|
onActivated: ShellState.close()
|
|
}
|
|
}
|