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:
@@ -73,7 +73,6 @@ SettingsPage {
|
||||
{ value: "background", label: "Background" },
|
||||
{ value: "type", label: "Typography" },
|
||||
{ value: "windows", label: "Windows" },
|
||||
{ value: "shell", label: "Shell" },
|
||||
]
|
||||
current: root.tab
|
||||
onSelected: value => root.tab = value
|
||||
@@ -581,48 +580,4 @@ SettingsPage {
|
||||
ToggleRow { setting: "animationsEnabled"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
visible: root.tab === "shell"
|
||||
title: "Clock"
|
||||
|
||||
ToggleRow { setting: "use24Hour" }
|
||||
ToggleRow { setting: "showSeconds" }
|
||||
ToggleRow { setting: "showWeekday"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
visible: root.tab === "shell"
|
||||
title: "System vitals"
|
||||
subtitle: "Choose what appears beside the workspace indicator."
|
||||
|
||||
ToggleRow { setting: "showCpu" }
|
||||
ToggleRow { setting: "showMemory" }
|
||||
ToggleRow { setting: "showGpu" }
|
||||
// Only where there is a battery to report on. A desktop should not be
|
||||
// offered a switch for a readout it can never show.
|
||||
ToggleRow { setting: "showBattery"; visible: Battery.available }
|
||||
ToggleRow { setting: "showBatteryPercent"; visible: Battery.available && Settings.showBattery }
|
||||
ToggleRow { setting: "showAgentUsage"; divider: true }
|
||||
// Refresh interval was on the Home page, which split one concept across
|
||||
// two pages -- what the vitals show here, how often they update there.
|
||||
SliderRow { setting: "vitalsIntervalMs"; divider: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing }
|
||||
|
||||
// Only worth asking when there is a choice to make.
|
||||
ChoiceGrid {
|
||||
visible: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing
|
||||
width: parent.width
|
||||
label: "Graphics device"
|
||||
detail: GraphicsDevices.selectionMissing
|
||||
? "The stored device is not present on this machine, so the graphics readout is hidden. Choose one below."
|
||||
: "Which GPU the graphics readout measures."
|
||||
options: GraphicsDevices.devices.map(device => ({
|
||||
value: device.path,
|
||||
label: GraphicsDevices.shortName(device.name)
|
||||
}))
|
||||
current: GraphicsDevices.selectedPath
|
||||
divider: false
|
||||
onPicked: value => GraphicsDevices.select(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// The bar.
|
||||
//
|
||||
// Everything Panama draws along the top edge: how legible it stays, what earns
|
||||
// a place in it, and how often the readouts refresh. The bar floats directly on
|
||||
// the wallpaper, so legibility is a real setting and not a theme detail -- a
|
||||
// theme that reads perfectly against the panel background can disappear
|
||||
// entirely over a bright photograph.
|
||||
//
|
||||
// The Clock and System vitals cards came from Appearance's Shell tab, which was
|
||||
// the wrong home for them: Appearance is about how surfaces look, and these
|
||||
// decide what the bar contains.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
title: "Bar"
|
||||
lede: "The bar sits on whatever wallpaper you chose. These keep it legible on all of them."
|
||||
|
||||
SettingsCard {
|
||||
title: "Visibility"
|
||||
subtitle: "The real bar above this window is the preview — every change here lands on it immediately."
|
||||
|
||||
ChoiceRow { setting: "barTextTone" }
|
||||
ToggleRow { setting: "barTextShadow" }
|
||||
ToggleRow { setting: "barBackdrop"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Widgets"
|
||||
subtitle: "What earns a place in the bar. Indicators that carry state — health, activity, focus, video wallpaper — appear on their own and leave when they are done."
|
||||
|
||||
ToggleRow { setting: "showWeatherWidget" }
|
||||
ToggleRow { setting: "showMediaWidget" }
|
||||
ToggleRow { setting: "showClipboardButton" }
|
||||
ToggleRow { setting: "showCalendarCountdown" }
|
||||
ToggleRow { setting: "showCpu" }
|
||||
ToggleRow { setting: "showMemory" }
|
||||
ToggleRow { setting: "showGpu" }
|
||||
// Only where there is a battery to report on. A desktop should not be
|
||||
// offered a switch for a readout it can never show.
|
||||
ToggleRow { setting: "showBattery"; visible: Battery.available }
|
||||
ToggleRow { setting: "showBatteryPercent"; visible: Battery.available && Settings.showBattery }
|
||||
ToggleRow { setting: "showAgentUsage"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Clock"
|
||||
subtitle: "24-hour time lives in System › Date & Time — it drives the date menu, notifications, and the lock screen too, so it was never just the bar's."
|
||||
|
||||
ToggleRow { setting: "showSeconds" }
|
||||
ToggleRow { setting: "showWeekday"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Vitals"
|
||||
|
||||
SliderRow {
|
||||
setting: "vitalsIntervalMs"
|
||||
divider: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing
|
||||
}
|
||||
|
||||
// Only worth asking when there is a choice to make.
|
||||
ChoiceGrid {
|
||||
visible: GraphicsDevices.devices.length > 1 || GraphicsDevices.selectionMissing
|
||||
width: parent.width
|
||||
label: "Graphics device"
|
||||
detail: GraphicsDevices.selectionMissing
|
||||
? "The stored device is not present on this machine, so the graphics readout is hidden. Choose one below."
|
||||
: "Which GPU the graphics readout measures."
|
||||
options: GraphicsDevices.devices.map(device => ({
|
||||
value: device.path,
|
||||
label: GraphicsDevices.shortName(device.name)
|
||||
}))
|
||||
current: GraphicsDevices.selectedPath
|
||||
divider: false
|
||||
onPicked: value => GraphicsDevices.select(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Control Center.
|
||||
//
|
||||
// The panel behind the bar's right corner. Its sections are real surfaces with
|
||||
// real cost -- Home talks to Home Assistant, Phone to KDE Connect -- so a
|
||||
// machine that has neither should be able to say so once instead of scrolling
|
||||
// past two empty shelves every time the panel opens.
|
||||
//
|
||||
// The accessories themselves are not configured here. Which lights appear,
|
||||
// what they are called, and what order they sit in is one arrangement shared
|
||||
// with the Home page, and it is edited there.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
title: "Control Center"
|
||||
lede: "What the panel offers when it opens."
|
||||
|
||||
SettingsCard {
|
||||
title: "Sections"
|
||||
subtitle: "Turning one off hides it from the panel. Nothing stops working — the settings pages behind each still do."
|
||||
|
||||
ToggleRow { setting: "ccShowFocus" }
|
||||
ToggleRow { setting: "ccShowHome" }
|
||||
ToggleRow { setting: "ccShowPhone"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Accessories"
|
||||
|
||||
ActionRow {
|
||||
label: "Control Center shelf"
|
||||
detail: "Which lights and scenes appear, their names and their order — arranged on Home › My Home"
|
||||
action: "Open My Home"
|
||||
divider: false
|
||||
onTriggered: ShellState.openSettings("my-home")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
// These belong to the machine rather than to Panama, so nothing here is stored
|
||||
// in Panama's settings file -- it would be a second answer to a question the
|
||||
// system already answers. Timezone and network time are read from and written
|
||||
// to timedatectl directly; the clock's presentation lives on Appearance,
|
||||
// because that genuinely is a Panama preference.
|
||||
// to timedatectl directly. 24-hour time is the one presentation choice that
|
||||
// belongs here rather than on Shell › Bar: it drives the date menu,
|
||||
// notification timestamps and the lock screen as well as the bar. The rest of
|
||||
// the bar clock's presentation -- seconds, weekday -- stays with the bar.
|
||||
//
|
||||
// Changing the timezone or network time needs privilege. timedatectl asks
|
||||
// polkit, and a canceled dialog surfaces as an error rather than as a value
|
||||
@@ -30,6 +32,11 @@ SettingsPage {
|
||||
detail: DateTime.timezone === "" ? "Reading the system clock" : DateTime.timezone
|
||||
value: Qt.formatDateTime(clock.date, Settings.use24Hour ? "ddd d MMM HH:mm" : "ddd d MMM h:mm AP")
|
||||
}
|
||||
// Not just the bar's. The same choice reads out in the date menu,
|
||||
// every notification's timestamp, and the lock screen, so it belongs
|
||||
// beside the clock the whole machine shares rather than on a page
|
||||
// about one surface.
|
||||
ToggleRow { setting: "use24Hour" }
|
||||
SettingRow {
|
||||
label: "Set automatically"
|
||||
detail: DateTime.ntpEnabled
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
// 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
|
||||
|
||||
// Which project is one click from being forgotten. Same in-place confirm the
|
||||
// SSH Keys and Snapshots pages use.
|
||||
property string confirmingProject: ""
|
||||
|
||||
// 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 }
|
||||
}
|
||||
|
||||
// Saved here, not created here. A layout is worth recording at the moment
|
||||
// you have it right, so saving is a launcher command; this is where the
|
||||
// ones you kept are reviewed and the ones you did not are removed.
|
||||
SettingsCard {
|
||||
title: "Projects"
|
||||
subtitle: Projects.projects.length > 0
|
||||
? "Saved window layouts. Opening one claims free workspaces, so it never lands on top of what you are doing."
|
||||
: "No saved layouts yet. Arrange your windows, then run \"Save Layout as Project\" from the launcher."
|
||||
|
||||
Repeater {
|
||||
model: Projects.projects
|
||||
|
||||
delegate: SettingRow {
|
||||
id: projectRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string projectName: String(projectRow.modelData.name ?? "")
|
||||
readonly property bool confirming: root.confirmingProject === projectRow.projectName
|
||||
|
||||
label: projectRow.projectName
|
||||
detail: projectRow.confirming
|
||||
? "Forgetting this only removes the layout. Nothing that is open closes."
|
||||
: projectRow.modelData.windows + " windows across "
|
||||
+ projectRow.modelData.workspaces + " workspaces — "
|
||||
+ (projectRow.modelData.applications ?? []).join(", ")
|
||||
divider: projectRow.index < Projects.projects.length - 1
|
||||
controlWidth: 210
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !projectRow.confirming
|
||||
text: "Open"
|
||||
enabled: !Projects.busy
|
||||
onClicked: Projects.open(projectRow.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: projectRow.confirming
|
||||
text: "Forget it"
|
||||
tone: "danger"
|
||||
enabled: !Projects.busy
|
||||
onClicked: {
|
||||
root.confirmingProject = "";
|
||||
Projects.remove(projectRow.projectName);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: projectRow.confirming ? "Keep" : "Forget"
|
||||
enabled: !Projects.busy
|
||||
onClicked: root.confirmingProject =
|
||||
projectRow.confirming ? "" : projectRow.projectName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Projects.lastError !== ""
|
||||
label: "Problem"
|
||||
detail: Projects.lastError
|
||||
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" }
|
||||
ToggleRow { setting: "windowSwallow"; 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,12 @@ Column {
|
||||
|
||||
signal picked(string id)
|
||||
|
||||
// For surfaces that open straight into this picker instead of scrolling to
|
||||
// it, so nothing has to be clicked before typing works.
|
||||
function grab(): void {
|
||||
search.grab();
|
||||
}
|
||||
|
||||
readonly property var matches: {
|
||||
const needle = search.text.trim().toLowerCase();
|
||||
if (needle === "")
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
// Dock.
|
||||
//
|
||||
// The pinned applications come first because they are what the Dock is; the
|
||||
// behaviour card below is how it gets out of the way. 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.
|
||||
|
||||
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: "Dock"
|
||||
lede: "What sits in the Dock, and how it behaves when you are not using it."
|
||||
|
||||
SettingsCard {
|
||||
title: "Pinned applications"
|
||||
subtitle: "Drag an icon to reorder it, hover for the unpin button. Order here is the order on screen; these stay whether or not the application is running."
|
||||
|
||||
DockPinsStrip {
|
||||
id: pins
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Item { width: 1; height: 13 }
|
||||
|
||||
DockAppPicker {
|
||||
width: parent.width
|
||||
pinned: pins.pinned
|
||||
onPicked: id => pins.add(id)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Behavior"
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
// The Dock's pinned applications: reorder, remove, and add.
|
||||
//
|
||||
// The list was a sixteen-entry literal in Settings.qml, so changing what sits
|
||||
// in the Dock meant editing a QML file and reloading the shell. It is a plain
|
||||
// ordered list of desktop entry ids, stored in the shared settings file, which
|
||||
// means it is covered by Restore defaults like everything else.
|
||||
//
|
||||
// Move up / move down rather than drag-and-drop. Dragging inside a Flickable
|
||||
// that is itself inside a scrolling page is a genuinely hard interaction to get
|
||||
// right, and it fails in a way the user reads as the app being broken; two
|
||||
// buttons are unambiguous and keyboard-reachable.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.config
|
||||
import qs.services
|
||||
import qs.widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
readonly property var pinned: {
|
||||
const stored = DesktopPreferences.get("dockPinned");
|
||||
return Array.isArray(stored) ? stored : [];
|
||||
}
|
||||
|
||||
// DesktopEntries populates asynchronously, so this must be read as a
|
||||
// binding rather than looked up inside one -- byId() called during
|
||||
// evaluation registers no dependency and answers from an empty list.
|
||||
readonly property var entriesById: {
|
||||
const index = {};
|
||||
for (const entry of DesktopEntries.applications.values)
|
||||
index[entry.id] = entry;
|
||||
return index;
|
||||
}
|
||||
|
||||
function nameFor(id: string): string {
|
||||
const entry = root.entriesById[id];
|
||||
return entry ? entry.name : id;
|
||||
}
|
||||
|
||||
function commit(next: var): void {
|
||||
DesktopPreferences.set("dockPinned", next);
|
||||
}
|
||||
|
||||
// ── Dragging ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// By a grip rather than the whole row. The objection this file used to
|
||||
// record -- that dragging inside a Flickable inside a scrolling page is
|
||||
// hard to get right and fails in a way that reads as breakage -- is real,
|
||||
// and the answer is preventStealing on the grip: the Flickable cannot take
|
||||
// a gesture that started there, so a vertical drag reorders instead of
|
||||
// scrolling the page out from under it. The arrow buttons stay, because
|
||||
// they are the keyboard-reachable path and a grip is not.
|
||||
//
|
||||
// The order is held here while the drag runs and written once on release.
|
||||
// Committing on every slot crossed would rewrite settings.json a dozen
|
||||
// times for one gesture.
|
||||
property int draggingIndex: -1
|
||||
property var workingOrder: []
|
||||
|
||||
readonly property var displayed: root.draggingIndex >= 0 ? root.workingOrder : root.pinned
|
||||
|
||||
function beginDrag(index: int): void {
|
||||
root.workingOrder = root.pinned.slice();
|
||||
root.draggingIndex = index;
|
||||
}
|
||||
|
||||
function dragTo(target: int): void {
|
||||
if (root.draggingIndex < 0 || target === root.draggingIndex)
|
||||
return;
|
||||
if (target < 0 || target >= root.workingOrder.length)
|
||||
return;
|
||||
const next = root.workingOrder.slice();
|
||||
const moved = next.splice(root.draggingIndex, 1)[0];
|
||||
next.splice(target, 0, moved);
|
||||
root.workingOrder = next;
|
||||
root.draggingIndex = target;
|
||||
}
|
||||
|
||||
function endDrag(): void {
|
||||
if (root.draggingIndex < 0)
|
||||
return;
|
||||
const next = root.workingOrder.slice();
|
||||
root.draggingIndex = -1;
|
||||
root.workingOrder = [];
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
function move(from: int, to: int): void {
|
||||
if (to < 0 || to >= root.pinned.length)
|
||||
return;
|
||||
const next = root.pinned.slice();
|
||||
const moved = next.splice(from, 1)[0];
|
||||
next.splice(to, 0, moved);
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
function remove(index: int): void {
|
||||
const next = root.pinned.slice();
|
||||
next.splice(index, 1);
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
function add(id: string): void {
|
||||
if (root.pinned.indexOf(id) >= 0)
|
||||
return;
|
||||
root.commit(root.pinned.concat([id]));
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.displayed
|
||||
|
||||
SettingRow {
|
||||
id: pin
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
label: root.nameFor(pin.modelData)
|
||||
// The desktop id is shown only when the name alone would not say
|
||||
// which entry this is. It is developer text, and repeating it under
|
||||
// fifteen recognisable application names is noise that makes the
|
||||
// list harder to scan, not easier.
|
||||
detail: root.pinned.filter(other =>
|
||||
root.nameFor(other) === root.nameFor(pin.modelData)).length > 1
|
||||
? pin.modelData
|
||||
: ""
|
||||
divider: pin.index < root.pinned.length - 1
|
||||
controlWidth: 132
|
||||
|
||||
// Lifted while dragging so the row being moved is the one that
|
||||
// looks moved.
|
||||
z: root.draggingIndex === pin.index ? 2 : 0
|
||||
opacity: root.draggingIndex === pin.index ? 0.85 : 1
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
// The grip. preventStealing is the whole reason this works
|
||||
// inside a scrolling page: without it the Flickable claims the
|
||||
// vertical gesture and the row never moves.
|
||||
Item {
|
||||
width: 26
|
||||
height: 26
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "\u2261"
|
||||
color: root.draggingIndex === pin.index ? Theme.accent : Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 15
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: grip
|
||||
anchors.fill: parent
|
||||
preventStealing: true
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
|
||||
property real pressY: 0
|
||||
|
||||
onPressed: mouse => {
|
||||
grip.pressY = mouse.y;
|
||||
root.beginDrag(pin.index);
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (root.draggingIndex < 0 || pin.height <= 0)
|
||||
return;
|
||||
// How many whole rows the pointer has travelled from
|
||||
// where it started. Rounded, so the swap happens as
|
||||
// the grip passes the midpoint of the next row.
|
||||
const travelled = (mouse.y - grip.pressY);
|
||||
const slots = Math.round(travelled / pin.height);
|
||||
if (slots !== 0)
|
||||
root.dragTo(root.draggingIndex + slots);
|
||||
}
|
||||
onReleased: root.endDrag()
|
||||
onCanceled: root.endDrag()
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
text: "↑"
|
||||
enabled: pin.index > 0
|
||||
onClicked: root.move(pin.index, pin.index - 1)
|
||||
}
|
||||
SettingsButton {
|
||||
text: "↓"
|
||||
enabled: pin.index < root.pinned.length - 1
|
||||
onClicked: root.move(pin.index, pin.index + 1)
|
||||
}
|
||||
SettingsButton {
|
||||
text: "Unpin"
|
||||
onClicked: root.remove(pin.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingRow {
|
||||
visible: root.pinned.length === 0
|
||||
label: "Nothing is pinned"
|
||||
detail: "The Dock will only show running applications"
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// The Dock's pinned applications, shown as the dock shows them.
|
||||
//
|
||||
// This replaces a sixteen-row list of names. The list was honest but it was not
|
||||
// the thing being edited: the Dock is a horizontal row of icons, and the order
|
||||
// of that row was being decided in a vertical column of text. Editing a
|
||||
// picture of the result is faster than editing a description of it, and it
|
||||
// removes the translation step where you count rows to work out which icon ends
|
||||
// up third.
|
||||
//
|
||||
// Icons resolve exactly the way the Dock resolves them, so what is drawn here
|
||||
// is what will be drawn there -- including the initial-letter fallback for an
|
||||
// application whose icon the theme cannot find.
|
||||
//
|
||||
// Reordering is a drag, with two things that make a drag safe inside a
|
||||
// scrolling page:
|
||||
//
|
||||
// * preventStealing on the grabbing MouseArea. Without it the page's own
|
||||
// Flickable claims the gesture and the icon never moves, which reads as
|
||||
// breakage rather than as a page that scrolls.
|
||||
// * The order is held here while the drag runs and written once on release.
|
||||
// Committing on every cell crossed would rewrite settings.json a dozen
|
||||
// times for one gesture.
|
||||
//
|
||||
// Every icon is also a tab stop: Left and Right move it, Delete unpins it. A
|
||||
// drag is not reachable from the keyboard, so the keyboard gets its own path
|
||||
// rather than a pair of arrow buttons bolted to each cell.
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: 0
|
||||
|
||||
// The cell is the icon plus the room its unpin button needs. Fixed, because
|
||||
// the drag arithmetic below counts cells rather than measuring them.
|
||||
readonly property int iconSize: 44
|
||||
readonly property int cellSize: 52
|
||||
readonly property int cellSpacing: 10
|
||||
readonly property int stride: root.cellSize + root.cellSpacing
|
||||
|
||||
// How many cells fit on one line. The drag turns a pointer offset into an
|
||||
// index, and on a wrapped strip moving down a line is a jump of this many.
|
||||
readonly property int perRow: Math.max(1,
|
||||
Math.floor((flow.width + root.cellSpacing) / root.stride))
|
||||
|
||||
readonly property var pinned: {
|
||||
const stored = DesktopPreferences.get("dockPinned");
|
||||
return Array.isArray(stored) ? stored : [];
|
||||
}
|
||||
|
||||
// DesktopEntries populates asynchronously, so this must be read as a
|
||||
// binding rather than looked up inside one -- byId() called during
|
||||
// evaluation registers no dependency and answers from an empty list.
|
||||
readonly property var entriesById: {
|
||||
const index = {};
|
||||
for (const entry of DesktopEntries.applications.values)
|
||||
index[entry.id] = entry;
|
||||
return index;
|
||||
}
|
||||
|
||||
function nameFor(id: string): string {
|
||||
const entry = root.entriesById[id];
|
||||
return entry ? entry.name : id;
|
||||
}
|
||||
|
||||
// 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
|
||||
// that renders as a black square in some themes.
|
||||
function iconFor(id: string): string {
|
||||
const entry = root.entriesById[id];
|
||||
const name = entry && entry.icon ? entry.icon : id;
|
||||
if (!name)
|
||||
return "";
|
||||
if (name.startsWith("/"))
|
||||
return "file://" + name;
|
||||
return Quickshell.iconPath(name, true);
|
||||
}
|
||||
|
||||
function commit(next: var): void {
|
||||
DesktopPreferences.set("dockPinned", next);
|
||||
}
|
||||
|
||||
// ── Dragging ────────────────────────────────────────────────────────────
|
||||
|
||||
property int draggingIndex: -1
|
||||
property var workingOrder: []
|
||||
|
||||
readonly property var displayed: root.draggingIndex >= 0 ? root.workingOrder : root.pinned
|
||||
|
||||
function beginDrag(index: int): void {
|
||||
root.workingOrder = root.pinned.slice();
|
||||
root.draggingIndex = index;
|
||||
}
|
||||
|
||||
function dragTo(target: int): void {
|
||||
if (root.draggingIndex < 0 || target === root.draggingIndex)
|
||||
return;
|
||||
if (target < 0 || target >= root.workingOrder.length)
|
||||
return;
|
||||
const next = root.workingOrder.slice();
|
||||
const moved = next.splice(root.draggingIndex, 1)[0];
|
||||
next.splice(target, 0, moved);
|
||||
root.workingOrder = next;
|
||||
root.draggingIndex = target;
|
||||
}
|
||||
|
||||
function endDrag(): void {
|
||||
if (root.draggingIndex < 0)
|
||||
return;
|
||||
const next = root.workingOrder.slice();
|
||||
root.draggingIndex = -1;
|
||||
root.workingOrder = [];
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
// ── Keyboard ────────────────────────────────────────────────────────────
|
||||
//
|
||||
// The Repeater's model is a plain array, so committing a move rebuilds
|
||||
// every delegate and the focused one is destroyed mid-keystroke. The
|
||||
// focused position is remembered here instead of in the delegate, and the
|
||||
// cell that lands on it takes focus back as it is created.
|
||||
|
||||
property int keyboardIndex: -1
|
||||
|
||||
function move(from: int, to: int): void {
|
||||
if (to < 0 || to >= root.pinned.length)
|
||||
return;
|
||||
const next = root.pinned.slice();
|
||||
const moved = next.splice(from, 1)[0];
|
||||
next.splice(to, 0, moved);
|
||||
root.keyboardIndex = to;
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
function remove(index: int): void {
|
||||
const next = root.pinned.slice();
|
||||
next.splice(index, 1);
|
||||
root.keyboardIndex = Math.min(index, next.length - 1);
|
||||
root.commit(next);
|
||||
}
|
||||
|
||||
function add(id: string): void {
|
||||
if (root.pinned.indexOf(id) >= 0)
|
||||
return;
|
||||
root.commit(root.pinned.concat([id]));
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: flow
|
||||
|
||||
width: parent.width
|
||||
visible: root.displayed.length > 0
|
||||
spacing: root.cellSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.displayed
|
||||
|
||||
delegate: Item {
|
||||
id: cell
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property bool dragging: root.draggingIndex === cell.index
|
||||
readonly property string appName: root.nameFor(cell.modelData)
|
||||
readonly property string iconSource: root.iconFor(cell.modelData)
|
||||
|
||||
width: root.cellSize
|
||||
height: root.cellSize
|
||||
|
||||
// The dragged cell rides above its neighbours so the icon being
|
||||
// moved is the one that looks moved.
|
||||
z: cell.dragging ? 2 : 0
|
||||
|
||||
activeFocusOnTab: true
|
||||
Accessible.role: Accessible.Button
|
||||
Accessible.name: cell.appName
|
||||
Accessible.description: "Pinned to the Dock, position "
|
||||
+ (cell.index + 1) + " of " + root.displayed.length
|
||||
+ ". Left and Right move it, Delete unpins it."
|
||||
Accessible.focusable: true
|
||||
Accessible.focused: cell.activeFocus
|
||||
|
||||
Keys.onLeftPressed: root.move(cell.index, cell.index - 1)
|
||||
Keys.onRightPressed: root.move(cell.index, cell.index + 1)
|
||||
Keys.onDeletePressed: root.remove(cell.index)
|
||||
|
||||
onActiveFocusChanged: if (cell.activeFocus) root.keyboardIndex = cell.index
|
||||
Component.onCompleted: if (root.keyboardIndex === cell.index) cell.forceActiveFocus()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onKeyboardIndexChanged(): void {
|
||||
if (root.keyboardIndex === cell.index)
|
||||
cell.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
// Where the icon would land if the drag ended now. It sits in
|
||||
// the gap to the left of the cell rather than under it, because
|
||||
// the question a drop indicator answers is "between which two".
|
||||
Rectangle {
|
||||
visible: cell.dragging
|
||||
anchors.right: parent.left
|
||||
anchors.rightMargin: Math.round(root.cellSpacing / 2) - 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 4
|
||||
height: root.iconSize
|
||||
radius: 2
|
||||
border.width: 0
|
||||
color: Theme.accent
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: tile
|
||||
|
||||
anchors.centerIn: parent
|
||||
width: root.iconSize
|
||||
height: root.iconSize
|
||||
radius: Theme.cardRadius
|
||||
color: cell.dragging
|
||||
? Theme.alpha(Theme.accent, 0.18)
|
||||
: Theme.alpha(Theme.fg, grab.containsMouse || cell.activeFocus ? 0.09 : 0.05)
|
||||
border.width: cell.dragging || cell.activeFocus ? 2 : 1
|
||||
border.color: cell.dragging
|
||||
? Theme.accent
|
||||
: (cell.activeFocus ? Theme.accentSecondary : Theme.alpha(Theme.fg, 0.1))
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
visible: cell.iconSource !== ""
|
||||
source: cell.iconSource
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
// Last resort for an app with no resolvable icon: an
|
||||
// initial, which is still recognizable, unlike the theme's
|
||||
// broken-icon placeholder.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: cell.iconSource === ""
|
||||
text: cell.appName ? cell.appName.charAt(0).toUpperCase() : "?"
|
||||
color: Theme.fg
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Math.round(root.iconSize * 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
// The whole tile is the grip. preventStealing is the reason a
|
||||
// drag works at all inside a scrolling page.
|
||||
MouseArea {
|
||||
id: grab
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
cursorShape: cell.dragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
|
||||
property real pressX: 0
|
||||
property real pressY: 0
|
||||
|
||||
onPressed: mouse => {
|
||||
grab.pressX = mouse.x;
|
||||
grab.pressY = mouse.y;
|
||||
cell.forceActiveFocus();
|
||||
root.beginDrag(cell.index);
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (root.draggingIndex < 0)
|
||||
return;
|
||||
// How many whole cells the pointer has travelled from
|
||||
// where it started, across and down. Rounded, so the
|
||||
// swap happens as the icon passes the midpoint of its
|
||||
// neighbour; a line down is a jump of one full row.
|
||||
const columns = Math.round((mouse.x - grab.pressX) / root.stride);
|
||||
const rows = Math.round((mouse.y - grab.pressY) / root.stride);
|
||||
const slots = rows * root.perRow + columns;
|
||||
if (slots !== 0)
|
||||
root.dragTo(root.draggingIndex + slots);
|
||||
}
|
||||
onReleased: root.endDrag()
|
||||
onCanceled: root.endDrag()
|
||||
}
|
||||
|
||||
// Unpin. Hidden until the icon is hovered or focused, because
|
||||
// eleven permanent × badges read as an error state rather than
|
||||
// as eleven applications you chose.
|
||||
Rectangle {
|
||||
id: unpin
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
z: 3
|
||||
visible: grab.containsMouse || unpinMouse.containsMouse
|
||||
|| cell.activeFocus
|
||||
color: Theme.danger
|
||||
border.width: 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: Theme.bgDark
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: unpinMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.remove(cell.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
visible: root.pinned.length === 0
|
||||
text: "Nothing is pinned — the Dock will only show running applications. Search below to add one."
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
@@ -80,12 +80,15 @@ Settings because Fedora's system services own those areas.
|
||||
|
||||
## Appearance
|
||||
|
||||
Six tabs, in the order the questions are actually asked: **Themes**, **Theme
|
||||
editor**, **Background**, **Typography**, **Windows**, **Shell**. Themes leads
|
||||
because light and dark is the control reached most often, and it used to be the
|
||||
third section down, under a wallpaper grid and the whole lock screen. The old
|
||||
`theme` section id still resolves to it, so deep links and IPC calls keep
|
||||
working.
|
||||
Five tabs, in the order the questions are actually asked: **Themes**, **Theme
|
||||
editor**, **Background**, **Typography**, **Windows**. Themes leads because
|
||||
light and dark is the control reached most often, and it used to be the third
|
||||
section down, under a wallpaper grid and the whole lock screen. The old `theme`
|
||||
section id still resolves to it, so deep links and IPC calls keep working.
|
||||
|
||||
The sixth tab, **Shell**, is gone: its two cards moved to **Shell › Bar**, which
|
||||
is where somebody who wants to change the bar now looks. Appearance answers what
|
||||
the desktop looks like; Shell answers what the desktop *is*.
|
||||
|
||||
### Themes
|
||||
|
||||
@@ -145,6 +148,109 @@ and Panama's own Settings titlebar shows one button. `panamaTitlebar` turns
|
||||
that bar off entirely, leaving the window pure Hyprland: Super+Q closes,
|
||||
Super+drag moves, Escape still works.
|
||||
|
||||
## Shell
|
||||
|
||||
Everything Quickshell itself draws, in five tabs: **Bar**, **Dock**, **Control
|
||||
Center**, **Tiling**, **Workspaces**. The category used to be **Desktop & Dock**,
|
||||
a single page that held the dock alongside window layout, workspaces, and the
|
||||
settings-management cluster — three unrelated subjects and a filing cabinet. Its
|
||||
`desktop` id is retired to `bar`, so old deep links land on the first tab.
|
||||
|
||||
The split is by *surface*. If you can point at it on screen, it has a tab.
|
||||
|
||||
### Bar
|
||||
|
||||
The first settings surface the bar has ever had. Three cards.
|
||||
|
||||
**Visibility** exists because the bar is the one thing the shell draws that has
|
||||
no ground of its own — it floats on the wallpaper, which the theme has never
|
||||
seen. So a palette that is correct everywhere else can still be unreadable
|
||||
exactly where the clock is. `barTextTone` (`theme`/`light`/`dark`) drives a
|
||||
second neutral family in `Theme.qml` — `barFg`, `barFgDim`, `barFgMuted` — that
|
||||
thirteen bar widgets bind to instead of `fg`/`fgDim`/`fgMuted`. Left on `theme`
|
||||
the family *is* the fg family by identity, so nothing changes for anyone who
|
||||
never asks, and a custom palette still reaches the bar. Semantic tones (warn,
|
||||
danger, accent, ok) never became bar tones: a battery at 4% is red whatever the
|
||||
neutrals were forced to.
|
||||
|
||||
`barTextShadow` and `barBackdrop` are the two escape hatches for a wallpaper no
|
||||
tone wins against. The shadow is **one** `layer.effect` MultiEffect over the
|
||||
whole of `Bar.qml`'s content rather than one per widget, so a widget added
|
||||
tomorrow picks it up without opting in; the backdrop is a static top-down
|
||||
gradient Rectangle behind the content rows. Both default off and both are read
|
||||
from the preference — during the build they briefly landed as literal `true`,
|
||||
which forced a dark band and an extra compositing layer on everyone.
|
||||
`tests/quickshell/bar-visibility-contract` pins both bindings for that reason,
|
||||
along with every widget's use of the bar tones.
|
||||
|
||||
**Widgets** is one toggle per thing that earns a place: weather, media,
|
||||
clipboard, calendar countdown, the three vitals readouts, battery, agent usage.
|
||||
Each toggle is **ANDed** with the widget's own state condition rather than
|
||||
replacing it, so switching one on never conjures a pill with nothing in it.
|
||||
State-driven indicators — health, activity, focus, video wallpaper — get no
|
||||
toggle: they appear when they have something to say and leave when they are
|
||||
done. `VitalsWidget` answers for its own pill as well as its three fields,
|
||||
because an invisible child still occupies its Row and gating the fields alone
|
||||
left a padded, empty pill in the bar.
|
||||
|
||||
**Clock** keeps `showSeconds` and `showWeekday`. `use24Hour` is *not* here: it
|
||||
drives the date menu, notification timestamps, and the lock screen too, so it
|
||||
moved to group `datetime` and lives on **System › Date & Time**. The card says
|
||||
so rather than leaving its absence looking like an oversight.
|
||||
|
||||
Right-clicking the vitals or agent-usage pill opens this page.
|
||||
|
||||
### Dock
|
||||
|
||||
Behavior — position, screens, autohide, reveal and hide delays, icon size —
|
||||
unchanged. **Pinned applications** is now `DockPinsStrip.qml`: the dock's own
|
||||
row of icons, resolved exactly the way the dock resolves them, drag to reorder,
|
||||
hover for the unpin ×, `DockAppPicker` search underneath. It replaces a sixteen-
|
||||
row list of names, which was honest but was not the thing being edited — the
|
||||
order of a horizontal row of icons was being decided in a vertical column of
|
||||
text.
|
||||
|
||||
Two details make the drag safe: `preventStealing` on the grabbing MouseArea, or
|
||||
the page's Flickable claims the gesture and the icon never moves; and the order
|
||||
is held in the strip while the drag runs and written **once** on release, rather
|
||||
than rewriting `settings.json` a dozen times for one gesture. Every icon is also
|
||||
a tab stop — Left and Right move it, Delete unpins it — because a drag is not
|
||||
reachable from the keyboard.
|
||||
|
||||
The live dock gained the matching gestures: drag to reorder on the dock itself,
|
||||
a right-click menu (window rows, pin, unpin, quit, new window, and a **Dock
|
||||
settings** entry that deep-links here), scroll an icon to cycle its windows, and
|
||||
hover previews from `DockPreviews.qml`. `DockPickOverlay.qml` plus the `dock`
|
||||
IPC target back the launcher's **Add App to Dock** command, so pinning never
|
||||
requires opening Settings at all.
|
||||
|
||||
### Control Center
|
||||
|
||||
The quick settings panel's first settings surface. **Sections** is one
|
||||
`ccShow*` bool per real section of `QuickSettingsPanel` — Focus, Home, Phone —
|
||||
and only for sections that actually exist; a toggle for a section the panel does
|
||||
not draw is a control that writes a preference nothing reads. Turning one off
|
||||
hides it from the panel and stops nothing: the page behind it still works.
|
||||
**Accessories** is a labeled handoff to Home › My Home rather than a second copy
|
||||
of the shelf editor.
|
||||
|
||||
### Tiling and Workspaces
|
||||
|
||||
Rows moved unchanged. Tiling holds window layout (with the handoff to Appearance
|
||||
› Windows for gaps, corners, and effects), master and stack, window edges, and
|
||||
the Hyprland notices. Workspaces holds the workspace and focus toggles, focus
|
||||
session length, and Projects.
|
||||
|
||||
## System › Sync & Backup
|
||||
|
||||
`SyncPage.qml`, the tenth tab of System: **Carry settings to another machine**
|
||||
(`SettingsSync`), **Settings backups** (`SettingsBackup`), and **Reset**. It was
|
||||
the settings-management cluster on the old Desktop page, which is the one thing
|
||||
on that page that was never a surface. Backups is titled that way deliberately
|
||||
so it stops colliding with the btrfs **Snapshots** tab one along — the two mean
|
||||
different things and used to share a word. The search index's **Restore
|
||||
defaults** entry points here.
|
||||
|
||||
## Adding a setting
|
||||
|
||||
One schema entry. That is the whole job.
|
||||
|
||||
@@ -166,7 +166,12 @@ Rectangle {
|
||||
case "connectivity": return connectivityPage;
|
||||
case "my-home": return myHomePage;
|
||||
case "phone": return phonePage;
|
||||
case "desktop": return desktopPage;
|
||||
case "bar": return barPage;
|
||||
case "dock": return dockPage;
|
||||
case "control-center": return controlCenterPage;
|
||||
case "tiling": return tilingPage;
|
||||
case "workspaces": return workspacesPage;
|
||||
case "sync": return syncPage;
|
||||
case "sound": return soundPage;
|
||||
case "gaming": return gamingPage;
|
||||
case "notifications": return notificationsPage;
|
||||
@@ -246,7 +251,12 @@ Rectangle {
|
||||
Component { id: connectivityPage; ConnectivityPage {} }
|
||||
Component { id: myHomePage; MyHomePage {} }
|
||||
Component { id: phonePage; PhonePage {} }
|
||||
Component { id: desktopPage; DesktopPage {} }
|
||||
Component { id: barPage; BarPage {} }
|
||||
Component { id: dockPage; DockPage {} }
|
||||
Component { id: controlCenterPage; ControlCenterPage {} }
|
||||
Component { id: tilingPage; TilingPage {} }
|
||||
Component { id: workspacesPage; WorkspacesPage {} }
|
||||
Component { id: syncPage; SyncPage {} }
|
||||
Component { id: soundPage; SoundPage {} }
|
||||
Component { id: gamingPage; GamingPage {} }
|
||||
Component { id: notificationsPage; NotificationsPage {} }
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Sync & Backup.
|
||||
//
|
||||
// Three answers to three different questions, which is why they sit together:
|
||||
// carrying settings to a different machine, putting this machine's settings
|
||||
// back as they were, and throwing them away.
|
||||
//
|
||||
// The backups here are copies of the preferences file. They are not the btrfs
|
||||
// snapshots one tab over in System › Snapshots, which put the whole filesystem
|
||||
// back -- the card says so, because the two used to share the word "Snapshots"
|
||||
// and nothing distinguished them.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
title: "Sync & Backup"
|
||||
lede: "Carry your settings to another machine, keep copies of them, or start over."
|
||||
|
||||
// Distinct from the backups 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: "Settings backups"
|
||||
subtitle: SettingsBackup.lastError !== ""
|
||||
? SettingsBackup.lastError
|
||||
: "Copies of your preferences, not of the filesystem — System › Snapshots keeps the btrfs ones. Your whole desktop configuration is a single file, so a backup is a copy of it. Restoring also backs up what it replaces, so it is itself undoable."
|
||||
|
||||
ActionRow {
|
||||
label: "Back up current settings"
|
||||
detail: SettingsBackup.snapshots.length === 0
|
||||
? "No backups yet"
|
||||
: SettingsBackup.snapshots.length + (SettingsBackup.snapshots.length === 1 ? " backup kept" : " backups 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Tiling.
|
||||
//
|
||||
// How windows share the space. The window-layout card keeps its handoff row
|
||||
// because the adjustable parts of window *appearance* -- gaps, corners,
|
||||
// borders, effects -- live on the Appearance page next to the preview that
|
||||
// explains them. Splitting them would put two halves of one idea on two pages;
|
||||
// pointing at the other half keeps one.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
title: "Tiling"
|
||||
lede: "How windows share the space, and where their edges are."
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,13 @@ Item {
|
||||
|
||||
// Videos join the grid only in single mode — slideshow and per-display
|
||||
// are hyprpaper's modes and stills-only.
|
||||
readonly property var catalog: root.mode === "single"
|
||||
// The test seam: a harness that supplies its own stills sets this false,
|
||||
// and the picker then never touches the VideoWallpaper singleton at all —
|
||||
// merely instantiating it would fire its startup restore inside the
|
||||
// harness and play the machine's real wallpaper in a test instance.
|
||||
property bool videoAware: true
|
||||
|
||||
readonly property var catalog: root.mode === "single" && root.videoAware
|
||||
? Wallpaper.available.concat(VideoWallpaper.candidates)
|
||||
: Wallpaper.available
|
||||
|
||||
@@ -50,7 +56,7 @@ Item {
|
||||
readonly property real cellWidth: columns > 0 ? (width - (columns - 1) * 10) / columns : 160
|
||||
|
||||
function isCurrent(path: string): bool {
|
||||
if (VideoWallpaper.active)
|
||||
if (root.videoAware && VideoWallpaper.active)
|
||||
return VideoWallpaper.path === path;
|
||||
return root.activeByOutput[root.selectedOutput] === path;
|
||||
}
|
||||
@@ -88,7 +94,7 @@ Item {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool current: root.isCurrent(tile.modelData)
|
||||
readonly property bool video: VideoWallpaper.isVideo(tile.modelData)
|
||||
readonly property bool video: root.videoAware && VideoWallpaper.isVideo(tile.modelData)
|
||||
readonly property bool member: root.mode === "slideshow" && root.selected(tile.modelData)
|
||||
|
||||
width: root.cellWidth
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Workspaces.
|
||||
//
|
||||
// Where windows go, how long a focus session runs, and the layouts you saved.
|
||||
// Projects are saved from the launcher rather than created here: a layout is
|
||||
// worth recording at the moment you have it right, not from a settings page
|
||||
// where you would have to describe it.
|
||||
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
// Which project is one click from being forgotten. Same in-place confirm the
|
||||
// SSH Keys and Snapshots pages use.
|
||||
property string confirmingProject: ""
|
||||
|
||||
title: "Workspaces"
|
||||
lede: "Workspaces, focus, and the projects that restore them."
|
||||
|
||||
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" }
|
||||
ToggleRow { setting: "windowSwallow"; divider: false }
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: "Focus"
|
||||
|
||||
SliderRow { setting: "focusDurationMinutes"; divider: false }
|
||||
}
|
||||
|
||||
// Saved here, not created here. A layout is worth recording at the moment
|
||||
// you have it right, so saving is a launcher command; this is where the
|
||||
// ones you kept are reviewed and the ones you did not are removed.
|
||||
SettingsCard {
|
||||
title: "Projects"
|
||||
subtitle: Projects.projects.length > 0
|
||||
? "Saved window layouts. Opening one claims free workspaces, so it never lands on top of what you are doing."
|
||||
: "No saved layouts yet. Arrange your windows, then run \"Save Layout as Project\" from the launcher."
|
||||
|
||||
Repeater {
|
||||
model: Projects.projects
|
||||
|
||||
delegate: SettingRow {
|
||||
id: projectRow
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string projectName: String(projectRow.modelData.name ?? "")
|
||||
readonly property bool confirming: root.confirmingProject === projectRow.projectName
|
||||
|
||||
label: projectRow.projectName
|
||||
detail: projectRow.confirming
|
||||
? "Forgetting this only removes the layout. Nothing that is open closes."
|
||||
: projectRow.modelData.windows + " windows across "
|
||||
+ projectRow.modelData.workspaces + " workspaces — "
|
||||
+ (projectRow.modelData.applications ?? []).join(", ")
|
||||
divider: projectRow.index < Projects.projects.length - 1
|
||||
controlWidth: 210
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 8
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !projectRow.confirming
|
||||
text: "Open"
|
||||
enabled: !Projects.busy
|
||||
onClicked: Projects.open(projectRow.projectName)
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: projectRow.confirming
|
||||
text: "Forget it"
|
||||
tone: "danger"
|
||||
enabled: !Projects.busy
|
||||
onClicked: {
|
||||
root.confirmingProject = "";
|
||||
Projects.remove(projectRow.projectName);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: projectRow.confirming ? "Keep" : "Forget"
|
||||
enabled: !Projects.busy
|
||||
onClicked: root.confirmingProject =
|
||||
projectRow.confirming ? "" : projectRow.projectName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextRow {
|
||||
visible: Projects.lastError !== ""
|
||||
label: "Problem"
|
||||
detail: Projects.lastError
|
||||
divider: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,12 @@ SettingsTabs 1.0 SettingsTabs.qml
|
||||
GamingPage 1.0 GamingPage.qml
|
||||
HomeFavoriteCard 1.0 HomeFavoriteCard.qml
|
||||
AvailableLightRow 1.0 AvailableLightRow.qml
|
||||
DesktopPage 1.0 DesktopPage.qml
|
||||
BarPage 1.0 BarPage.qml
|
||||
DockPage 1.0 DockPage.qml
|
||||
ControlCenterPage 1.0 ControlCenterPage.qml
|
||||
TilingPage 1.0 TilingPage.qml
|
||||
WorkspacesPage 1.0 WorkspacesPage.qml
|
||||
SyncPage 1.0 SyncPage.qml
|
||||
DisplaysPage 1.0 DisplaysPage.qml
|
||||
HomePage 1.0 HomePage.qml
|
||||
NotificationsPage 1.0 NotificationsPage.qml
|
||||
@@ -54,7 +59,7 @@ WallpaperPicker 1.0 WallpaperPicker.qml
|
||||
WallpaperControls 1.0 WallpaperControls.qml
|
||||
ApplicationsPage 1.0 ApplicationsPage.qml
|
||||
AutostartAppPicker 1.0 AutostartAppPicker.qml
|
||||
DockPinsEditor 1.0 DockPinsEditor.qml
|
||||
DockPinsStrip 1.0 DockPinsStrip.qml
|
||||
DockAppPicker 1.0 DockAppPicker.qml
|
||||
ShortcutCapture 1.0 ShortcutCapture.qml
|
||||
ChoiceGrid 1.0 ChoiceGrid.qml
|
||||
|
||||
Reference in New Issue
Block a user