Three things that were parked, and the reasons they were parked turned out to be the useful part of doing them. The Dock can sit on the left or the right as well as the bottom. Everything that assumed the bottom edge is now asked which edge it is on: the anchors, the axis that gets an implicit size, the sliver of input region that survives hiding, the direction the body slides away in, and which side a tooltip opens towards. The body was a Row and is a Grid, because one declaration then serves both orientations -- Row and Column would each need their own children, and the cross-axis anchors that centre items in a Row are the wrong axis in a Column. Bottom is unchanged in every particular, and the settings default to it, so a hot reload in the middle of this work left the running dock exactly where it was. One bug worth recording because static review would never have found it: a dock spans the edge it lives on, which means anchoring BOTH ends of that edge. The first side dock anchored top and left only, was free to collapse to its implicit height, and came out one pixel tall. It parsed, it loaded, and it rendered nothing. The contract measures the geometry rather than reading the source for that reason, and was verified by putting the single-ended anchor back. Per-screen is a list of names where empty means every screen, because a list is what goes stale when a display is unplugged and "all" should not be spelled as one. Turning off the last screen collapses to "all" rather than leaving no dock anywhere and no obvious way back. Pins can be dragged by a grip. The objection this file recorded for a long time was real -- dragging inside a Flickable inside a scrolling page fails in a way that reads as breakage -- and the answer is preventStealing on the grip, so the page cannot claim a gesture that started there. The arrow buttons stay: they are the keyboard-reachable path and a grip is not. The order is held locally during the drag and written once on release, rather than rewriting settings.json for every slot crossed. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
317 lines
12 KiB
QML
317 lines
12 KiB
QML
// Desktop & Dock.
|
|
//
|
|
// The dock timings used to be shown here as text -- "Instant", "250 ms" -- even
|
|
// though they were already stored, mutable integers. They are controls now.
|
|
// The window-layout card keeps text rows because those really are facts about
|
|
// how Panama tiles rather than settings: the adjustable parts of window
|
|
// appearance live on the Appearance page, next to the preview that explains
|
|
// them.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
// Turning the last screen off would leave no dock anywhere and no obvious
|
|
// way back, so the final one cannot be removed -- it collapses to "every
|
|
// screen" instead, which is the same thing on one display and recoverable
|
|
// on several.
|
|
function toggleDockScreen(name: string): void {
|
|
const all = Quickshell.screens.map(screen => String(screen.name));
|
|
const current = Settings.dockScreens.length === 0
|
|
? all.slice()
|
|
: Settings.dockScreens.map(String);
|
|
const at = current.indexOf(name);
|
|
let next = current.slice();
|
|
if (at >= 0)
|
|
next.splice(at, 1);
|
|
else
|
|
next.push(name);
|
|
if (next.length === 0 || next.length === all.length)
|
|
next = [];
|
|
DesktopPreferences.set("dockScreens", next);
|
|
}
|
|
|
|
title: "Desktop & Dock"
|
|
lede: "Keep the shell instant, spatial, and out of your way."
|
|
|
|
SettingsCard {
|
|
title: "Dock"
|
|
|
|
ChoiceRow { setting: "dockPosition" }
|
|
|
|
// One row per connected screen. Nothing selected means every screen,
|
|
// which is stated rather than left as an empty list somebody has to
|
|
// interpret -- and it is what a single-monitor machine should do
|
|
// without being configured at all.
|
|
SettingRow {
|
|
label: "Screens"
|
|
detail: Settings.dockScreens.length === 0
|
|
? "On every display"
|
|
: "On " + Settings.dockScreens.length + " of "
|
|
+ Quickshell.screens.length + " displays"
|
|
visible: Quickshell.screens.length > 1
|
|
controlWidth: 260
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 7
|
|
|
|
Repeater {
|
|
model: Quickshell.screens
|
|
|
|
delegate: Rectangle {
|
|
id: screenPill
|
|
|
|
required property var modelData
|
|
|
|
readonly property string screenName: String(screenPill.modelData.name ?? "")
|
|
// An empty list means all, so every pill reads as on.
|
|
readonly property bool on: Settings.dockScreens.length === 0
|
|
|| Settings.dockScreens.indexOf(screenPill.screenName) >= 0
|
|
|
|
width: pillLabel.implicitWidth + 20
|
|
height: 28
|
|
radius: 8
|
|
color: screenPill.on ? Theme.alpha(Theme.accent, 0.22)
|
|
: Theme.alpha(Theme.fg, 0.06)
|
|
border.width: screenPill.on ? 1 : 0
|
|
border.color: Theme.alpha(Theme.accent, 0.5)
|
|
|
|
Text {
|
|
id: pillLabel
|
|
anchors.centerIn: parent
|
|
text: screenPill.screenName
|
|
color: screenPill.on ? Theme.fg : Theme.fgDim
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
|
|
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
|
TapHandler { onTapped: root.toggleDockScreen(screenPill.screenName) }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ToggleRow { setting: "dockAutohide" }
|
|
SliderRow { setting: "dockRevealDelayMs"; zeroLabel: "Instant" }
|
|
SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant" }
|
|
SliderRow { setting: "dockIconSize"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Pinned applications"
|
|
subtitle: "What sits in the Dock whether or not it is running. Order here is the order on screen."
|
|
|
|
DockPinsEditor {
|
|
id: pins
|
|
width: parent.width
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Pin another application"
|
|
|
|
DockAppPicker {
|
|
width: parent.width
|
|
pinned: pins.pinned
|
|
onPicked: id => pins.add(id)
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Window layout"
|
|
subtitle: "Follows the Forge mental model, with native Hyprland tiling."
|
|
|
|
// These were two read-only rows reporting "Tiling" and "Dynamic", which
|
|
// described settings rather than facts -- both are ordinary Hyprland
|
|
// options that simply had no controls. TextRow's own documentation says
|
|
// a setting the user could reasonably change does not belong in it.
|
|
ChoiceRow { setting: "windowLayout" }
|
|
ToggleRow { setting: "preserveSplit" }
|
|
ChoiceRow { setting: "forceSplit" }
|
|
ToggleRow { setting: "windowSnapping" }
|
|
ActionRow {
|
|
label: "Gaps, corners, and effects"
|
|
detail: "Adjusted on the Appearance page, beside a live preview"
|
|
action: "Open Appearance"
|
|
divider: false
|
|
onTriggered: ShellState.openSettingsSection("appearance", "windows")
|
|
}
|
|
}
|
|
|
|
// GNOME's Multitasking panel, in Hyprland's terms.
|
|
// Only meaningful when the layout above is Master and stack. Hidden
|
|
// otherwise, because a card of settings that do nothing under the layout
|
|
// you are actually running is worse than not offering the layout at all.
|
|
SettingsCard {
|
|
visible: DesktopPreferences.get("windowLayout") === "master"
|
|
title: "Master and stack"
|
|
subtitle: "How the master area behaves. These apply only while the tiling layout above is Master and stack."
|
|
|
|
SliderRow { setting: "masterFactor" }
|
|
ChoiceRow { setting: "masterOrientation" }
|
|
ChoiceRow { setting: "masterNewStatus" }
|
|
ToggleRow { setting: "masterNewOnTop"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Window edges"
|
|
subtitle: "How the pointer grabs a window's border, and how floating windows behave near each other and the screen edge."
|
|
|
|
ToggleRow { setting: "resizeOnBorder" }
|
|
SliderRow { setting: "borderGrabArea"; zeroLabel: "Border only" }
|
|
ToggleRow { setting: "hoverIconOnBorder" }
|
|
SliderRow { setting: "snapWindowGap"; zeroLabel: "Touching" }
|
|
SliderRow { setting: "snapMonitorGap"; zeroLabel: "Touching" }
|
|
ToggleRow { setting: "snapRespectGaps"; divider: false }
|
|
}
|
|
|
|
// Hyprland's own interruptions. Panama turns all four off, which is a
|
|
// defensible default and was not previously a decision anyone could
|
|
// reverse without editing looks.lua.
|
|
SettingsCard {
|
|
title: "Hyprland notices"
|
|
subtitle: "Panama hides all of these by default. They are the compositor's own, not Panama's."
|
|
|
|
ToggleRow { setting: "hyprlandLogo" }
|
|
ToggleRow { setting: "hyprlandSplash" }
|
|
ToggleRow { setting: "hyprlandUpdateNews" }
|
|
ToggleRow { setting: "hyprlandDonationNag"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Workspaces & focus"
|
|
subtitle: "Hyprland's workspaces are created and destroyed as you use them, so there is no fixed count to set."
|
|
|
|
ToggleRow { setting: "workspaceBackAndForth" }
|
|
ToggleRow { setting: "allowWorkspaceCycles" }
|
|
ToggleRow { setting: "focusOnActivate" }
|
|
ToggleRow { setting: "mouseMoveFocusesMonitor"; divider: false }
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Focus"
|
|
|
|
SliderRow { setting: "focusDurationMinutes"; divider: false }
|
|
}
|
|
|
|
// Distinct from the snapshots below, which put THIS machine back as it
|
|
// was. This carries settings to a different one, and deliberately leaves
|
|
// behind anything that describes hardware.
|
|
SettingsCard {
|
|
title: "Carry settings to another machine"
|
|
subtitle: SettingsSync.lastError !== ""
|
|
? SettingsSync.lastError
|
|
: "Everything except what describes this machine: the display arrangement stays here."
|
|
|
|
ActionRow {
|
|
label: "Export"
|
|
detail: SettingsSync.lastAction === "export" && SettingsSync.carried > 0
|
|
? SettingsSync.carried + " settings written to " + SettingsSync.defaultPath
|
|
: "Writes " + SettingsSync.defaultPath
|
|
action: "Export"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.exportTo(SettingsSync.defaultPath)
|
|
}
|
|
|
|
ActionRow {
|
|
label: "See what an import would change"
|
|
detail: SettingsSync.previewed
|
|
? SettingsSync.changes.length + " would change, "
|
|
+ SettingsSync.skipped.length + " skipped"
|
|
: "Reads " + SettingsSync.defaultPath + " without applying anything"
|
|
action: "Preview"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.preview(SettingsSync.defaultPath)
|
|
}
|
|
|
|
// Only offered once a preview has said what it would do. Importing
|
|
// settings sight unseen is how somebody ends up wondering why their
|
|
// desktop changed.
|
|
ActionRow {
|
|
visible: SettingsSync.previewed && SettingsSync.changes.length > 0
|
|
label: "Apply those " + SettingsSync.changes.length + " changes"
|
|
detail: "Settings the file does not mention are left alone"
|
|
action: "Import"
|
|
enabled: !SettingsSync.busy
|
|
onTriggered: SettingsSync.importFrom(SettingsSync.defaultPath)
|
|
}
|
|
|
|
Repeater {
|
|
model: SettingsSync.previewed ? SettingsSync.skipped : []
|
|
|
|
delegate: TextRow {
|
|
required property var modelData
|
|
width: parent.width
|
|
label: String(modelData.key ?? "")
|
|
detail: "Skipped: " + String(modelData.reason ?? "")
|
|
value: ""
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
visible: SettingsSync.lastAction === "import" && SettingsSync.lastError === ""
|
|
label: SettingsSync.applied === 0
|
|
? "Nothing needed changing"
|
|
: SettingsSync.applied + " settings applied"
|
|
detail: "From " + (SettingsSync.exportedFrom || "the export")
|
|
value: ""
|
|
divider: false
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Snapshots"
|
|
subtitle: SettingsBackup.lastError !== ""
|
|
? SettingsBackup.lastError
|
|
: "Your whole desktop configuration is one file, so a snapshot is a copy of it. Restoring also snapshots what it replaces, so it is itself undoable."
|
|
|
|
ActionRow {
|
|
label: "Back up current settings"
|
|
detail: SettingsBackup.snapshots.length === 0
|
|
? "No snapshots yet"
|
|
: SettingsBackup.snapshots.length + (SettingsBackup.snapshots.length === 1 ? " snapshot kept" : " snapshots kept") + ", newest first"
|
|
action: "Back up now"
|
|
enabled: !SettingsBackup.busy
|
|
divider: SettingsBackup.snapshots.length > 0
|
|
onTriggered: SettingsBackup.save()
|
|
}
|
|
|
|
Repeater {
|
|
id: snapshotRows
|
|
model: SettingsBackup.snapshots
|
|
|
|
ActionRow {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
label: modelData.when
|
|
detail: modelData.keys + " settings"
|
|
action: "Restore"
|
|
enabled: !SettingsBackup.busy
|
|
divider: index < snapshotRows.count - 1
|
|
onTriggered: SettingsBackup.restore(modelData.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingsCard {
|
|
title: "Reset"
|
|
subtitle: "Restores the appearance, dock, clock, focus, and display policy, and clears your Home accessory arrangement. Pinned applications, files, and paired devices are not changed."
|
|
|
|
ActionRow {
|
|
label: "Restore defaults"
|
|
detail: "Applies immediately, including to the compositor"
|
|
action: "Restore defaults"
|
|
divider: false
|
|
onTriggered: SystemSettings.restoreDefaults()
|
|
}
|
|
}
|
|
}
|