From 4b4df8b4f58409a7fb3c2035a5a4bc28457a1b2c Mon Sep 17 00:00:00 2001 From: Ryan Hughes Date: Mon, 18 May 2026 00:29:26 -0400 Subject: [PATCH] Fold settings ActionPill + IconButton into qs.Ui kit --- .../omarchy-shell/Ui/PanelActionButton.qml | 30 +++- .../omarchy-shell/Ui/PillButton.qml | 30 +++- default/quickshell/omarchy-shell/Ui/README.md | 4 +- .../plugins/dev-gallery/GalleryPanel.qml | 19 +++ .../plugins/settings/SettingsPanel.qml | 157 +++++++----------- 5 files changed, 135 insertions(+), 105 deletions(-) diff --git a/default/quickshell/omarchy-shell/Ui/PanelActionButton.qml b/default/quickshell/omarchy-shell/Ui/PanelActionButton.qml index abd4c140..4273f4b5 100644 --- a/default/quickshell/omarchy-shell/Ui/PanelActionButton.qml +++ b/default/quickshell/omarchy-shell/Ui/PanelActionButton.qml @@ -12,6 +12,12 @@ import qs.Commons // hover state visuals; mouse hover does NOT update any panel cursor state // here because action buttons are not cursor targets — the row they live // in is. +// +// Set `focusable: true` to make the button keyboard-tabbable with an +// accent focus ring (Style.focusBorderColor / FillColor / Width). Use this +// in form contexts (the bar settings widget cards) 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. Rectangle { id: root @@ -24,15 +30,28 @@ Rectangle { property real fontSize: 14 property real size: 22 + property bool focusable: false + signal clicked() + activeFocusOnTab: focusable + Keys.onReturnPressed: if (focusable) root.clicked() + Keys.onEnterPressed: if (focusable) root.clicked() + Keys.onSpacePressed: if (focusable) root.clicked() + implicitWidth: size implicitHeight: size radius: Style.cornerRadius - color: mouse.containsMouse && root.enabled - ? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20) - : "transparent" + readonly property bool _showFocusRing: focusable && activeFocus + + color: _showFocusRing + ? Style.focusFillColor + : (mouse.containsMouse && root.enabled + ? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20) + : "transparent") + border.width: _showFocusRing ? Style.focusBorderWidth : 0 + border.color: _showFocusRing ? Style.focusBorderColor : "transparent" Behavior on color { ColorAnimation { duration: 60 } } @@ -52,7 +71,10 @@ Rectangle { hoverEnabled: true cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor enabled: root.enabled - onClicked: root.clicked() + onClicked: { + if (root.focusable) root.forceActiveFocus() + root.clicked() + } } PanelToolTip { diff --git a/default/quickshell/omarchy-shell/Ui/PillButton.qml b/default/quickshell/omarchy-shell/Ui/PillButton.qml index 65acc241..7cdae396 100644 --- a/default/quickshell/omarchy-shell/Ui/PillButton.qml +++ b/default/quickshell/omarchy-shell/Ui/PillButton.qml @@ -28,6 +28,24 @@ Rectangle { // indistinguishable. Default false; bind from a panel's cursor state. property bool hasCursor: false + // Tab-focusable form-button mode. Enables activeFocusOnTab and + // Enter/Return/Space activation, and uses Style.focusBorderColor / FillColor + // for the focus ring (distinct from the panel-cursor `hot` state). Set + // true for settings buttons (Save, Cancel, Reset); leave false for the + // panel-cursor pills (DNS picker, header actions). + property bool focusable: false + + // Persistent 1px foreground border at idle. Use for "primary" form buttons + // (Save, Apply, + Add widget) so they read as buttons before the cursor + // hits them. The hot/cursor state paints its own 1px border, so changing + // this doesn't affect panel-pill visuals. + property bool bordered: false + + activeFocusOnTab: focusable + Keys.onReturnPressed: if (focusable) root.clicked() + Keys.onEnterPressed: if (focusable) root.clicked() + Keys.onSpacePressed: if (focusable) root.clicked() + ToolTip { visible: root.tooltipText !== "" && mouseArea.containsMouse text: root.tooltipText @@ -66,11 +84,18 @@ Rectangle { // as hovered, matching CursorSurface's behaviour for navigable rows). readonly property bool hot: mouseArea.containsMouse || hasCursor + // Tab-focus styling wins over hot — the accent ring is the strongest + // signal and shouldn't be masked by a hover landing on the focused item. + readonly property bool _showFocusRing: focusable && activeFocus + color: mouseArea.pressed ? pressedBackground + : _showFocusRing ? Style.focusFillColor : hot ? hoverBackground : (active ? activeBackground : background) - border.width: hot ? 1 : 0 - border.color: foreground + border.width: _showFocusRing ? Style.focusBorderWidth + : hot ? 1 + : (bordered ? 1 : 0) + border.color: _showFocusRing ? Style.focusBorderColor : foreground Behavior on color { ColorAnimation { duration: 120 } @@ -110,6 +135,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: function(mouse) { + if (root.focusable) root.forceActiveFocus() if (mouse.button === Qt.RightButton) root.rightClicked() else root.clicked() } diff --git a/default/quickshell/omarchy-shell/Ui/README.md b/default/quickshell/omarchy-shell/Ui/README.md index c0c24860..4eb57838 100644 --- a/default/quickshell/omarchy-shell/Ui/README.md +++ b/default/quickshell/omarchy-shell/Ui/README.md @@ -53,12 +53,12 @@ Grouped by what they're for, not alphabetically. | Type | Purpose | |---|---| -| `PillButton` | Rounded button with optional icon + label + tooltip. Has `active`, `hasCursor` (keyboard cursor), and `enabled`. Hover and keyboard cursor render identically (fill + border) via the shared `hot` state. | +| `PillButton` | Rounded button with optional icon + label + tooltip. Has `active`, `hasCursor` (keyboard cursor), `focusable` (Tab-focus with accent ring), `bordered` (persistent 1px idle border for primary form buttons), and `enabled`. Hover and keyboard cursor render identically (fill + border) via the shared `hot` state; Tab-focus uses an accent ring that wins over both. | | `CursorPill` | `PillButton` that participates in a panel's single-cursor model. Adds a `hovered(bool)` signal so the panel can update its cursor state on mouse enter/leave. Use for DNS-pill / header-pill / segmented-choice patterns. | | `ChoiceButton` | A single button in a mutually-exclusive choice group (segmented control). `selected` uses accent fill+border; focus uses `Style.focusBorderColor` so keyboard nav reads differently from selection. | | `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. | | `TextField` | Single-line input. Inherits from Qt Quick Controls `TextField` so all of its base API (text, placeholderText, accepted, validator, ...) is available. Adds `password: bool`, `foreground` / `accent` / `selectionTint` color overrides, and `horizontalPadding` / `verticalPadding` size knobs. Focus styling uses `Style.focusBorderColor` to match `Toggle` and `ChoiceButton`. | -| `PanelActionButton` | 22×22 right-edge action button (confirm, forget, unpair). `hoverColor` swaps between default foreground tint and urgent (red) tint. | +| `PanelActionButton` | 22×22 right-edge action button (confirm, forget, unpair). `hoverColor` swaps between default foreground tint and urgent (red) tint. `focusable: true` enables Tab-focus with an accent ring — used for the bar settings widget-card row controls. | | `PanelSlider` | Volume/progress slider. Drag, click track, or wheel. `moved(value)` fires per change, `released(value)` once at end. (Named to avoid colliding with `QtQuick.Controls.Slider`.) | | `WidgetButton` | Bar widget chrome — for the strip itself, not for inside panels. | diff --git a/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml b/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml index 710eefa3..8c587a50 100644 --- a/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/dev-gallery/GalleryPanel.qml @@ -394,6 +394,14 @@ Item { fontFamily: root.fontFamily active: true } + + PillButton { + text: "Apply" + foreground: root.foreground + fontFamily: root.fontFamily + focusable: true + bordered: true + } } } } @@ -516,6 +524,17 @@ Item { fontFamily: root.fontFamily enabled: false } + + PanelActionButton { + iconText: "⚙" + tooltipText: "Focusable (settings form button)" + foreground: root.foreground + panelBackground: root.background + fontFamily: root.fontFamily + fontSize: 13 + size: 26 + focusable: true + } } } } diff --git a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml index dd961ec0..eb377d56 100644 --- a/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml +++ b/default/quickshell/omarchy-shell/plugins/settings/SettingsPanel.qml @@ -671,9 +671,12 @@ Item { Row { Layout.alignment: Qt.AlignRight - ActionPill { + PillButton { text: "Reset bar to defaults" foreground: root.urgent + fontFamily: root.fontFamily + focusable: true + bordered: true onClicked: root.resetBarToDefaults() } } @@ -725,87 +728,6 @@ Item { } } - component ActionPill: Rectangle { - id: pill - property string text: "" - property color foreground: root.foreground - property bool bordered: true - signal clicked() - - activeFocusOnTab: true - Keys.onReturnPressed: pill.clicked() - Keys.onEnterPressed: pill.clicked() - Keys.onSpacePressed: pill.clicked() - - implicitWidth: pillLabel.implicitWidth + 22 - implicitHeight: 26 - radius: root.cornerRadius - color: pill.activeFocus - ? root.focusFillColor - : (pillArea.containsMouse ? Qt.rgba(pill.foreground.r, pill.foreground.g, pill.foreground.b, 0.15) : "transparent") - border.color: pill.activeFocus ? root.focusBorderColor : (pill.bordered ? pill.foreground : "transparent") - border.width: pill.activeFocus ? root.focusBorderWidth : 1 - - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - id: pillLabel - anchors.centerIn: parent - text: pill.text - color: pill.foreground - font.family: root.fontFamily - font.pixelSize: 11 - } - - MouseArea { - id: pillArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: pill.clicked() - } - } - - component IconButton: Rectangle { - id: iconButton - property string glyph: "" - property string tooltip: "" - property color foreground: root.foreground - signal clicked() - - activeFocusOnTab: true - Keys.onReturnPressed: iconButton.clicked() - Keys.onEnterPressed: iconButton.clicked() - Keys.onSpacePressed: iconButton.clicked() - - implicitWidth: 26 - implicitHeight: 26 - radius: root.cornerRadius - color: iconButton.activeFocus - ? root.focusFillColor - : (iconArea.containsMouse ? Qt.rgba(iconButton.foreground.r, iconButton.foreground.g, iconButton.foreground.b, 0.18) : "transparent") - border.color: iconButton.activeFocus ? root.focusBorderColor : "transparent" - border.width: iconButton.activeFocus ? root.focusBorderWidth : 0 - - Behavior on color { ColorAnimation { duration: 100 } } - - Text { - anchors.centerIn: parent - text: iconButton.glyph - color: iconButton.foreground - font.family: root.fontFamily - font.pixelSize: 13 - } - - MouseArea { - id: iconArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor - onClicked: iconButton.clicked() - } - } - // ===================== bar layout pieces ================================= component SectionEditor: Column { id: section @@ -848,9 +770,13 @@ Item { height: 1 } - ActionPill { + PillButton { id: addPill text: "+ Add widget" + foreground: root.foreground + fontFamily: root.fontFamily + focusable: true + bordered: true onClicked: addPopup.open() } } @@ -993,26 +919,50 @@ Item { anchors.verticalCenter: parent.verticalCenter spacing: 4 - IconButton { - glyph: "↑" - tooltip: "Move up" + PanelActionButton { + iconText: "↑" + tooltipText: "Move up" + foreground: root.foreground + panelBackground: root.background + fontFamily: root.fontFamily + fontSize: 13 + size: 26 + focusable: true onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex - 1) } - IconButton { - glyph: "↓" - tooltip: "Move down" + PanelActionButton { + iconText: "↓" + tooltipText: "Move down" + foreground: root.foreground + panelBackground: root.background + fontFamily: root.fontFamily + fontSize: 13 + size: 26 + focusable: true onClicked: root.moveEntry(card.sectionKey, card.entryIndex, card.entryIndex + 1) } - IconButton { - glyph: "⚙" - tooltip: "Settings" + PanelActionButton { + iconText: "⚙" + tooltipText: "Settings" + foreground: root.foreground + panelBackground: root.background + fontFamily: root.fontFamily + fontSize: 13 + size: 26 + focusable: true visible: card.hasSettings onClicked: settingsLoader.open(card.entry) } - IconButton { - glyph: "✕" - tooltip: "Remove" + PanelActionButton { + iconText: "✕" + tooltipText: "Remove" foreground: root.urgent + hoverColor: root.urgent + panelBackground: root.background + fontFamily: root.fontFamily + fontSize: 13 + size: 26 + focusable: true onClicked: root.removeEntry(card.sectionKey, card.entryIndex) } } @@ -1140,8 +1090,21 @@ Item { Row { Layout.alignment: Qt.AlignRight spacing: 8 - ActionPill { text: "Cancel"; bordered: false; onClicked: dialog.discard() } - ActionPill { text: "Apply"; onClicked: dialog.commit() } + PillButton { + text: "Cancel" + foreground: root.foreground + fontFamily: root.fontFamily + focusable: true + onClicked: dialog.discard() + } + PillButton { + text: "Apply" + foreground: root.foreground + fontFamily: root.fontFamily + focusable: true + bordered: true + onClicked: dialog.commit() + } } } }