Make Shell a category, the bar legible, and the dock a real dock
Desktop & Dock becomes Shell — Bar, Dock, Control Center, Tiling, Workspaces — the home for everything Quickshell draws. The settings- management cluster moves to System as Sync & Backup, Appearance's Shell tab dissolves, and 24-hour time finally lives on Date & Time, which always owned it. The bar gets what it never had: a way to survive the wallpaper. A second neutral text family (follow theme, or forced light or dark), a one-layer shadow under every glyph, and a gradient scrim for wallpapers nothing else survives — all off by default, pixel-identical until asked. Widgets earn toggles (weather, media, clipboard, calendar countdown), the vitals cluster stops leaving a dead pill behind, and Control Center's sections learn to step aside. The dock graduates from MVP: a context menu with window rows, pin, unpin, quit and new-window; scroll an icon to cycle its windows; drag to reorder on the dock itself; hover previews with one-shot captures; and "Add App to Dock" in the launcher. Three real bugs died en route — menus that slid away with the autohide, a readonly-property crash on every menu open, and a drag that drifted half a slot per icon on side docks. The pinned-apps editor in Settings becomes a drag strip. 166 contracts; the full suite is green except two live display and switcher tests that cannot run behind a locked session — re-verified on unlock. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
@@ -11,8 +11,8 @@ import qs.config
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// { entry: DesktopEntry|null, windows: [HyprlandToplevel], appId: string }
|
||||
// Built by DockBody so this file stays presentational.
|
||||
// { entry: DesktopEntry|null, windows: [HyprlandToplevel], appId: string,
|
||||
// pinned: bool }. Built by DockBody so this file stays presentational.
|
||||
required property var app
|
||||
|
||||
readonly property DesktopEntry entry: app && app.entry ? app.entry : null
|
||||
@@ -21,16 +21,49 @@ Item {
|
||||
readonly property bool running: windows.length > 0
|
||||
readonly property bool hovered: mouse.containsMouse
|
||||
|
||||
// Set by DockBody. Which way the dock runs decides which axis a drag reads.
|
||||
property bool vertical: false
|
||||
|
||||
// Reordering, driven from DockBody: whether this icon may be dragged at
|
||||
// all, how far it is currently displaced, and whether it is the one being
|
||||
// dragged rather than one being pushed aside.
|
||||
property bool draggable: false
|
||||
property real dragShift: 0
|
||||
property bool dragging: false
|
||||
|
||||
// Emitted so DockBody can drive the single shared tooltip.
|
||||
signal entered
|
||||
signal exited
|
||||
signal contextMenuRequested
|
||||
|
||||
// The drag, reported as travel along the dock's own axis from where the
|
||||
// press landed. DockBody owns what that means.
|
||||
//
|
||||
// The start also carries the pitch -- one cell plus the gap after it --
|
||||
// because the cell size is this file's business: an item is taller than it
|
||||
// is wide (the running dots sit under the icon), so a slot down a side dock
|
||||
// is further than a slot across a bottom one. Measuring it here rather than
|
||||
// recomputing it in DockBody keeps one copy of that arithmetic.
|
||||
signal dragStarted(real pitch)
|
||||
signal dragMoved(real travel)
|
||||
signal dragEnded
|
||||
signal dragCancelled
|
||||
|
||||
// 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
|
||||
|
||||
// Above the icons it is passing.
|
||||
z: root.dragging ? 2 : 0
|
||||
|
||||
// A transform, not an x/y binding: the Grid owns those, and assigning them
|
||||
// in a delegate fights the positioner rather than moving the icon.
|
||||
transform: Translate {
|
||||
x: root.vertical ? 0 : root.dragShift
|
||||
y: root.vertical ? root.dragShift : 0
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -118,10 +151,83 @@ Item {
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
|
||||
// Where the press landed, and whether it has travelled far enough to
|
||||
// stop being a click. The threshold is the whole reason a drag can
|
||||
// share this MouseArea with the launch click: a hand that means to
|
||||
// click never moves eight pixels while the button is down.
|
||||
readonly property int dragThreshold: 8
|
||||
property real pressX: 0
|
||||
property real pressY: 0
|
||||
property bool dragActive: false
|
||||
property bool dragConsumed: false
|
||||
|
||||
// A wheel notch is 120 units, but a touchpad sends far smaller ones and
|
||||
// a free-spinning wheel sends larger. Accumulating and spending whole
|
||||
// notches is what makes both feel the same -- reacting to every event
|
||||
// would make a touchpad flick blur through every window an app owns.
|
||||
property int wheelTravel: 0
|
||||
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
|
||||
onWheel: event => {
|
||||
if (!root.running) {
|
||||
mouse.wheelTravel = 0;
|
||||
return;
|
||||
}
|
||||
mouse.wheelTravel += event.angleDelta.y;
|
||||
while (mouse.wheelTravel >= 120) {
|
||||
mouse.wheelTravel -= 120;
|
||||
root.focusBy(-1);
|
||||
}
|
||||
while (mouse.wheelTravel <= -120) {
|
||||
mouse.wheelTravel += 120;
|
||||
root.focusBy(1);
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: mev => {
|
||||
mouse.pressX = mev.x;
|
||||
mouse.pressY = mev.y;
|
||||
mouse.dragActive = false;
|
||||
mouse.dragConsumed = false;
|
||||
}
|
||||
|
||||
onPositionChanged: mev => {
|
||||
if (!root.draggable || !mouse.pressedButtons)
|
||||
return;
|
||||
const travel = root.vertical ? mev.y - mouse.pressY : mev.x - mouse.pressX;
|
||||
if (!mouse.dragActive) {
|
||||
if (Math.abs(travel) < mouse.dragThreshold)
|
||||
return;
|
||||
mouse.dragActive = true;
|
||||
mouse.dragConsumed = true;
|
||||
root.dragStarted((root.vertical ? root.height : root.width) + Theme.dockGap);
|
||||
}
|
||||
root.dragMoved(travel);
|
||||
}
|
||||
|
||||
onReleased: {
|
||||
if (!mouse.dragActive)
|
||||
return;
|
||||
mouse.dragActive = false;
|
||||
root.dragEnded();
|
||||
}
|
||||
|
||||
onCanceled: {
|
||||
if (!mouse.dragActive)
|
||||
return;
|
||||
mouse.dragActive = false;
|
||||
mouse.dragConsumed = false;
|
||||
root.dragCancelled();
|
||||
}
|
||||
|
||||
onClicked: mev => {
|
||||
// A gesture that reordered the dock is not also a launch.
|
||||
if (mouse.dragConsumed) {
|
||||
mouse.dragConsumed = false;
|
||||
return;
|
||||
}
|
||||
// Middle click always starts a new instance, as in GNOME.
|
||||
if (mev.button === Qt.MiddleButton) {
|
||||
root.launch();
|
||||
@@ -132,7 +238,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
if (root.running)
|
||||
root.focusNext();
|
||||
root.focusBy(1);
|
||||
else
|
||||
root.launch();
|
||||
}
|
||||
@@ -143,19 +249,26 @@ Item {
|
||||
root.entry.execute();
|
||||
}
|
||||
|
||||
// Clicking a running app cycles through its windows, matching GNOME's dash.
|
||||
function focusNext(): void {
|
||||
// Stepping through an app's windows: clicking a running app takes one step
|
||||
// forward, matching GNOME's dash, and the wheel takes one in either
|
||||
// direction. With nothing of this app focused, any step lands on its first
|
||||
// window rather than counting from a window the user is not looking at.
|
||||
function focusBy(delta: int): void {
|
||||
const wins = root.windows;
|
||||
if (wins.length === 0)
|
||||
return;
|
||||
|
||||
let next = wins[0];
|
||||
let current = -1;
|
||||
for (let i = 0; i < wins.length; i++) {
|
||||
if (wins[i].activated) {
|
||||
next = wins[(i + 1) % wins.length];
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const next = current < 0
|
||||
? wins[0]
|
||||
: wins[((current + delta) % wins.length + wins.length) % wins.length];
|
||||
if (!next)
|
||||
return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user