Dock + pill z-order and icon centering pass; GlobalStates.qml and a ConflictKiller service for the ii sidebar.
317 lines
15 KiB
QML
317 lines
15 KiB
QML
// Pixel3Arch patch to ii's stock Dock.qml (2026-07-07).
|
|
//
|
|
// Problem: the dock (pinned, exclusiveZone claiming real screen space) and
|
|
// wvkbd (the on-screen keyboard, see OnScreenKeyboard.qml) both anchor to
|
|
// the bottom edge and both claim exclusive zones — their reservations
|
|
// stack instead of one giving way, so the keyboard/its focus-grab shield
|
|
// drift out of alignment with each other depending on what else is
|
|
// reserving space. Simplest real fix: the dock gets out of the way
|
|
// entirely while the keyboard is open, instead of trying to make the
|
|
// keyboard-side math account for wherever the dock happens to be.
|
|
//
|
|
// GlobalStates.oskOpen suppresses both reveal-when-idle and the pinned
|
|
// exclusive zone. Double-tapping the pill (see
|
|
// overlays/quickshell-pill/shell.qml) still pulses the dock visible for a
|
|
// few seconds via GlobalStates.dockRevealPulse, for reaching a dock app
|
|
// without first closing the keyboard.
|
|
import qs
|
|
import qs.services
|
|
import qs.modules.common
|
|
import qs.modules.common.widgets
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import QtQuick.Effects
|
|
import QtQuick.Layouts
|
|
import Quickshell.Io
|
|
import Quickshell.Hyprland
|
|
import Quickshell
|
|
import Quickshell.Widgets
|
|
import Quickshell.Wayland
|
|
import Quickshell.Hyprland
|
|
|
|
Scope { // Scope
|
|
id: root
|
|
property bool pinned: Config.options?.dock.pinnedOnStartup ?? false
|
|
|
|
// Dock visibility as one explicit state instead of five OR'd booleans
|
|
// (Phosh's lesson: a small enum beats an overlapping-boolean knot).
|
|
// HIDDEN - fully tucked below the edge
|
|
// PEEK - hover strip / empty-desktop / pill-pulse showing it briefly
|
|
// SHOWN - visible but not claiming exclusive space
|
|
// PINNED - visible AND reserving an exclusive zone
|
|
// OSK-open and previewPopup-hover are *inputs* to this, not states.
|
|
enum DockState { Hidden, Peek, Shown, Pinned }
|
|
|
|
// The real pin, suppressed while the OSK is open unless the user is mid
|
|
// pill-pulse. Kept as a helper the state below reads.
|
|
property bool effectivePinned: root.pinned && (!GlobalStates.oskOpen || GlobalStates.dockRevealPulse)
|
|
|
|
// requestDockShow (previewPopup hover) is threaded up from DockApps via
|
|
// this alias so the state computation can see it in one place.
|
|
property bool previewShowing: false
|
|
|
|
// App mode: any fullscreen window on the focused monitor owns the
|
|
// display, so the dock hides. ii's own screenCorners/Background use this
|
|
// same scan (activeToplevel.wayland.fullscreen is NOT reliable); mirror
|
|
// it verbatim. dockRevealPulse still overrides so swipe-up can flash it.
|
|
readonly property var focusedWorkspaces: Hyprland.workspaces.values.filter(
|
|
ws => ws.monitor && ws.monitor.name === Hyprland.focusedMonitor?.name)
|
|
readonly property var activeFullscreenWorkspace: root.focusedWorkspaces.filter(
|
|
ws => ws.active && ws.toplevels.values.filter(w => w.wayland?.fullscreen)[0] !== undefined)[0]
|
|
readonly property bool activeMonitorHasFullscreen: root.activeFullscreenWorkspace !== undefined
|
|
|
|
function computeDockState() {
|
|
if (root.activeMonitorHasFullscreen && !GlobalStates.dockRevealPulse)
|
|
return Dock.DockState.Hidden;
|
|
if (root.effectivePinned)
|
|
return Dock.DockState.Pinned;
|
|
if (GlobalStates.dockRevealPulse || root.previewShowing)
|
|
return Dock.DockState.Peek;
|
|
// Empty desktop (nothing focused) reveals the dock, unless the OSK
|
|
// took the bottom edge.
|
|
if (!GlobalStates.oskOpen && !ToplevelManager.activeToplevel?.activated)
|
|
return Dock.DockState.Peek;
|
|
return Dock.DockState.Hidden;
|
|
}
|
|
|
|
property int dockState: computeDockState()
|
|
|
|
// Two-stage bottom-edge swipe (hyprgrass edge:d:u -> `qs -c ii ipc call
|
|
// dock swipeUp`): first swipe pulses the dock visible for a few
|
|
// seconds; swiping again while it's showing (or when it's pinned
|
|
// anyway) escalates to the overview. Swipe with the overview open
|
|
// closes it again.
|
|
IpcHandler {
|
|
target: "dock"
|
|
|
|
function swipeUp(): void {
|
|
if (GlobalStates.overviewOpen) {
|
|
GlobalStates.overviewOpen = false;
|
|
return;
|
|
}
|
|
// Dock already visible (Peek/Shown/Pinned) -> escalate to overview.
|
|
if (root.dockState !== Dock.DockState.Hidden) {
|
|
GlobalStates.dockRevealPulse = false;
|
|
GlobalStates.overviewOpen = true;
|
|
} else {
|
|
GlobalStates.pulseDockReveal();
|
|
}
|
|
}
|
|
|
|
// Swipe down while the dock is showing dismisses it: clears the
|
|
// pulse and, if the overview is up, closes that instead.
|
|
function swipeDown(): void {
|
|
if (GlobalStates.overviewOpen) {
|
|
GlobalStates.overviewOpen = false;
|
|
return;
|
|
}
|
|
GlobalStates.dockRevealPulse = false;
|
|
}
|
|
|
|
function reveal(): void {
|
|
GlobalStates.pulseDockReveal();
|
|
}
|
|
|
|
// Toggle app-mode fullscreen on the REAL active window. The pill can't
|
|
// use `hyprctl dispatch fullscreen` because tapping the pill makes the
|
|
// shell (org.quickshell) the focused surface, so hyprctl maximized the
|
|
// pill, not the app. ToplevelManager.activeToplevel is the last real
|
|
// app toplevel — a layer-shell tap never becomes activeToplevel — and
|
|
// its .wayland.fullscreen is writable (Quickshell ToplevelHandle::
|
|
// setFullscreen). Same ii-side-sees-the-real-window trick the dock
|
|
// buttons use with .activate().
|
|
function fullscreen(): void {
|
|
// App-mode fullscreen = mode 0 (whole display, no gaps/border).
|
|
// Hyprland.activeToplevel is Hyprland's real active APP window and
|
|
// its .address is a stable window handle — a layer-shell pill tap
|
|
// never becomes a Hyprland toplevel, so this stays the app even
|
|
// after the tap focuses the shell. Dispatch AT that address so we
|
|
// fullscreen the app, not whatever hyprctl thinks is focused.
|
|
const raw = Hyprland.activeToplevel?.address;
|
|
if (!raw) return;
|
|
// .address may or may not carry the 0x prefix; normalize to exactly
|
|
// one. Selector "address:0x..." is verified working on this fork.
|
|
const addr = raw.startsWith("0x") ? raw : "0x" + raw;
|
|
Quickshell.execDetached(["hyprctl", "dispatch",
|
|
`hl.dsp.window.fullscreen({ window = "address:${addr}", mode = 0 })`]);
|
|
}
|
|
}
|
|
|
|
// Settings-surface stub (entry point b). souveraine-settings doesn't
|
|
// exist yet; the long-hold "App settings…" menu calls dockSettings.openApp
|
|
// here. For now it just pulses the dock and logs, so nothing errors and
|
|
// the future settings app has a stable IPC name to take over.
|
|
IpcHandler {
|
|
target: "dockSettings"
|
|
|
|
function openApp(appId: string): void {
|
|
console.log("[dockSettings] openApp stub for", appId,
|
|
"- souveraine-settings not yet installed");
|
|
GlobalStates.pulseDockReveal();
|
|
}
|
|
|
|
function open(): void {
|
|
console.log("[dockSettings] open stub - souveraine-settings not yet installed");
|
|
GlobalStates.pulseDockReveal();
|
|
}
|
|
}
|
|
|
|
Variants {
|
|
// For each monitor
|
|
model: Quickshell.screens
|
|
|
|
PanelWindow {
|
|
id: dockRoot
|
|
// Window
|
|
required property var modelData
|
|
screen: modelData
|
|
|
|
// The dock window maps only when it should paint. This is what
|
|
// actually hides it over a fullscreen app (a Hidden dockState
|
|
// unmounts the layer entirely) — the earlier `visible` was
|
|
// always-true and only `reveal` flipped, so the dock kept
|
|
// painting over fullscreen. hoverToReveal (mouse, off by default)
|
|
// keeps the strip alive for desktop pointer use.
|
|
property bool reveal: root.dockState !== Dock.DockState.Hidden
|
|
|| (Config.options?.dock.hoverToReveal && dockMouseArea.containsMouse)
|
|
visible: !GlobalStates.screenLocked && reveal
|
|
|
|
anchors {
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
exclusiveZone: root.dockState === Dock.DockState.Pinned ? implicitHeight - (Appearance.sizes.hyprlandGapsOut) - (Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut) : 0
|
|
|
|
implicitWidth: dockBackground.implicitWidth
|
|
WlrLayershell.namespace: "quickshell:dock"
|
|
// Overlay, not Top: fullscreen windows render above the Top
|
|
// layer, and the whole point of the edge swipe is to reach the
|
|
// dock from a fullscreen app.
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
color: "transparent"
|
|
|
|
// Content-driven: tall enough for one 64px dock button + the
|
|
// row's 8px bottom margin + the 32px pill strip, with Config
|
|
// dock.height as a floor. A fixed config height (72) left the
|
|
// visible bar ~36px for 64px buttons — icons poked out the
|
|
// bottom and count dots landed under the bar. Size off the
|
|
// BUTTON's implicit height, NOT dockRow.implicitHeight: the
|
|
// separator's Layout margins inflate the row's implicit and
|
|
// made the bar overshoot (content then top-aligned with a dead
|
|
// band underneath).
|
|
implicitHeight: Math.max(Config.options?.dock.height ?? 70,
|
|
overviewButton.implicitHeight + 8 + 32)
|
|
+ Appearance.sizes.elevationMargin + Appearance.sizes.hyprlandGapsOut
|
|
|
|
mask: Region {
|
|
item: dockMouseArea
|
|
}
|
|
|
|
MouseArea {
|
|
id: dockMouseArea
|
|
height: parent.height
|
|
anchors {
|
|
top: parent.top
|
|
topMargin: dockRoot.reveal ? 0 : Config.options?.dock.hoverToReveal ? (dockRoot.implicitHeight - Config.options.dock.hoverRegionHeight) : (dockRoot.implicitHeight + 1)
|
|
horizontalCenter: parent.horizontalCenter
|
|
}
|
|
implicitWidth: dockHoverRegion.implicitWidth + Appearance.sizes.elevationMargin * 2
|
|
hoverEnabled: true
|
|
|
|
Behavior on anchors.topMargin {
|
|
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
|
}
|
|
|
|
Item {
|
|
id: dockHoverRegion
|
|
anchors.fill: parent
|
|
implicitWidth: dockBackground.implicitWidth
|
|
|
|
Item { // Wrapper for the dock background
|
|
id: dockBackground
|
|
anchors {
|
|
top: parent.top
|
|
// Reserve the pill strip: bottom-anchor short of
|
|
// the window's bottom by thePillZone so the dock
|
|
// floats above the pill and never covers it.
|
|
bottom: parent.bottom
|
|
bottomMargin: 32
|
|
horizontalCenter: parent.horizontalCenter
|
|
}
|
|
|
|
implicitWidth: dockRow.implicitWidth + 5 * 2
|
|
height: parent.height - Appearance.sizes.elevationMargin - Appearance.sizes.hyprlandGapsOut - 32
|
|
|
|
StyledRectangularShadow {
|
|
target: dockVisualBackground
|
|
}
|
|
Rectangle { // The real rectangle that is visible
|
|
id: dockVisualBackground
|
|
property real margin: Appearance.sizes.elevationMargin
|
|
anchors.fill: parent
|
|
anchors.topMargin: Appearance.sizes.elevationMargin
|
|
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
|
|
color: Appearance.colors.colLayer0
|
|
border.width: 1
|
|
border.color: Appearance.colors.colLayer0Border
|
|
radius: Appearance.rounding.large
|
|
}
|
|
|
|
RowLayout {
|
|
id: dockRow
|
|
// Anchored to the visible bar. Split top/bottom
|
|
// so the icons ride UP inside the bar instead of
|
|
// drooping out the bottom (uniform margins left
|
|
// them low; less top + more bottom lifts them).
|
|
anchors.fill: dockVisualBackground
|
|
anchors.topMargin: 0
|
|
anchors.bottomMargin: 8
|
|
// The background is built 2*padding wider than the
|
|
// row (dockBackground.implicitWidth); inset the row
|
|
// so that width actually becomes side padding —
|
|
// without it the first icon sat ON the rounded corner.
|
|
anchors.leftMargin: padding
|
|
anchors.rightMargin: padding
|
|
spacing: 3
|
|
property real padding: 5
|
|
|
|
DockApps {
|
|
id: dockApps
|
|
buttonPadding: dockRow.padding
|
|
// Keep the whole dock on-screen: the app list
|
|
// may take at most what's left after the fixed
|
|
// separator + overview button + paddings. Past
|
|
// that it scrolls horizontally.
|
|
maxWidth: dockRoot.width
|
|
- Appearance.sizes.elevationMargin * 2
|
|
- dockSeparator.implicitWidth
|
|
- overviewButton.implicitWidth
|
|
- dockRow.spacing * 2 - 5 * 2
|
|
onRequestDockShowChanged: root.previewShowing = requestDockShow
|
|
}
|
|
DockSeparator {
|
|
id: dockSeparator
|
|
}
|
|
DockButton {
|
|
id: overviewButton
|
|
Layout.fillHeight: true
|
|
onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
|
|
topInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
|
|
bottomInset: Appearance.sizes.hyprlandGapsOut + dockRow.padding
|
|
contentItem: MaterialSymbol {
|
|
anchors.fill: parent
|
|
horizontalAlignment: Text.AlignHCenter
|
|
font.pixelSize: parent.width / 2
|
|
text: "apps"
|
|
color: Appearance.colors.colOnLayer0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|