From bc32980cca7adf5091f4b1dfb8335b4b76d1e10c Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 27 May 2026 15:02:15 +0200 Subject: [PATCH] Replace bar settings with inline config panel --- bin/omarchy-launch-bar-settings | 4 +- bin/omarchy-plugin | 6 +- bin/omarchy-shell | 2 +- default/hypr/apps/omarchy-shell.lua | 19 +- docs/omarchy-shell.md | 5 +- shell/Commons/Util.qml | 2 +- shell/README.md | 17 +- shell/Ui/MultiSelect.qml | 2 +- shell/Ui/PanelActionButton.qml | 2 +- shell/Ui/SearchableDropdown.qml | 2 +- shell/plugins/README.md | 12 - shell/plugins/bar/Bar.qml | 119 +- shell/plugins/bar/BarConfigPanel.qml | 246 ++++ shell/plugins/bar/README.md | 4 +- shell/plugins/dev-gallery/GalleryPanel.qml | 2 +- shell/plugins/notifications/Service.qml | 2 +- shell/plugins/panels/tailscale/README.md | 2 +- shell/plugins/settings/SettingsPanel.qml | 1268 ----------------- .../components/DynamicSettingsForm.qml | 281 ---- .../plugins/settings/components/NDropdown.qml | 143 -- shell/plugins/settings/manifest.json | 14 - shell/services/BarWidgetRegistry.qml | 2 +- shell/shell.qml | 4 + test/shell.d/runtime-smoke-test.sh | 5 +- 24 files changed, 377 insertions(+), 1788 deletions(-) create mode 100644 shell/plugins/bar/BarConfigPanel.qml delete mode 100644 shell/plugins/settings/SettingsPanel.qml delete mode 100644 shell/plugins/settings/components/DynamicSettingsForm.qml delete mode 100644 shell/plugins/settings/components/NDropdown.qml delete mode 100644 shell/plugins/settings/manifest.json diff --git a/bin/omarchy-launch-bar-settings b/bin/omarchy-launch-bar-settings index 6f59409a..23a557c9 100755 --- a/bin/omarchy-launch-bar-settings +++ b/bin/omarchy-launch-bar-settings @@ -1,5 +1,5 @@ #!/bin/bash -# omarchy:summary=Launch the Omarchy bar settings panel +# omarchy:summary=Open the Omarchy bar config panel -exec omarchy-shell shell summon omarchy.settings "{}" +exec omarchy-shell shell openBarConfig diff --git a/bin/omarchy-plugin b/bin/omarchy-plugin index bfe522e7..39688fc7 100755 --- a/bin/omarchy-plugin +++ b/bin/omarchy-plugin @@ -3,7 +3,7 @@ # omarchy:summary=Manage Omarchy shell plugins and bar widgets # omarchy:group=plugin # omarchy:args= [...] -# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock --with ai | omarchy plugin edit local.clock --with editor | omarchy plugin enable acme.weather --section right | omarchy plugin bar edit +# omarchy:examples=omarchy plugin list | omarchy plugin clone omarchy.clock local.clock --with ai | omarchy plugin edit local.clock --with editor | omarchy plugin enable acme.weather --section right | omarchy plugin bar settings set -euo pipefail @@ -30,7 +30,7 @@ Clone options: --add [placement] Add cloned bar widget to the bar Bar widget commands: - bar edit Open the visual bar settings panel + bar settings Open the inline bar config panel bar list [--json] List current bar layout bar add [placement] [--duplicate] bar move [placement] [--from-section SECTION] [--from-index N] @@ -1136,7 +1136,7 @@ bar_command() { case "$command" in edit | settings) - (( $# == 0 )) || fail "bar edit does not take arguments" + (( $# == 0 )) || fail "bar settings does not take arguments" exec omarchy-launch-bar-settings ;; list) diff --git a/bin/omarchy-shell b/bin/omarchy-shell index a2e55200..79edb244 100755 --- a/bin/omarchy-shell +++ b/bin/omarchy-shell @@ -23,7 +23,7 @@ Options: Examples: omarchy-shell shell ping - omarchy-shell shell summon omarchy.settings "{}" + omarchy-shell shell openBarConfig omarchy-shell -q Indicators refresh omarchy-shell shell listPlugins omarchy-shell shell toggle omarchy.menu '{"menu":"root"}' diff --git a/default/hypr/apps/omarchy-shell.lua b/default/hypr/apps/omarchy-shell.lua index cfcffd10..efdbc351 100644 --- a/default/hypr/apps/omarchy-shell.lua +++ b/default/hypr/apps/omarchy-shell.lua @@ -1,6 +1,5 @@ -- Window and layer rules for the Omarchy Quickshell surfaces. The --- shell-wide bar / menu / popouts are layer-shell; the bar settings panel --- is a regular Hyprland window via Quickshell's FloatingWindow. +-- shell-wide bar / menu / popouts are layer-shell. -- Keep the bar instant: no layer-shell fade/slide animation. hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation = "none" }) @@ -10,22 +9,6 @@ hl.layer_rule({ match = { namespace = "omarchy-bar" }, no_anim = true, animation -- QML opacity transition for normal open/close, and skip it for panel handoff. hl.layer_rule({ match = { namespace = "^(omarchy-menu|omarchy-launcher|omarchy-image-selector|omarchy-emojis|omarchy-clipboard|omarchy-keyboard-panel)$" }, no_anim = true, animation = "none" }) --- Bar settings floats centered with a sensible default size instead of --- tiling — it's a transient dialog, not a workspace surface. -o.window({ - class = "^org.quickshell$", - title = "^Omarchy Bar Settings$", -}, { - float = true, - center = true, - size = { 760, 620 }, -}) - -- Dev gallery is the main shell workbench; open it maximized like -- SUPER+ALT+F so component previews have the whole workspace. o.window({ class = "^org.quickshell$", title = "^Omarchy shell – dev gallery$" }, { maximize = true }) - --- Per-widget settings dialog opens as a smaller FloatingWindow off the --- bar settings panel; keep it floating with its own default size. -o.window({ class = "^org.quickshell$", title = "^Widget settings " }, { float = true }) -o.window({ class = "^org.quickshell$", title = "^Widget settings " }, { size = { 380, 320 } }) diff --git a/docs/omarchy-shell.md b/docs/omarchy-shell.md index 330ded37..5f13b987 100644 --- a/docs/omarchy-shell.md +++ b/docs/omarchy-shell.md @@ -26,7 +26,7 @@ first call. | Kind | What it is | |--------------|---------------------------------------------| | `bar-widget` | Component the bar drops into a section | -| `panel` | Floating window (e.g. bar settings) | +| `panel` | Floating window (e.g. OSD) | | `overlay` | Fullscreen overlay (e.g. background picker) | | `menu` | Summoned menu surface | | `service` | Headless singleton, no UI | @@ -43,8 +43,7 @@ Full schema: [`shell/services/PluginRegistry.qml`](../shell/services/PluginRegis plus the QML referenced from `entryPoints`. 2. `omarchy plugin rescan` 3. `omarchy plugin enable ` -4. Bar widgets also need adding to a section with `omarchy plugin bar add ` - or the visual editor (`omarchy plugin bar edit`). +4. Bar widgets also need adding to a section with `omarchy plugin bar add `. The lower-level IPC methods remain available through `omarchy-shell shell ...` for callers that need to talk directly to the running shell. diff --git a/shell/Commons/Util.qml b/shell/Commons/Util.qml index 2927176f..972154ee 100644 --- a/shell/Commons/Util.qml +++ b/shell/Commons/Util.qml @@ -83,7 +83,7 @@ QtObject { } } - // Layout normalization shared by the bar host and the bar settings panel + // Layout normalization shared by bar config consumers // so the two never drift. Entries are deep-cloned to decouple from the // input config; consumers can mutate without leaking back to shell.json. function normalizeLayoutEntry(entry) { diff --git a/shell/README.md b/shell/README.md index 03e2490a..a8bf2325 100644 --- a/shell/README.md +++ b/shell/README.md @@ -2,9 +2,8 @@ `omarchy-shell` is a single long-running [Quickshell](https://quickshell.org/) instance that hosts the Omarchy desktop. Hyprland autostart launches one shell -per graphical session; everything else — the bar, the bar settings UI, the -background switcher, future panels and overlays — runs **inside** the shell as -a plugin. +per graphical session; everything else — the bar, background switcher, panels, +and overlays — runs **inside** the shell as a plugin. Hosting everything inside one shell means: @@ -24,7 +23,6 @@ shell/ BarWidgetRegistry.qml unified registry for bar widgets (1p + 3p) plugins/ bar/ first-party plugins (see plugins/README.md) - settings/ settings panel + plugin-declared widget forms launcher/ image-picker/ menu/ @@ -77,7 +75,7 @@ Supported `kinds`: | Kind | What it is | |--------------|--------------------------------------------------------------| | `bar-widget` | A component that the bar can drop into a section | -| `panel` | A persistent or summoned floating window (e.g. bar settings) | +| `panel` | A persistent or summoned floating window (e.g. OSD) | | `overlay` | A fullscreen overlay (e.g. background switcher) | | `menu` | A summoned menu surface | | `service` | A headless singleton, no UI | @@ -97,8 +95,7 @@ The full schema lives in `services/PluginRegistry.qml`. referenced from its `entryPoints`. 2. `omarchy plugin rescan`. 3. Enable the plugin with `omarchy plugin enable `. -4. If it's a `bar-widget`, place it with `omarchy plugin bar add ` or - open the visual editor with `omarchy plugin bar edit`. +4. If it's a `bar-widget`, place it with `omarchy plugin bar add `. The lower-level IPC equivalents are still available via `omarchy-shell shell rescanPlugins`, `omarchy-shell shell setPluginEnabled true`, and `omarchy-shell shell listPlugins`. @@ -153,7 +150,7 @@ calls to the running shell. It does not start the shell. ``` omarchy-shell shell ping -omarchy-shell shell summon omarchy.settings "{}" +omarchy-shell shell openBarConfig omarchy-shell shell toggle omarchy.menu '{"menu":"root"}' omarchy-shell shell listPlugins omarchy-shell shell rescanPlugins @@ -177,9 +174,7 @@ customization from the shipped defaults lives in it. The `config/omarchy/shell.json` default config describes the fresh-install state. When the user has no `shell.json`, the shell uses the defaults verbatim. Once the user customizes anything, `shell.json` -becomes the authoritative file — we do **not** deep-merge defaults back -in. Pressing **Reset bar to defaults** in `omarchy launch bar settings` -rewrites the `bar` subtree from the current default shell config. +becomes the authoritative file — we do **not** deep-merge defaults back in. ### shell.json shape diff --git a/shell/Ui/MultiSelect.qml b/shell/Ui/MultiSelect.qml index 8869158f..93c4bfe7 100644 --- a/shell/Ui/MultiSelect.qml +++ b/shell/Ui/MultiSelect.qml @@ -332,7 +332,7 @@ Item { QQC.Popup { id: popup // Reparent to the window's content item so the popup is free of any - // clipping ancestor (e.g. the bar settings dialog Flickable). Position + // clipping ancestor. Position // and available height are recomputed on open and any time the // trigger's geometry changes, since a binding on mapToItem alone // won't reliably re-evaluate when ancestors scroll or resize. diff --git a/shell/Ui/PanelActionButton.qml b/shell/Ui/PanelActionButton.qml index 5871b79d..cf437607 100644 --- a/shell/Ui/PanelActionButton.qml +++ b/shell/Ui/PanelActionButton.qml @@ -15,7 +15,7 @@ import qs.Commons // // Set `focusable: true` to make the button keyboard-tabbable with the // shared hover-cursor/focus tokens. Use this -// in form contexts (the bar settings widget cards) where Tab walks a list +// in form contexts where Tab walks a list // of controls; leave it false for the right-edge actions on panel rows // where the row's CursorSurface owns the keyboard cursor. // diff --git a/shell/Ui/SearchableDropdown.qml b/shell/Ui/SearchableDropdown.qml index 5c876fa5..59c1f9dc 100644 --- a/shell/Ui/SearchableDropdown.qml +++ b/shell/Ui/SearchableDropdown.qml @@ -5,7 +5,7 @@ import qs.Commons // Searchable single-select dropdown. Same trigger shape as Dropdown, but // the popup leads with an embedded TextField that filters the option // list in real time. Use for pickers with enough options that scanning -// is friction (e.g. bar settings "+ Add widget"). +// is friction. // // Filtering is case-insensitive substring against each option's label. // Options can be string[] or [{ value, label, description? }] — the same diff --git a/shell/plugins/README.md b/shell/plugins/README.md index 38536482..63d95160 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -12,7 +12,6 @@ User-installed plugins live alongside these conceptually but on disk under | Plugin | id | kinds | entry point | |---------------|---------------------------|-------------------------|---------------------------------------| | Bar | `omarchy.bar` | `bar` | `bar/Bar.qml` | -| Bar settings | `omarchy.settings` | `panel` | `settings/SettingsPanel.qml` | | Launcher | `omarchy.launcher` | `overlay` | `launcher/Launcher.qml` | | Image picker | `omarchy.image-picker` | `overlay` | `image-picker/ImagePicker.qml` | | Emojis | `omarchy.emojis` | `overlay` | `emojis/Emojis.qml` | @@ -46,17 +45,6 @@ providing [`config/omarchy/shell.json`](../../config/omarchy/shell.json) when the user has no file). See [`bar/README.md`](bar/README.md) for the widget catalogue and customization schema. -## Bar settings - -Visual editor for the bar layout. Summoned by -`omarchy-shell shell summon omarchy.settings "{}"` (which is what -`omarchy launch bar settings` ultimately calls). Provides: - -- bar position and center-anchor controls -- per-section add/move/remove/edit of bar widget entries -- dynamic per-widget settings forms that write inline back to the - corresponding shell.json entry - ## Launcher Quickshell-powered launcher. It uses Quickshell's native diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index 4491268a..ddeaaccf 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -12,8 +12,7 @@ Item { // The omarchy-shell host injects omarchyPath from OMARCHY_PATH. required property string omarchyPath - // Injected by the host shell. Shared with the bar settings panel so both - // see the same widget catalogue. + // Injected by the host shell so bar slots can resolve enabled widgets. required property var barWidgetRegistry // Injected by the host shell every time shell.json is reloaded. Holds the // `bar:` subtree: position, centerAnchor, layout. The host owns file IO; @@ -69,6 +68,7 @@ Item { property var activePopout: null property var barDragTarget: null property bool barDragAfter: false + property var configControls: [] property var clickTargets: [] property var debugModuleSlots: [] @@ -96,6 +96,18 @@ Item { debugModuleSlots = next } + function registerConfigControl(control) { + if (!control || configControls.indexOf(control) !== -1) return + var next = configControls.slice() + next.push(control) + configControls = next + } + + function unregisterConfigControl(control) { + var next = configControls.filter(function(item) { return item !== control }) + configControls = next + } + function debugBarGeometry() { var out = [] for (var i = 0; i < debugModuleSlots.length; i++) { @@ -164,7 +176,7 @@ Item { } // Apply tray-pinning on top of the shared layout normalization so the - // bar host and the bar settings panel can't drift on entry shape. + // bar host and scriptable config helpers can't drift on entry shape. function normalizeLayout(layout) { var normalized = Util.normalizeLayout(Util.isPlainObject(layout) ? layout : fallbackBarConfig.layout) return { @@ -305,12 +317,14 @@ Item { launcher.startDetached() } - function openBarSettings() { - if (root.shell && typeof root.shell.summon === "function") { - root.shell.summon("omarchy.settings", "{}") - } else { - root.run("omarchy-launch-bar-settings") + function openConfigPanel() { + for (var i = 0; i < configControls.length; i++) { + var control = configControls[i] + if (!control || control.visible !== true || typeof control.openPanel !== "function") continue + control.openPanel() + return true } + return false } function toggleTransparency() { @@ -821,7 +835,7 @@ Item { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) region: "center" - anchors.right: centerAnchorModule.left + anchors.right: centerConfigControl.visible ? centerConfigControl.left : centerAnchorModule.left anchors.verticalCenter: centerAnchorModule.verticalCenter } @@ -833,6 +847,15 @@ Item { anchors.centerIn: parent } + BarConfigControl { + id: centerConfigControl + + visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" + clockHovered: centerAnchorModule.hovered + anchors.right: centerAnchorModule.left + anchors.verticalCenter: centerAnchorModule.verticalCenter + } + ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) @@ -862,7 +885,7 @@ Item { visible: centerRoot.hasAnchor entries: root.entriesBefore(centerRoot.entries, root.centerAnchor) region: "center" - anchors.bottom: centerAnchorModule.top + anchors.bottom: centerConfigControl.visible ? centerConfigControl.top : centerAnchorModule.top anchors.horizontalCenter: centerAnchorModule.horizontalCenter } @@ -874,6 +897,15 @@ Item { anchors.centerIn: parent } + BarConfigControl { + id: centerConfigControl + + visible: centerRoot.hasAnchor && centerAnchorModule.moduleName === "omarchy.clock" + clockHovered: centerAnchorModule.hovered + anchors.bottom: centerAnchorModule.top + anchors.horizontalCenter: centerAnchorModule.horizontalCenter + } + ModuleList { visible: centerRoot.hasAnchor entries: root.entriesAfter(centerRoot.entries, root.centerAnchor) @@ -886,23 +918,69 @@ Item { } component CenterGestureArea: MouseArea { - acceptedButtons: Qt.LeftButton | Qt.RightButton - - onClicked: function(mouse) { - if (mouse.button === Qt.RightButton) { - root.openBarSettings() - mouse.accepted = true - } - } + acceptedButtons: Qt.LeftButton onDoubleClicked: function(mouse) { - if (mouse.button !== Qt.RightButton) { + if (mouse.button === Qt.LeftButton) { root.toggleTransparency() mouse.accepted = true } } } + component BarConfigControl: Item { + id: configControl + + property bool clockHovered: false + + readonly property bool panelOpen: configPanel.opened + readonly property bool revealed: visible && (clockHovered || controlHover.hovered || panelOpen) + + implicitWidth: button.implicitWidth + implicitHeight: button.implicitHeight + width: implicitWidth + height: implicitHeight + opacity: revealed ? 1.0 : 0 + z: 500 + + Behavior on opacity { + NumberAnimation { duration: 120; easing.type: Easing.OutCubic } + } + + HoverHandler { id: controlHover } + + Component.onCompleted: root.registerConfigControl(configControl) + Component.onDestruction: root.unregisterConfigControl(configControl) + + function openPanel() { + configPanel.open() + } + + WidgetButton { + id: button + + anchors.fill: parent + bar: root + text: "" + keepSpace: true + concealed: !configControl.revealed + interactive: configControl.revealed + horizontalMargin: 6.5 + verticalPadding: 6 + tooltipText: "Bar config" + onPressed: function(b) { + if (b === Qt.LeftButton) configPanel.toggle() + } + } + + BarConfigPanel { + id: configPanel + + bar: root + anchorItem: button + } + } + component ModuleList: Loader { id: moduleListRoot @@ -976,6 +1054,7 @@ Item { if (qmlCustom) return qmlLoader.item return componentLoader.item } + readonly property bool hovered: moduleHover.hovered implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0 implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0 @@ -986,6 +1065,8 @@ Item { Component.onCompleted: root.registerDebugModuleSlot(slot) Component.onDestruction: root.unregisterDebugModuleSlot(slot) + HoverHandler { id: moduleHover } + Loader { id: componentLoader active: !slot.qmlCustom && !slot.registered diff --git a/shell/plugins/bar/BarConfigPanel.qml b/shell/plugins/bar/BarConfigPanel.qml new file mode 100644 index 00000000..4e52ec16 --- /dev/null +++ b/shell/plugins/bar/BarConfigPanel.qml @@ -0,0 +1,246 @@ +import QtQuick +import qs.Commons +import qs.Ui + +Panel { + id: root + + moduleName: "omarchy.bar-config" + manageIpc: false + + property Item anchorItem: null + property string focusSection: "transparency" + property int positionIndex: 0 + property bool cursorActive: false + property int phraseIndex: 0 + + readonly property string currentPosition: bar ? bar.position : "top" + readonly property bool transparent: bar ? bar.requestedTransparent === true : false + readonly property color foreground: bar ? bar.foreground : Color.foreground + readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family + readonly property var heroPhrases: [ + "Picking Sides", + "Choose Transparency", + "Edge Decisions", + "Bar Exam", + "Top Shelf Thinking", + "Side Quest Settings" + ] + readonly property string heroPhraseText: heroPhrases[phraseIndex % heroPhrases.length] + readonly property var positionOptions: [ + { value: "top", label: "Top" }, + { value: "bottom", label: "Bottom" }, + { value: "left", label: "Left" }, + { value: "right", label: "Right" } + ] + + function normalizePosition(value) { + var next = String(value || "") + return /^(top|bottom|left|right)$/.test(next) ? next : "top" + } + + function positionLabel(value) { + var next = normalizePosition(value) + return next.charAt(0).toUpperCase() + next.slice(1) + } + + function currentPositionIndex() { + var current = normalizePosition(currentPosition) + for (var i = 0; i < positionOptions.length; i++) + if (positionOptions[i].value === current) return i + return 0 + } + + function mutateBarConfig(mutator) { + if (!bar || !bar.shell || typeof bar.shell.mutateShellConfig !== "function") return false + bar.shell.mutateShellConfig(function(config) { + if (!Util.isPlainObject(config.bar)) config.bar = {} + mutator(config.bar) + }) + return true + } + + function setTransparency(value) { + var next = value === true + if (mutateBarConfig(function(barConfig) { barConfig.transparent = next })) return + if (bar && typeof bar.setRequestedTransparency === "function") bar.setRequestedTransparency(next) + } + + function setPosition(value) { + var next = normalizePosition(value) + if (mutateBarConfig(function(barConfig) { barConfig.position = next })) return + if (bar) bar.position = next + } + + function moveCursor(dx, dy) { + if (!cursorActive) { + cursorActive = true + return + } + + if (dy !== 0) { + focusSection = focusSection === "transparency" ? "position" : "transparency" + if (focusSection === "position") positionIndex = currentPositionIndex() + return + } + + if (dx !== 0 && focusSection === "position") { + var count = positionOptions.length + positionIndex = (positionIndex + (dx > 0 ? 1 : -1) + count) % count + } + } + + function activateCursor() { + if (focusSection === "transparency") { + setTransparency(!transparent) + return + } + + var option = positionOptions[positionIndex] + if (option) setPosition(option.value) + } + + onOpenedChanged: { + if (!opened) return + focusSection = "transparency" + positionIndex = currentPositionIndex() + cursorActive = false + } + + onCurrentPositionChanged: if (!cursorActive || focusSection === "position") positionIndex = currentPositionIndex() + + Timer { + id: phraseTimer + interval: 2200 + running: root.opened + repeat: true + onTriggered: phraseSwap.restart() + } + + SequentialAnimation { + id: phraseSwap + PropertyAnimation { + target: hero; property: "metaOpacity" + to: 0.0; duration: 180; easing.type: Easing.OutQuad + } + ScriptAction { + script: root.phraseIndex = (root.phraseIndex + 1) % root.heroPhrases.length + } + PropertyAnimation { + target: hero; property: "metaOpacity" + to: 1.0; duration: 260; easing.type: Easing.InQuad + } + } + + Component { + id: heroIconComponent + + Text { + text: "" + color: root.foreground + font.family: root.fontFamily + font.pixelSize: Style.font.display + } + } + + KeyboardPanel { + id: panel + + anchorItem: root.anchorItem + owner: root + bar: root.bar + open: root.opened && root.anchorItem !== null + focusTarget: keyCatcher + contentWidth: panel.fittedContentWidth(Style.space(380)) + contentHeight: panel.fittedContentHeight(contentColumn.implicitHeight) + + PanelKeyCatcher { + id: keyCatcher + + anchors.fill: parent + onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) } + onActivateRequested: root.activateCursor() + onCloseRequested: root.close() + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Style.space(14) + + PanelHero { + id: hero + + width: parent.width + iconComponent: heroIconComponent + title: "Bar" + meta: root.heroPhraseText + foreground: root.foreground + fontFamily: root.fontFamily + } + + Column { + width: parent.width + spacing: Style.space(8) + + PanelSectionHeader { + text: "APPEARANCE" + foreground: root.foreground + fontFamily: root.fontFamily + } + + Toggle { + width: parent.width + label: "Transparency" + description: root.transparent ? "Wallpaper visible" : "Solid background" + checked: root.transparent + hasCursor: root.cursorActive && root.focusSection === "transparency" + foreground: root.foreground + accent: Color.accent + fontFamily: root.fontFamily + onClicked: root.setTransparency(!root.transparent) + onHovered: function(h) { + if (!h) return + root.cursorActive = true + root.focusSection = "transparency" + } + } + } + + Column { + width: parent.width + spacing: Style.space(8) + + PanelSectionHeader { + text: "POSITION" + foreground: root.foreground + fontFamily: root.fontFamily + } + + ButtonGroup { + id: positionGroup + + width: parent.width + options: root.positionOptions + value: root.currentPosition + foreground: root.foreground + background: "transparent" + accent: Color.accent + fontFamily: root.fontFamily + focusable: false + cursorIndex: root.cursorActive && root.focusSection === "position" ? root.positionIndex : -1 + onChanged: function(value) { root.setPosition(value) } + onHovered: function(index, hovered) { + if (!hovered) return + root.cursorActive = true + root.focusSection = "position" + root.positionIndex = index + } + } + } + } + } + } +} diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 7e90bb52..48ee24b7 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -14,9 +14,9 @@ the shell for its whole session. ## Customizing -The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via `omarchy launch bar settings` or by editing shell.json directly, your file is canonical — there is no deep-merge. +The bar config lives under the `bar:` key of [`~/.config/omarchy/shell.json`](../../README.md#shelljson-shape). Out of the box the shell uses [`config/omarchy/shell.json`](../../../config/omarchy/shell.json). Once you customize anything via the inline bar config panel, `omarchy plugin bar ...`, or by editing shell.json directly, your file is canonical — there is no deep-merge. -Launch the visual editor with `omarchy launch bar settings` / `omarchy plugin bar edit` (or run `omarchy-launch-bar-settings`) to reorder widgets, add/remove them, and tweak per-widget options without editing JSON by hand. For scriptable changes, use `omarchy plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin bar set`. You can also right-click empty space to the left or right of the centered clock module to open it; double-left-click the same empty space to toggle bar transparency. +Open quick position and transparency controls with `omarchy launch bar settings` / `omarchy plugin bar settings` (or run `omarchy-launch-bar-settings`). You can also hover the centered clock module to reveal the inline bar config button. For scriptable widget changes, use `omarchy plugin bar list`, `omarchy plugin bar add`, `omarchy plugin bar move`, `omarchy plugin bar remove`, and `omarchy plugin bar set`. Double-left-click empty center-bar space to toggle bar transparency. Example `shell.json` (bar subtree only shown): diff --git a/shell/plugins/dev-gallery/GalleryPanel.qml b/shell/plugins/dev-gallery/GalleryPanel.qml index 31ded41a..e679b50a 100644 --- a/shell/plugins/dev-gallery/GalleryPanel.qml +++ b/shell/plugins/dev-gallery/GalleryPanel.qml @@ -1550,7 +1550,7 @@ Item { font.bold: true } Text { - text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for the bar settings \"+ Add widget\" picker and any other long-list selector." + text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for long option lists where plain scrolling is friction." color: Qt.darker(root.foreground, 1.5) font.family: root.fontFamily font.pixelSize: Style.font.caption diff --git a/shell/plugins/notifications/Service.qml b/shell/plugins/notifications/Service.qml index 16970294..501b2b28 100644 --- a/shell/plugins/notifications/Service.qml +++ b/shell/plugins/notifications/Service.qml @@ -29,7 +29,7 @@ Item { // ~/.cache where regeneratable artifacts belong. readonly property string cacheDir: home + "/.cache/omarchy/" readonly property string imageCacheDir: cacheDir + "notification-images/" - // Corner radius is shared with the menu and bar settings panel. + // Corner radius is shared with the menu and shell panels. // It mirrors Hyprland's current decoration:rounding value. readonly property int cornerRadius: Style.cornerRadius // Surfaces anchor relative to the omarchy bar so popups and history land diff --git a/shell/plugins/panels/tailscale/README.md b/shell/plugins/panels/tailscale/README.md index e22bd031..e4bf8a03 100644 --- a/shell/plugins/panels/tailscale/README.md +++ b/shell/plugins/panels/tailscale/README.md @@ -35,4 +35,4 @@ Renders the Tailscale mark natively as a theme-colored 3×3 dot grid, matching t ## Add to the bar -This widget ships as first-party plugin `omarchy.tailscale`. Add it through Omarchy's bar settings UI, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell. +This widget ships as first-party plugin `omarchy.tailscale`. Add it with `omarchy plugin bar add omarchy.tailscale`, or add an entry such as `{ "id": "omarchy.tailscale" }` to one of the `bar.layout` sections in `~/.config/omarchy/shell.json` and restart the shell. diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml deleted file mode 100644 index 4f851082..00000000 --- a/shell/plugins/settings/SettingsPanel.qml +++ /dev/null @@ -1,1268 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell -import Quickshell.Io -import qs.Commons -import qs.Ui - -import "./components" as Cmp - -Item { - id: root - - // ---------------- plugin lifecycle --------------------------------------- - property bool closingFromHost: false - - function open(payloadJson) { - closingFromHost = false - window.visible = true - Qt.callLater(parkFocusOnSink) - } - - function close() { - closingFromHost = true - window.visible = false - closingFromHost = false - } - - // ---------------- host injections ---------------------------------------- - property var barWidgetRegistry: null - property var pluginRegistry: null - property var shell: null - - // ---------------- paths -------------------------------------------------- - property string omarchyPath: Quickshell.env("OMARCHY_PATH") - readonly property string home: Quickshell.env("HOME") - readonly property string userConfigPath: home + "/.config/omarchy/shell.json" - readonly property string defaultsPath: omarchyPath + "/config/omarchy/shell.json" - - // ---------------- theme -------------------------------------------------- - // Bar settings deliberately isn't a themable surface in shell.toml — it - // tracks the foundational palette so every theme renders consistently. - property color foreground: Color.popups.text - property color background: Color.popups.background - property color accent: Color.accent - property color urgent: Color.urgent - property string fontFamily: "monospace" - - // Structural style tokens live on the shared Style singleton so theme swaps - // and Hyprland-derived values update every consumer at once. - // Aliasing them as readonly properties keeps the existing inline component - // bindings (`root.cornerRadius`, `root.focusBorderColor`, ...) working - // without sprinkling Style.* across the file. - readonly property int cornerRadius: Style.cornerRadius - readonly property color focusBorderColor: Style.focusBorderColor - readonly property color focusFillColor: Style.focusFillColor - readonly property int focusBorderWidth: Style.focusBorderWidth - - // Move activeFocus to a dedicated sink Item that lives outside the body - // tree. Just clearing focus on the previously focused descendant isn't - // enough — controls like ComboBox keep an internal focused child that - // FocusScope happily restores. Forcing focus onto a known sink reliably - // clears every body focus ring. - function parkFocusOnSink() { - if (typeof navFocusSink !== "undefined" && navFocusSink) navFocusSink.forceActiveFocus() - else if (navRoot) navRoot.forceActiveFocus() - } - - // Walk the visible body subtree and collect any item with - // `activeFocusOnTab: true` so j/k can move through the form. - function gatherBodyFocusables() { - var arr = [] - function walk(item) { - if (!item || !item.visible || item.enabled === false) return - if (item.activeFocusOnTab === true) arr.push(item) - var children = item.children - if (!children) return - for (var i = 0; i < children.length; i++) walk(children[i]) - } - if (typeof bodyScroll !== "undefined" && bodyScroll && bodyScroll.contentItem) - walk(bodyScroll.contentItem) - return arr - } - - function focusFirstBodyItem() { - var items = gatherBodyFocusables() - if (items.length > 0) { - items[0].forceActiveFocus() - ensureBodyItemVisible(items[0]) - } else if (navRoot) { - navRoot.forceActiveFocus() - } - } - - function focusBodyDelta(delta) { - var items = gatherBodyFocusables() - if (items.length === 0) { parkFocusOnSink(); return } - var current = -1 - for (var i = 0; i < items.length; i++) { - if (items[i].activeFocus) { current = i; break } - } - var next = current < 0 ? 0 : current + delta - if (next < 0) next = items.length - 1 - if (next >= items.length) next = 0 - items[next].forceActiveFocus() - ensureBodyItemVisible(items[next]) - } - - // Scroll the bodyScroll Flickable so `item` is fully on-screen, with a - // little padding above/below. - function ensureBodyItemVisible(item) { - if (!item || typeof bodyScroll === "undefined" || !bodyScroll || !bodyScroll.contentItem) return - var pos = item.mapToItem(bodyScroll.contentItem, 0, 0) - var pad = Style.space(24) - var top = pos.y - pad - var bottom = pos.y + item.height + pad - if (top < bodyScroll.contentY) { - bodyScroll.contentY = Math.max(0, top) - } else if (bottom > bodyScroll.contentY + bodyScroll.height) { - var maxY = Math.max(0, bodyScroll.contentHeight - bodyScroll.height) - bodyScroll.contentY = Math.min(maxY, bottom - bodyScroll.height) - } - } - - // ---------------- bundled defaults --------------------------------------- - readonly property var builtinShellConfig: ({ - version: 1, - bar: { - position: "top", - transparent: false, - centerAnchor: "omarchy.clock", - layout: { - left: [{ id: "omarchy.menu" }, { id: "omarchy.workspaces" }], - center: [ - { id: "omarchy.clock", format: "dddd HH:mm", formatAlt: "dd MMMM 'W'ww yyyy", verticalFormat: "HH\n\u2014\nmm" }, - { id: "omarchy.weather" }, { id: "omarchy.system-update" }, { id: "omarchy.indicators" } - ], - right: [ - { id: "omarchy.tray" }, { id: "omarchy.bluetooth" }, { id: "omarchy.network" }, - { id: "omarchy.audio" }, { id: "omarchy.monitor" }, { id: "omarchy.power" } - ] - } - }, - plugins: [] - }) - - property var defaultConfig: builtinShellConfig - property var draft: ({ version: 1, bar: { position: "top", transparent: false, centerAnchor: "omarchy.clock", layout: { left: [], center: [], right: [] } }, plugins: [] }) - property int draftRevision: 0 - property bool suppressReload: false - - // When a widget action moves an entry, the Repeater rebuilds / reindexes - // cards. Remember the action group position so focus follows the moved - // widget instead of falling back to the first action on the old/new row. - property string pendingActionFocusSection: "" - property int pendingActionFocusIndex: -1 - property int pendingActionFocusAction: 0 - property int pendingActionFocusRevision: 0 - - function scheduleActionFocus(section, index, action) { - pendingActionFocusSection = section - pendingActionFocusIndex = index - pendingActionFocusAction = action - pendingActionFocusRevision++ - } - - function clearPendingActionFocus() { - pendingActionFocusSection = "" - pendingActionFocusIndex = -1 - pendingActionFocusAction = 0 - } - - // ---------------- draft helpers ------------------------------------------ - function normalizeDraft(source) { - var bar = Util.isPlainObject(source.bar) ? source.bar : {} - var plugins = Array.isArray(source.plugins) ? source.plugins.slice() : [] - return { - version: 1, - bar: { - position: String(bar.position || "top"), - transparent: bar.transparent === true, - centerAnchor: Util.canonicalWidgetId(bar.centerAnchor || ""), - layout: Util.normalizeLayout(bar.layout || {}) - }, - plugins: plugins - .map(Util.normalizeLayoutEntry) - .filter(function(e) { - if (!e) return false - var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[e.id] : null - if (manifest && manifest.__isFirstParty) return false - return true - }) - } - } - - function loadConfig() { - var defaults = builtinShellConfig - var diskText = defaultsFile.text() - if (diskText) { - try { - var parsed = JSON.parse(diskText) - if (Util.isPlainObject(parsed) && parsed.version === 1) defaults = parsed - } catch (e) { - console.warn("Bad default shell JSON, falling back to builtin:", e) - defaults = builtinShellConfig - } - } - defaultConfig = defaults - - var userText = userFile.text() || "" - var source = defaults - if (userText.trim()) { - try { - var u = JSON.parse(userText) - if (Util.isPlainObject(u) && u.version === 1) source = u - } catch (e) { - console.warn("shell.json parse failed in panel:", e) - } - } - draft = normalizeDraft(source) - draftRevision++ - } - - function persistDraft() { - suppressReload = true - userFile.setText(JSON.stringify(draft, null, 2) + "\n") - } - - function defaultBarDraft() { - var source = defaultConfig - if (!Util.isPlainObject(source) || !Util.isPlainObject(source.bar) || !Util.isPlainObject(source.bar.layout)) { - source = builtinShellConfig - } else { - var l = source.bar.layout - var anyEntries = (l.left && l.left.length) || (l.center && l.center.length) || (l.right && l.right.length) - if (!anyEntries) source = builtinShellConfig - } - return normalizeDraft(source).bar - } - - function resetBarToDefaults() { - var next = Util.cloneJson(draft) - next.bar = Util.cloneJson(defaultBarDraft()) - draft = next - draftRevision++ - persistDraft() - } - - function markDirty() { - draftRevision++ - persistDraft() - } - - function sectionArray(section) { - if (section === "plugins") return draft.plugins || [] - return (draft.bar && draft.bar.layout && draft.bar.layout[section]) || [] - } - - function mutateSection(section, mutator) { - var arr = sectionArray(section).slice() - mutator(arr) - var nextDraft = Util.cloneJson(draft) - if (section === "plugins") nextDraft.plugins = arr - else nextDraft.bar.layout[section] = arr - draft = nextDraft - markDirty() - } - - function moveEntry(section, fromIndex, toIndex, focusActionIndex) { - var arr = sectionArray(section) - if (toIndex < 0 || toIndex >= arr.length) return - mutateSection(section, function(a) { - var item = a[fromIndex] - a.splice(fromIndex, 1) - a.splice(toIndex, 0, item) - }) - if (focusActionIndex !== undefined) scheduleActionFocus(section, toIndex, focusActionIndex) - } - - function removeEntry(section, index) { - mutateSection(section, function(a) { a.splice(index, 1) }) - } - - function defaultEntryForWidget(id) { - var bar = defaultBarDraft() - var layout = bar && bar.layout ? bar.layout : {} - var sections = ["left", "center", "right"] - for (var s = 0; s < sections.length; s++) { - var entries = layout[sections[s]] || [] - for (var i = 0; i < entries.length; i++) { - if (entries[i].id === id) return Util.cloneJson(entries[i]) - } - } - return { id: id } - } - - function addEntry(section, id) { - mutateSection(section, function(a) { a.push(defaultEntryForWidget(id)) }) - } - - function updateEntry(section, index, newEntry) { - mutateSection(section, function(a) { a[index] = Util.cloneJson(newEntry) }) - } - - // ---------------- widget catalog ----------------------------------------- - property int catalogRevision: 0 - onBarWidgetRegistryChanged: { - catalogRevision++ - if (!root.barWidgetRegistry) return - console.log("bar settings panel open. omarchyPath=" + root.omarchyPath, - "defaultsPath=" + root.defaultsPath, - "userConfigPath=" + root.userConfigPath, - "registry has", - root.barWidgetRegistry.availableIds().length, - "widgets") - } - Connections { - target: root.barWidgetRegistry - function onChanged() { root.catalogRevision++ } - } - - function canonicalWidgetId(id) { - return Util.canonicalWidgetId(id) - } - - function widgetMetadata(id) { - var key = String(id || "") - var canonicalKey = canonicalWidgetId(key) - if (root.barWidgetRegistry && root.barWidgetRegistry.has(canonicalKey)) - return root.barWidgetRegistry.metadataFor(canonicalKey) || {} - - var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[key] : null - if (manifest) { - var meta = manifest.barWidget || {} - return { - displayName: meta.displayName || manifest.name || key, - name: meta.displayName || manifest.name || key, - description: meta.description || manifest.description || "", - category: meta.category || "Plugin", - allowMultiple: meta.allowMultiple === true, - settingsForm: meta.settingsForm || "", - schema: Array.isArray(meta.schema) ? meta.schema : [], - sourceDir: manifest.__sourceDir || "", - source: "plugin" - } - } - return {} - } - - function widgetSourceDir(id) { - var meta = widgetMetadata(id) - return meta && meta.sourceDir ? String(meta.sourceDir) : "" - } - - function widgetName(id) { - var rev = catalogRevision - var meta = widgetMetadata(id) - return meta.displayName || meta.name || id - } - - function widgetDescription(id) { - var rev = catalogRevision - var meta = widgetMetadata(id) - return meta.description || "" - } - - function widgetSchema(id) { - var meta = widgetMetadata(id) - return Array.isArray(meta.schema) ? meta.schema : [] - } - - function widgetHasSettings(id) { - var rev = catalogRevision - var meta = widgetMetadata(id) - if (meta.settingsForm) return true - if (widgetSchema(id).length > 0) return true - return false - } - - function widgetAllowsMultiple(id) { - var meta = widgetMetadata(id) - if (meta.allowMultiple === true) return true - return canonicalWidgetId(id) === "omarchy.spacer" - } - - function catalogIds() { - var rev = catalogRevision - var ids = {} - if (root.barWidgetRegistry) { - var registered = root.barWidgetRegistry.availableIds() - for (var i = 0; i < registered.length; i++) ids[registered[i]] = true - } - if (root.pluginRegistry && root.pluginRegistry.installedPlugins) { - var plugins = root.pluginRegistry.installedPlugins - for (var pid in plugins) { - var manifest = plugins[pid] - if (manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1) - ids[pid] = true - } - } - return Object.keys(ids) - } - - function availableToAdd(section) { - var rev = catalogRevision - var barSections = ["left", "center", "right"] - - var existingInBar = {} - for (var s = 0; s < barSections.length; s++) { - var list = sectionArray(barSections[s]) - for (var i = 0; i < list.length; i++) existingInBar[list[i].id] = true - } - - var ids = catalogIds().sort(function(a, b) { return widgetName(a).localeCompare(widgetName(b)) }) - - var result = [] - for (var k = 0; k < ids.length; k++) { - var id = ids[k] - var meta = widgetMetadata(id) - var manifest = root.pluginRegistry ? root.pluginRegistry.installedPlugins[id] : null - var manifestIsBarWidget = manifest && Array.isArray(manifest.kinds) && manifest.kinds.indexOf("bar-widget") !== -1 - var isBarWidget = !!(meta && meta.source !== "plugin") || manifestIsBarWidget - if (!isBarWidget) continue - - var inSection = sectionArray(section) - var existsHere = false - for (var x = 0; x < inSection.length; x++) if (inSection[x].id === id) { existsHere = true; break } - var allowsMultiple = widgetAllowsMultiple(id) - if (!allowsMultiple && existingInBar[id]) continue - result.push({ id: id, name: widgetName(id), description: widgetDescription(id), - elsewhere: allowsMultiple && !!existingInBar[id] && !existsHere }) - } - return result - } - - // ---------------- file watchers ------------------------------------------ - FileView { - id: defaultsFile - path: root.defaultsPath - watchChanges: true - printErrors: true - onLoaded: root.loadConfig() - onLoadFailed: function(error) { console.warn("defaults load failed:", error, "path=" + root.defaultsPath) } - onFileChanged: reload() - } - - FileView { - id: userFile - path: root.userConfigPath - watchChanges: true - atomicWrites: true - printErrors: false - onLoaded: { - if (root.suppressReload) { root.suppressReload = false; return } - root.loadConfig() - } - onFileChanged: reload() - } - - // ---------------- window ------------------------------------------------- - // ---------------- per-widget settings dialog state ----------------------- - property bool widgetDialogVisible: false - property string widgetDialogSection: "" - property int widgetDialogIndex: -1 - property var widgetDialogEntry: ({}) - - function openWidgetSettings(sectionKey, entryIndex, entry) { - widgetDialogEntry = Util.cloneJson(entry) - widgetDialogSection = sectionKey - widgetDialogIndex = entryIndex - widgetDialogVisible = true - } - - function commitWidgetSettings() { - if (widgetDialogFormLoader.item && typeof widgetDialogFormLoader.item.saveSettings === "function") { - widgetDialogFormLoader.item.saveSettings() - } else { - root.updateEntry(widgetDialogSection, widgetDialogIndex, widgetDialogEntry) - } - widgetDialogVisible = false - } - - function discardWidgetSettings() { widgetDialogVisible = false } - - function widgetDialogFieldChanged(key, value) { - var copy = Util.cloneJson(widgetDialogEntry) - copy[key] = value - widgetDialogEntry = copy - if (widgetDialogFormLoader.item && "entry" in widgetDialogFormLoader.item) - widgetDialogFormLoader.item.entry = widgetDialogEntry - } - - FloatingWindow { - id: window - title: "Omarchy Bar Settings" - color: root.background - implicitWidth: Style.space(760) - implicitHeight: Style.space(620) - minimumSize: Qt.size(Style.space(620), Style.space(480)) - - onVisibleChanged: { - if (!visible && !root.closingFromHost && root.shell && typeof root.shell.hide === "function") - root.shell.hide("omarchy.settings") - if (visible) Qt.callLater(root.parkFocusOnSink) - } - - FocusScope { - id: navRoot - anchors.fill: parent - focus: true - - Component.onCompleted: navFocusSink.forceActiveFocus() - - // Invisible focus sink. When no specific body item is focused, - // activeFocus lives on this 1px Item so body controls render their - // unfocused state cleanly. - Item { - id: navFocusSink - width: 1 - height: 1 - objectName: "navFocusSink" - } - - Keys.priority: Keys.BeforeItem - Keys.onPressed: function(event) { - switch (event.key) { - case Qt.Key_Escape: - root.close(); event.accepted = true; return - case Qt.Key_J: - case Qt.Key_Down: - case Qt.Key_Tab: - root.focusBodyDelta(+1); event.accepted = true; return - case Qt.Key_K: - case Qt.Key_Up: - case Qt.Key_Backtab: - root.focusBodyDelta(-1); event.accepted = true; return - } - } - - Rectangle { - anchors.fill: parent - color: root.background - // No explicit border — the Hyprland window decoration already draws one. - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - // Header - Item { - Layout.fillWidth: true - Layout.preferredHeight: Math.max(Style.space(48), Style.font.heading + Style.spacing.controlPaddingY * 2) - - Text { - text: "Omarchy Bar Settings" - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.heading - font.bold: true - anchors.left: parent.left - anchors.leftMargin: Style.spacing.panelPadding - anchors.verticalCenter: parent.verticalCenter - } - - Text { - text: "~/.config/omarchy/shell.json" - color: Qt.darker(root.foreground, 1.8) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - anchors.right: parent.right - anchors.rightMargin: Style.spacing.panelPadding - anchors.verticalCenter: parent.verticalCenter - } - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.spacing.hairline - color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18) - } - - // Content - Flickable { - id: bodyScroll - Layout.fillWidth: true - Layout.fillHeight: true - Layout.margins: Style.spacing.panelPadding - clip: true - contentWidth: width - contentHeight: contentColumn.implicitHeight - boundsBehavior: Flickable.StopAtBounds - flickableDirection: Flickable.VerticalFlick - - ColumnLayout { - id: contentColumn - width: bodyScroll.width - spacing: Style.spacing.panelGap - - BarCategory { Layout.fillWidth: true } - } - } - } - } - } - - // ---------- per-widget settings overlay ----------------------------------- - Rectangle { - anchors.fill: parent - visible: root.widgetDialogVisible - color: Qt.rgba(0, 0, 0, 0.45) - z: 100 - - focus: visible - onVisibleChanged: if (visible) Qt.callLater(forceActiveFocus) - - MouseArea { - anchors.fill: parent - onClicked: root.discardWidgetSettings() - acceptedButtons: Qt.LeftButton | Qt.RightButton - } - - Keys.priority: Keys.BeforeItem - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Escape) { - root.discardWidgetSettings() - event.accepted = true - } - } - - Rectangle { - anchors.centerIn: parent - width: Math.min(Style.space(420), parent.width - Style.gapsOut * 2) - height: Math.min(parent.height - Style.space(60), Style.space(380)) - color: root.background - radius: Style.cornerRadius - border.color: Style.normalBorderFor(root.foreground, root.accent) - border.width: Style.normalBorderWidth - - MouseArea { anchors.fill: parent } - - ColumnLayout { - anchors.fill: parent - anchors.margins: Style.spacing.panelPadding - spacing: Style.spacing.rowPaddingX - - Text { - text: root.widgetName(root.widgetDialogEntry.id || "") - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.title - font.bold: true - } - - Text { - text: root.widgetDescription(root.widgetDialogEntry.id || "") - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Flickable { - id: widgetDialogScroll - Layout.fillWidth: true - Layout.fillHeight: true - contentWidth: width - contentHeight: widgetDialogFormLoader.item ? widgetDialogFormLoader.item.implicitHeight : 0 - clip: true - boundsBehavior: Flickable.StopAtBounds - flickableDirection: Flickable.VerticalFlick - ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } - - Loader { - id: widgetDialogFormLoader - width: widgetDialogScroll.width - sourceComponent: root.widgetDialogVisible ? formComponent(root.widgetDialogEntry.id || "") : null - onLoaded: { - if (item && "entry" in item) item.entry = root.widgetDialogEntry - if (item && "fieldChanged" in item) { - item.fieldChanged.connect(function(key, value) { root.widgetDialogFieldChanged(key, value) }) - } - } - } - } - - Row { - Layout.alignment: Qt.AlignRight - spacing: Style.spacing.rowGap - Button { - text: "Cancel" - foreground: root.foreground - fontFamily: root.fontFamily - focusable: true - onClicked: root.discardWidgetSettings() - } - Button { - text: "Apply" - foreground: root.foreground - fontFamily: root.fontFamily - focusable: true - bordered: true - onClicked: root.commitWidgetSettings() - } - } - } - } - } - } - - // ===================== bar category ====================================== - component BarCategory: ColumnLayout { - spacing: Style.spacing.panelGap - - Text { - text: "Bar" - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.iconLarge - font.bold: true - } - - Text { - text: "Drag widgets between the bar's three sections, drop in plugin widgets, and tweak per-widget options. Auto-saves to shell.json." - color: Qt.darker(root.foreground, 1.6) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - wrapMode: Text.WordWrap - Layout.fillWidth: true - } - - Row { - Layout.fillWidth: true - spacing: Style.spacing.panelGap - - Column { - spacing: Style.spacing.labelGap - - Text { - text: "Position" - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - font.bold: true - } - - ButtonGroup { - options: ["top", "right", "bottom", "left"] - value: root.draft.bar.position - foreground: root.foreground - background: root.background - accent: root.accent - fontFamily: root.fontFamily - onChanged: function(v) { - if (root.draft.bar.position === v) return - var next = Util.cloneJson(root.draft) - next.bar.position = v - root.draft = next - root.markDirty() - } - } - } - - Cmp.NDropdown { - label: "Center anchor" - value: root.draft.bar.centerAnchor || "(none)" - options: { - var list = ["(none)"] - var entries = root.draft.bar.layout.center || [] - for (var i = 0; i < entries.length; i++) list.push(entries[i].id) - return list - } - foreground: root.foreground - background: root.background - accent: root.accent - fontFamily: root.fontFamily - cornerRadius: root.cornerRadius - onChanged: function(v) { - var next = Util.cloneJson(root.draft) - next.bar.centerAnchor = v === "(none)" ? "" : v - root.draft = next - root.markDirty() - } - } - } - - Toggle { - Layout.fillWidth: true - label: "Transparent bar" - description: "Hide the bar background so the wallpaper shows through." - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - checked: root.draft.bar.transparent === true - onClicked: { - var next = Util.cloneJson(root.draft) - next.bar.transparent = !(next.bar.transparent === true) - root.draft = next - root.markDirty() - } - } - - SectionEditor { sectionKey: "left"; sectionLabel: "Bar · Left" } - SectionEditor { sectionKey: "center"; sectionLabel: "Bar · Center" } - SectionEditor { sectionKey: "right"; sectionLabel: "Bar · Right" } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: Style.spacing.hairline - color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12) - } - - Row { - Layout.alignment: Qt.AlignRight - Button { - text: "Reset bar to defaults" - foreground: root.urgent - fontFamily: root.fontFamily - focusable: true - bordered: true - onClicked: root.resetBarToDefaults() - } - } - } - - // ===================== bar layout pieces ================================= - component SectionEditor: Column { - id: section - - property string sectionKey: "" - property string sectionLabel: "" - property var entries: root.sectionArray(section.sectionKey) - Layout.fillWidth: true - Layout.topMargin: Style.spacing.rowGap - spacing: Style.spacing.rowGap - - Connections { - target: root - function onDraftRevisionChanged() { section.entries = root.sectionArray(section.sectionKey) } - } - - RowLayout { - width: section.width - spacing: Style.spacing.rowGap - - Text { - text: section.sectionLabel - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.subtitle - font.bold: true - Layout.alignment: Qt.AlignVCenter - } - - Text { - text: "· " + section.entries.length + (section.entries.length === 1 ? " widget" : " widgets") - color: Qt.darker(root.foreground, 1.5) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - Layout.alignment: Qt.AlignVCenter - } - - Item { Layout.fillWidth: true; implicitHeight: 1 } - - SearchableDropdown { - id: addPill - showLabel: false - triggerLabel: "󰐕 Add widget" - value: "" - placeholderText: "Search widgets..." - emptyText: "No widgets to add" - Layout.preferredWidth: Style.spacing.searchableDropdownWidth - Layout.alignment: Qt.AlignVCenter - options: { - var list = root.availableToAdd(section.sectionKey) - var out = [] - for (var i = 0; i < list.length; i++) { - out.push({ - value: list[i].id, - label: list[i].name + (list[i].elsewhere ? " (elsewhere)" : ""), - description: list[i].description || "" - }) - } - return out - } - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - onChanged: function(v) { - if (!v) return - root.addEntry(section.sectionKey, v) - addPill.value = "" - } - } - } - - Column { - Layout.fillWidth: true - width: section.width - spacing: Style.spacing.labelGap - - Repeater { - model: section.entries - delegate: WidgetCard { - required property var modelData - required property int index - width: section.width - sectionKey: section.sectionKey - entryIndex: index - entry: modelData - } - } - - Rectangle { - visible: section.entries.length === 0 - width: parent.width - height: Math.max(Style.space(32), Style.font.bodySmall + Style.spacing.controlPaddingY * 2) - radius: root.cornerRadius - color: Style.normalFillFor(root.foreground, root.accent) - border.color: Style.normalBorderFor(root.foreground, root.accent) - border.width: Style.normalBorderWidth - - Text { - anchors.centerIn: parent - text: "Empty — add a widget" - color: Qt.darker(root.foreground, 1.5) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } - } - } - } - - component WidgetCard: Rectangle { - id: card - property string sectionKey: "" - property int entryIndex: -1 - property var entry: ({}) - readonly property string entryId: entry && entry.id ? String(entry.id) : "" - readonly property string displayName: root.widgetName(entryId) - readonly property string description: root.widgetDescription(entryId) - readonly property bool hasSettings: root.widgetHasSettings(entryId) - - implicitHeight: Style.space(50) - radius: root.cornerRadius - color: cardArea.containsMouse || actionRow.activeFocus - ? Style.hoverFillFor(root.foreground, root.accent) - : Style.normalFillFor(root.foreground, root.accent) - border.color: cardArea.containsMouse || actionRow.activeFocus - ? Style.hoverBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent) - border.width: cardArea.containsMouse || actionRow.activeFocus ? Style.hoverBorderWidth : Style.normalBorderWidth - - Behavior on color { ColorAnimation { duration: 100 } } - - function maybeRestoreActionFocus() { - if (root.pendingActionFocusSection !== card.sectionKey) return - if (root.pendingActionFocusIndex !== card.entryIndex) return - - actionRow.actionIndex = root.pendingActionFocusAction - actionRow.clampActionIndex() - root.clearPendingActionFocus() - - Qt.callLater(function() { - actionRow.forceActiveFocus() - root.ensureBodyItemVisible(card) - }) - } - - onEntryIndexChanged: maybeRestoreActionFocus() - Component.onCompleted: maybeRestoreActionFocus() - - Connections { - target: root - function onPendingActionFocusRevisionChanged() { card.maybeRestoreActionFocus() } - } - - Row { - id: actionRow - anchors.right: parent.right - anchors.rightMargin: Style.spacing.controlGap - anchors.verticalCenter: parent.verticalCenter - spacing: Style.spacing.labelGap - activeFocusOnTab: true - - property int actionIndex: 0 - - onActiveFocusChanged: if (activeFocus) { - clampActionIndex() - root.ensureBodyItemVisible(card) - } - - function actionVisible(index) { - switch (index) { - case 0: return moveUpButton.visible && moveUpButton.enabled - case 1: return moveDownButton.visible && moveDownButton.enabled - case 2: return settingsButton.visible && settingsButton.enabled - case 3: return removeButton.visible && removeButton.enabled - } - return false - } - - function firstActionIndex() { - for (var i = 0; i < 4; i++) if (actionVisible(i)) return i - return 0 - } - - function clampActionIndex() { - if (actionVisible(actionIndex)) return - actionIndex = firstActionIndex() - } - - function moveAction(delta) { - clampActionIndex() - var next = actionIndex - while (true) { - next += delta - if (next < 0 || next > 3) return - if (actionVisible(next)) { actionIndex = next; return } - } - } - - function activateAction() { - clampActionIndex() - switch (actionIndex) { - case 0: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex - 1, actionIndex); return - case 1: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex + 1, actionIndex); return - case 2: root.openWidgetSettings(card.sectionKey, card.entryIndex, card.entry); return - case 3: root.removeEntry(card.sectionKey, card.entryIndex); return - } - } - - Keys.priority: Keys.BeforeItem - Keys.onPressed: function(event) { - if (event.key === Qt.Key_Left || event.text === "h") { - moveAction(-1); event.accepted = true; return - } - if (event.key === Qt.Key_Right || event.text === "l") { - moveAction(1); event.accepted = true; return - } - if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) { - activateAction(); event.accepted = true; return - } - } - - PanelActionButton { - id: moveUpButton - iconText: "󰁝" - tooltipText: "Move up" - foreground: root.foreground - fontFamily: root.fontFamily - fontSize: Style.font.subtitle - size: Style.space(26) - hasCursor: actionRow.activeFocus && actionRow.actionIndex === 0 - bordered: hasCursor - onHovered: function(h) { if (h) actionRow.actionIndex = 0 } - onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex - 1, 0) - } - PanelActionButton { - id: moveDownButton - iconText: "󰁅" - tooltipText: "Move down" - foreground: root.foreground - fontFamily: root.fontFamily - fontSize: Style.font.subtitle - size: Style.space(26) - hasCursor: actionRow.activeFocus && actionRow.actionIndex === 1 - bordered: hasCursor - onHovered: function(h) { if (h) actionRow.actionIndex = 1 } - onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex + 1, 1) - } - PanelActionButton { - id: settingsButton - iconText: "󰒓" - tooltipText: "Settings" - foreground: root.foreground - fontFamily: root.fontFamily - fontSize: Style.font.subtitle - size: Style.space(26) - visible: card.hasSettings - hasCursor: actionRow.activeFocus && actionRow.actionIndex === 2 - bordered: hasCursor - onVisibleChanged: if (!visible && actionRow.actionIndex === 2) actionRow.clampActionIndex() - onHovered: function(h) { if (h) actionRow.actionIndex = 2 } - onClicked: root.openWidgetSettings(card.sectionKey, card.entryIndex, card.entry) - } - PanelActionButton { - id: removeButton - iconText: "󰅖" - tooltipText: "Remove" - foreground: root.urgent - hoverColor: root.urgent - fontFamily: root.fontFamily - fontSize: Style.font.subtitle - size: Style.space(26) - hasCursor: actionRow.activeFocus && actionRow.actionIndex === 3 - bordered: hasCursor - onHovered: function(h) { if (h) actionRow.actionIndex = 3 } - onClicked: root.removeEntry(card.sectionKey, card.entryIndex) - } - } - - Column { - anchors.left: parent.left - anchors.right: actionRow.left - anchors.leftMargin: Style.spacing.rowPaddingX - anchors.rightMargin: Style.spacing.rowPaddingX - anchors.verticalCenter: parent.verticalCenter - spacing: Style.spacing.xxs - - Text { - text: card.displayName - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.body - font.bold: true - elide: Text.ElideRight - width: parent.width - } - Text { - visible: text !== "" - text: card.description - color: Qt.darker(root.foreground, 1.5) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - elide: Text.ElideRight - width: parent.width - } - } - - MouseArea { - id: cardArea - anchors.fill: parent - hoverEnabled: true - acceptedButtons: Qt.NoButton - } - } - - // ---------------- per-widget form resolution ----------------------------- - function formComponent(id) { - var meta = widgetMetadata(id) - if (meta && meta.settingsForm) { - switch (meta.settingsForm) { - case "spacerSettings": return spacerSettingsComponent - case "clockSettings": return clockSettingsComponent - case "weatherSettings": return weatherSettingsComponent - } - } - if (widgetSchema(id).length > 0) return dynamicSettingsComponent - return null - } - - Component { - id: dynamicSettingsComponent - Cmp.DynamicSettingsForm { - schema: root.widgetSchema(entry.id || "") - pluginSourceDir: root.widgetSourceDir(entry.id || "") - foreground: root.foreground - fontFamily: root.fontFamily - } - } - - Component { - id: spacerSettingsComponent - - Column { - id: spacerForm - signal fieldChanged(string key, var value) - property var entry: ({}) - - spacing: Style.spacing.rowGap - width: parent ? parent.width : 0 - - NumberField { - label: "Size (pixels)" - from: 0 - to: 256 - value: spacerForm.entry.size !== undefined ? spacerForm.entry.size : 12 - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - onModified: function(v) { spacerForm.fieldChanged("size", v) } - } - } - } - - Component { - id: clockSettingsComponent - - Column { - id: clockForm - signal fieldChanged(string key, var value) - property var entry: ({}) - - spacing: Style.spacing.rowGap - width: parent ? parent.width : 0 - - component ClockField: TextField { - property string fieldKey: "" - width: parent.width - foreground: root.foreground - accent: root.accent - font.family: root.fontFamily - font.pixelSize: Style.font.body - onEditingFinished: if (fieldKey) clockForm.fieldChanged(fieldKey, text) - } - - Text { - text: "Horizontal format" - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } - ClockField { - fieldKey: "format" - text: clockForm.entry.format || "dddd HH:mm" - } - - Text { - text: "Alternate format (click to toggle)" - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } - ClockField { - fieldKey: "formatAlt" - text: clockForm.entry.formatAlt || "dd MMMM 'W'ww yyyy" - } - - Text { - text: "Vertical format (left/right bars)" - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } - ClockField { - fieldKey: "verticalFormat" - text: clockForm.entry.verticalFormat || "HH\n—\nmm" - } - } - } - - Component { - id: weatherSettingsComponent - - Column { - id: weatherForm - signal fieldChanged(string key, var value) - property var entry: ({}) - - spacing: Style.spacing.rowGap - width: parent ? parent.width : 0 - - NumberField { - label: "Auto-refresh interval (minutes)" - from: 1 - to: 1440 - value: weatherForm.entry.refreshMinutes !== undefined ? weatherForm.entry.refreshMinutes : 15 - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - onModified: function(v) { weatherForm.fieldChanged("refreshMinutes", v) } - } - } - } - - -} diff --git a/shell/plugins/settings/components/DynamicSettingsForm.qml b/shell/plugins/settings/components/DynamicSettingsForm.qml deleted file mode 100644 index b697a03f..00000000 --- a/shell/plugins/settings/components/DynamicSettingsForm.qml +++ /dev/null @@ -1,281 +0,0 @@ -import QtQuick -import QtQuick.Controls as QQC -import qs.Commons -import qs.Ui as Ui - -Column { - id: root - - signal fieldChanged(string key, var value) - - property var schema: [] - property var entry: ({}) - property color foreground: Color.popups.text - property color accent: Color.accent - property string fontFamily: Style.font.family - // Resolved on disk path of the plugin that owns this form, used to - // resolve relative argv entries in a multiselect's `optionsCommand`. - property string pluginSourceDir: "" - - spacing: Style.spacing.xl - width: parent ? parent.width : 0 - - function currentValue(field) { - if (entry && entry[field.key] !== undefined) return entry[field.key] - if (field.defaultValue !== undefined) return field.defaultValue - - switch (field.type) { - case "boolean": return false - case "integer": - case "number": return field.min !== undefined ? field.min : 0 - case "enum": return field.options && field.options.length > 0 ? field.options[0] : "" - case "multiselect": return [] - default: return "" - } - } - - // Resolve a multiselect's optionsCommand argv against the plugin's source - // directory. The first entry is treated as a path relative to the plugin - // dir unless it is absolute or begins with ".". Subsequent entries pass - // through unchanged. - // Resolve a multiselect's optionsCommand argv against the plugin's source - // directory. The first argv entry must be a relative path inside the - // plugin dir (no leading `/`, no `..` segments, no empty segments); - // anything else is rejected and the field falls back to whatever static - // `options` it has. Subsequent argv entries pass through unchanged. We - // avoid `Array.isArray` because schema-supplied arrays sometimes arrive - // as QML JSValue lists that don't satisfy it; iterating by `.length` - // works for both real arrays and JSValue lists. - function resolveOptionsCommand(field) { - var oc = field ? field.optionsCommand : undefined - if (!oc || typeof oc.length !== "number" || oc.length === 0) return [] - var argv = [] - for (var i = 0; i < oc.length; i++) argv.push(String(oc[i])) - if (!pluginSourceDir) return [] - var head = argv[0] - if (head.length === 0 || head.charAt(0) === "/") { - console.warn("DynamicSettingsForm: optionsCommand must be a path relative to the plugin dir, got: " + head) - return [] - } - var rel = head.replace(/^\.\//, "") - var segments = rel.split("/") - for (var s = 0; s < segments.length; s++) { - if (segments[s] === ".." || segments[s] === "") { - console.warn("DynamicSettingsForm: optionsCommand may not contain '..' or empty segments: " + head) - return [] - } - } - var dir = pluginSourceDir.replace(/\/$/, "") - argv[0] = dir + "/" + rel - return argv - } - - Repeater { - model: root.schema - - Column { - required property var modelData - - width: root.width - spacing: Style.spacing.labelGap - - Text { - visible: text !== "" && String(modelData && modelData.type ? modelData.type : "") !== "boolean" - text: modelData && modelData.label ? modelData.label : (modelData && modelData.key ? modelData.key : "") - color: Qt.darker(root.foreground, 1.3) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - font.bold: true - } - - Text { - visible: !!(modelData && modelData.description) && String(modelData && modelData.type ? modelData.type : "") !== "boolean" - text: modelData ? (modelData.description || "") : "" - color: Qt.darker(root.foreground, 1.6) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - wrapMode: Text.WordWrap - width: parent.width - } - - Loader { - width: parent.width - height: item ? item.implicitHeight : 0 - sourceComponent: { - if (!modelData || !modelData.type) return stringField - switch (String(modelData.type)) { - case "boolean": return booleanField - case "enum": return enumField - case "integer": return integerField - case "number": return numberField - case "multiselect": return multiselectField - default: return stringField - } - } - onLoaded: { - if (!item) return - item.fieldKey = modelData.key - item.field = modelData - } - } - } - } - - Text { - visible: !root.schema || root.schema.length === 0 - text: "No settings." - color: Qt.darker(root.foreground, 1.5) - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - } - - Component { - id: stringField - - Ui.TextField { - property string fieldKey: "" - property var field: ({}) - - width: parent.width - foreground: root.foreground - accent: root.accent - font.family: root.fontFamily - font.pixelSize: Style.font.body - text: root.currentValue(field) === undefined ? "" : String(root.currentValue(field)) - - onEditingFinished: if (fieldKey) root.fieldChanged(fieldKey, text) - } - } - - Component { - id: booleanField - - Ui.Toggle { - property string fieldKey: "" - property var field: ({}) - - width: parent.width - label: field && field.label ? String(field.label) : "Enabled" - description: field && field.description ? String(field.description) : "" - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - checked: !!root.currentValue(field) - - onClicked: if (fieldKey) root.fieldChanged(fieldKey, !checked) - } - } - - Component { - id: enumField - - Ui.Dropdown { - property string fieldKey: "" - property var field: ({}) - - width: parent.width - showLabel: false - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - options: field && field.options ? field.options : [] - value: String(root.currentValue(field)) - - onChanged: function(value) { - if (fieldKey) root.fieldChanged(fieldKey, value) - } - } - } - - Component { - id: integerField - - Ui.NumberField { - property string fieldKey: "" - property var field: ({}) - - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - from: field && field.min !== undefined ? field.min : 0 - to: field && field.max !== undefined ? field.max : 9999 - stepSize: field && field.step !== undefined ? field.step : 1 - value: { - var v = root.currentValue(field) - var n = typeof v === "number" ? v : parseInt(String(v || 0), 10) - return isFinite(n) ? n : 0 - } - - onModified: function(value) { if (fieldKey) root.fieldChanged(fieldKey, value) } - } - } - - Component { - id: multiselectField - - Ui.MultiSelect { - property string fieldKey: "" - property var field: ({}) - - width: parent.width - foreground: root.foreground - accent: root.accent - fontFamily: root.fontFamily - showLabel: false - // MultiSelect's arrayFrom() tolerates JSValue-style schema arrays, so - // we can hand it `field.options` directly without an Array.isArray - // guard that would silently drop JSValue lists. - options: field ? (field.options || []) : [] - optionsCommand: root.resolveOptionsCommand(field) - optionsCommandCwd: root.pluginSourceDir - placeholderText: field && field.placeholderText ? String(field.placeholderText) : "Search..." - emptyText: field && field.emptyText ? String(field.emptyText) : "No options" - noSelectionText: field && field.noSelectionText ? String(field.noSelectionText) : "None selected" - values: { - var v = root.currentValue(field) - return v ? v : [] - } - - onChanged: function(arr) { if (fieldKey) root.fieldChanged(fieldKey, arr) } - } - } - - Component { - id: numberField - - Row { - property string fieldKey: "" - property var field: ({}) - property real currentNumber: { - var v = root.currentValue(field) - var n = typeof v === "number" ? v : parseFloat(String(v || 0)) - return isFinite(n) ? n : 0 - } - - width: parent.width - spacing: Style.spacing.rowGap - - QQC.Slider { - id: slider - - width: parent.width - readout.width - Style.spacing.rowGap - from: field && field.min !== undefined ? field.min : 0 - to: field && field.max !== undefined ? field.max : 1 - stepSize: field && field.step !== undefined ? field.step : 0.01 - value: parent.currentNumber - - onMoved: if (parent.fieldKey) root.fieldChanged(parent.fieldKey, value) - } - - Text { - id: readout - - text: slider.value.toFixed(2) - color: root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.bodySmall - anchors.verticalCenter: parent.verticalCenter - } - } - } -} diff --git a/shell/plugins/settings/components/NDropdown.qml b/shell/plugins/settings/components/NDropdown.qml deleted file mode 100644 index 5a2e4242..00000000 --- a/shell/plugins/settings/components/NDropdown.qml +++ /dev/null @@ -1,143 +0,0 @@ -import QtQuick -import QtQuick.Controls -import qs.Commons - -// Themed ComboBox + popup. Anchors below the trigger, paints with the host -// shell's foreground/background palette, and inherits the shell-wide corner -// radius so nothing renders rounded when the user has set sharp corners. -Item { - id: root - - property string label: "" - property string value: "" - property var options: [] - property color foreground: Color.popups.text - property color background: Color.popups.background - property color accent: Color.accent - property string fontFamily: Style.font.family - property int cornerRadius: 0 - property int rowHeight: Style.spacing.controlHeight - property int popupRowHeight: Style.spacing.popupRowHeight - property bool showLabel: true - - signal changed(string value) - - implicitWidth: Style.spacing.dropdownWidth - implicitHeight: showLabel ? rowHeight + Style.spacing.huge : rowHeight - - Column { - anchors.fill: parent - spacing: Style.spacing.labelGap - - Text { - visible: root.showLabel && root.label !== "" - text: root.label - color: Qt.darker(root.foreground, 1.4) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - font.bold: true - } - - ComboBox { - id: combo - width: parent.width - height: root.rowHeight - font.family: root.fontFamily - font.pixelSize: Style.font.body - model: root.options - currentIndex: { - for (var i = 0; i < model.length; i++) if (model[i] === root.value) return i - return -1 - } - onActivated: function(index) { - if (index >= 0 && index < model.length) root.changed(model[index]) - } - - background: Rectangle { - color: combo.activeFocus - ? Style.focusFillFor(root.foreground, root.accent) - : (combo.hovered - ? Style.hoverFillFor(root.foreground, root.accent) - : Style.normalFillFor(root.foreground, root.accent)) - border.color: combo.activeFocus - ? Style.focusBorderFor(root.foreground, root.accent) - : (combo.hovered - ? Style.hoverBorderFor(root.foreground, root.accent) - : Style.normalBorderFor(root.foreground, root.accent)) - border.width: combo.activeFocus - ? Style.focusBorderWidth - : (combo.hovered ? Style.hoverBorderWidth : Style.normalBorderWidth) - radius: root.cornerRadius - } - - contentItem: Text { - leftPadding: Style.spacing.controlGap - rightPadding: Style.space(24) - text: combo.displayText - color: root.foreground - font: combo.font - verticalAlignment: Text.AlignVCenter - } - - indicator: Text { - x: combo.width - width - Style.spacing.controlGap - y: combo.topPadding + (combo.availableHeight - height) / 2 - text: "▾" - color: Qt.darker(root.foreground, 1.2) - font.family: root.fontFamily - font.pixelSize: Style.font.caption - } - - popup: Popup { - // Anchor the popup directly under the field, full width, sharp/round - // matching the shell radius. Override the native white-with-blue look. - y: combo.height - width: combo.width - implicitHeight: Math.min(contentItem.implicitHeight, root.popupRowHeight * 8) - padding: Style.spacing.hairline - - background: Rectangle { - color: root.background - border.color: Style.normalBorderFor(root.foreground, root.accent) - border.width: Style.normalBorderWidth - radius: root.cornerRadius - } - - contentItem: ListView { - clip: true - implicitHeight: contentHeight - model: combo.delegateModel - currentIndex: combo.highlightedIndex - boundsBehavior: Flickable.StopAtBounds - } - } - - delegate: ItemDelegate { - required property var modelData - required property int index - - width: combo.width - height: root.popupRowHeight - padding: 0 - - contentItem: Text { - text: String(modelData) - color: index === combo.highlightedIndex ? Style.hoverStateColor(root.foreground, root.accent) : root.foreground - font.family: root.fontFamily - font.pixelSize: Style.font.body - leftPadding: Style.spacing.controlPaddingX - rightPadding: Style.spacing.controlPaddingX - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } - - background: Rectangle { - color: index === combo.highlightedIndex - ? Style.hoverFillFor(root.foreground, root.accent) - : "transparent" - radius: 0 - } - } - } - } -} diff --git a/shell/plugins/settings/manifest.json b/shell/plugins/settings/manifest.json deleted file mode 100644 index 0f27fdc9..00000000 --- a/shell/plugins/settings/manifest.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "schemaVersion": 1, - "id": "omarchy.settings", - "name": "Omarchy Bar Settings", - "version": "1.0.0", - "author": "Omarchy", - "description": "Customize the Omarchy bar position and widgets", - "kinds": [ - "panel" - ], - "entryPoints": { - "panel": "SettingsPanel.qml" - } -} diff --git a/shell/services/BarWidgetRegistry.qml b/shell/services/BarWidgetRegistry.qml index 6639d298..69491289 100644 --- a/shell/services/BarWidgetRegistry.qml +++ b/shell/services/BarWidgetRegistry.qml @@ -3,7 +3,7 @@ import QtQuick // Instance, not a singleton — instantiated once by shell.qml and injected into // plugins that need to read or extend the widget catalogue. Relative-path // singleton imports were creating per-importer instances which prevented the -// bar settings panel from seeing what the bar registered. +// shell host from seeing what the bar registered. QtObject { id: registry diff --git a/shell/shell.qml b/shell/shell.qml index b87115dc..3de55722 100644 --- a/shell/shell.qml +++ b/shell/shell.qml @@ -651,6 +651,10 @@ ShellRoot { return JSON.stringify(shell.bar && shell.bar.debugBarGeometry ? shell.bar.debugBarGeometry() : []) } + function openBarConfig(): string { + return shell.bar && shell.bar.openConfigPanel && shell.bar.openConfigPanel() ? "ok" : "unknown" + } + function summon(id: string, payloadJson: string): string { return shell.summon(id, payloadJson) ? "ok" : "unknown" } diff --git a/test/shell.d/runtime-smoke-test.sh b/test/shell.d/runtime-smoke-test.sh index 3654a428..675f89ed 100755 --- a/test/shell.d/runtime-smoke-test.sh +++ b/test/shell.d/runtime-smoke-test.sh @@ -95,7 +95,7 @@ done jq -e ' map(.id) as $ids | - all(["omarchy.menu", "omarchy.settings", "omarchy.notifications", "omarchy.clock"][]; $ids | index(.)) and + all(["omarchy.menu", "omarchy.notifications", "omarchy.clock", "omarchy.osd"][]; $ids | index(.)) and all(.[]; (.kinds | type == "array") and (.enabled | type == "boolean") and (.firstParty | type == "boolean")) ' <<<"$plugins" >/dev/null || { printf 'Plugins:\n%s\n' "$plugins" | jq . >&2 @@ -115,8 +115,7 @@ jq -e ' } pass "shell IPC returns effective shell config" -[[ $(shell_ipc shell summon omarchy.settings "{}") == "ok" ]] || fail_with_log "shell IPC summons settings panel" -shell_ipc_quiet shell hide omarchy.settings >/dev/null +[[ $(shell_ipc shell openBarConfig) == "ok" ]] || fail_with_log "shell IPC opens bar config panel" [[ $(shell_ipc shell summon omarchy.launcher '{"query":"term"}') == "ok" ]] || fail_with_log "shell IPC summons launcher overlay" shell_ipc_quiet shell hide omarchy.launcher >/dev/null [[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"