Replace bar settings with inline config panel
This commit is contained in:
@@ -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
|
||||
|
||||
+100
-19
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user