WifiList and the notification toasts built their model from a plain computed array, so any background property tick (a scan result, an unrelated notification arriving) reassigned the whole array and the Repeater destroyed and recreated every delegate -- including one with an open, focused password field or an in-progress reply. Switched both to a ScriptModel, which diffs by identity instead of resetting. ValueSlider had its pointer-to-value mapping offset by 16px (the hit-area margin was applied with the wrong sign), so 0% was unreachable and every click landed to the right of where it was placed -- affects every slider in the shell. SliderRow used -1 as a sentinel for "nothing pending," which collides with legitimate negative preference values like pointer sensitivity. Dock intellihide read the globally focused workspace instead of each monitor's own, so an empty workspace on one screen could hide the dock on another; ActivityPanel rebuilt every row once a second during a recording because the elapsed-time read lived in the model construction instead of each row's own binding. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
179 lines
7.2 KiB
QML
179 lines
7.2 KiB
QML
// The dock — a replacement for Dash-to-Dock configured as: bottom edge, 48px
|
|
// icons, intellihide against all windows, dot running-indicators, show-apps
|
|
// button at the leading edge.
|
|
//
|
|
// Intellihide means the dock reserves no space at all (exclusiveZone 0) and
|
|
// floats over windows, appearing when the pointer reaches the bottom edge or
|
|
// when nothing is in the way.
|
|
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Hyprland
|
|
import QtQuick
|
|
import qs.config
|
|
import qs.services
|
|
|
|
PanelWindow {
|
|
id: root
|
|
|
|
// Set by Variants when instantiated per-screen. Plain rather than
|
|
// `required` so the dock can also be created standalone while testing;
|
|
// Variants injects into the existing property either way.
|
|
property var modelData: null
|
|
screen: root.modelData
|
|
|
|
anchors {
|
|
bottom: true
|
|
left: true
|
|
right: true
|
|
}
|
|
|
|
color: "transparent"
|
|
|
|
// A dock that reserved space would not be intellihiding.
|
|
exclusiveZone: 0
|
|
exclusionMode: ExclusionMode.Ignore
|
|
|
|
// Matched by the `qs-dock` layer rule in hypr/rules.lua — do not rename.
|
|
WlrLayershell.namespace: "qs-dock"
|
|
WlrLayershell.layer: WlrLayer.Top
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
|
|
// ── Geometry ────────────────────────────────────────────────────────────
|
|
// Height = room for the tooltip above the bar + the bar + the gap under it.
|
|
readonly property int revealStripHeight: 3
|
|
readonly property int bottomMargin: Theme.barGap
|
|
readonly property int tooltipSpace: 34
|
|
|
|
implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin
|
|
|
|
// ── Intellihide ─────────────────────────────────────────────────────────
|
|
// This instance's own monitor, the same lookup Workspaces.qml uses to
|
|
// scope a per-screen bar to its own screen. Falls back to null when this
|
|
// Dock is created standalone (no `screen` set) rather than via Variants.
|
|
readonly property HyprlandMonitor monitor: root.screen ? Hyprland.monitorFor(root.screen) : null
|
|
|
|
// Hyprland does not expose live toplevel geometry, so exact overlap cannot
|
|
// be computed. "Is anything on this workspace at all" is the robust proxy,
|
|
// and it is what Dash-to-Dock's all-windows intellihide felt like in
|
|
// practice: an empty workspace keeps the dock out.
|
|
//
|
|
// Deliberately this instance's own monitor's active workspace, not the
|
|
// globally-focused one -- with one Dock per screen, keying off the global
|
|
// focus would make focusing an empty workspace on monitor A hide the dock
|
|
// on monitor B even though B's own workspace is still busy.
|
|
readonly property bool workspaceOccupied: {
|
|
const ws = root.monitor ? root.monitor.activeWorkspace : Hyprland.focusedWorkspace;
|
|
return !!ws && ws.toplevels.values.length > 0;
|
|
}
|
|
|
|
readonly property bool wantRevealed: !Settings.dockAutohide || !workspaceOccupied || pointer.hovered
|
|
|
|
property bool revealed: true
|
|
|
|
onWantRevealedChanged: {
|
|
if (wantRevealed) {
|
|
hideTimer.stop();
|
|
revealTimer.restart();
|
|
} else {
|
|
revealTimer.stop();
|
|
hideTimer.restart();
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: revealTimer
|
|
interval: Settings.dockRevealDelayMs
|
|
onTriggered: root.revealed = true
|
|
}
|
|
|
|
Timer {
|
|
id: hideTimer
|
|
interval: Settings.dockHideDelayMs
|
|
onTriggered: root.revealed = false
|
|
}
|
|
|
|
// Other modules (the bar, the capture overlay) read this. It is one
|
|
// shared flag but there is one Dock per monitor, so only the instance on
|
|
// the currently-focused monitor is allowed to write it -- otherwise
|
|
// whichever instance last changed reveal state would stomp the others,
|
|
// and a reader would see an arbitrary monitor's value. This scopes the
|
|
// flag to mean "is the dock revealed on the monitor the user is on",
|
|
// which is what a capture overlay or the bar actually care about.
|
|
// (A true per-monitor flag would need ShellState.dockRevealed itself to
|
|
// become keyed by screen, which is out of scope here -- see the report.)
|
|
readonly property bool isFocusedMonitorInstance: root.monitor === null || root.monitor === Hyprland.focusedMonitor
|
|
|
|
onRevealedChanged: root._syncShellState()
|
|
onIsFocusedMonitorInstanceChanged: root._syncShellState()
|
|
|
|
function _syncShellState(): void {
|
|
if (root.isFocusedMonitorInstance)
|
|
ShellState.dockRevealed = root.revealed;
|
|
}
|
|
|
|
// wantRevealed's first evaluation emits no change signal when it lands on
|
|
// false (the default), so the initial state has to be taken explicitly —
|
|
// otherwise a shell started on a busy workspace would leave the dock up.
|
|
Component.onCompleted: {
|
|
revealed = wantRevealed;
|
|
root._syncShellState();
|
|
}
|
|
|
|
// ── Input region ────────────────────────────────────────────────────────
|
|
// Revealed: the bar plus everything below it, so crossing the gap under the
|
|
// dock does not count as leaving. Hidden: a sliver along the screen edge,
|
|
// which is the only thing that can still trigger the dock — every other
|
|
// click passes straight through to the window underneath.
|
|
mask: Region {
|
|
item: maskItem
|
|
}
|
|
|
|
Item {
|
|
id: surface
|
|
anchors.fill: parent
|
|
|
|
// Reports the pointer anywhere inside the input region. A HoverHandler
|
|
// rather than a MouseArea because it keeps reporting while the icons'
|
|
// own MouseAreas are hovered.
|
|
HoverHandler {
|
|
id: pointer
|
|
}
|
|
|
|
Item {
|
|
id: maskItem
|
|
x: root.revealed ? body.x : 0
|
|
y: root.revealed ? body.y : surface.height - root.revealStripHeight
|
|
width: root.revealed ? body.width : surface.width
|
|
height: root.revealed ? surface.height - body.y : root.revealStripHeight
|
|
}
|
|
|
|
DockBody {
|
|
id: body
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
|
|
// Slides off the bottom edge when hidden.
|
|
y: root.revealed ? root.tooltipSpace : surface.height
|
|
opacity: root.revealed ? 1 : 0
|
|
|
|
// Asymmetric on purpose. Revealing is a response to something the
|
|
// user just did, so it has to feel immediate — any delay there
|
|
// reads as lag. Hiding is not a response to anything, so it can
|
|
// take its time and stay calm in peripheral vision.
|
|
Behavior on y {
|
|
NumberAnimation {
|
|
duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
|
|
easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: root.revealed ? Theme.durDockReveal : Theme.durNormal
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|