Fold settings ActionPill + IconButton into qs.Ui kit

This commit is contained in:
Ryan Hughes
2026-05-18 00:29:26 -04:00
parent 4529988676
commit 4b4df8b4f5
5 changed files with 135 additions and 105 deletions
@@ -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 {
@@ -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()
}
@@ -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. |
@@ -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
}
}
}
}
@@ -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()
}
}
}
}