mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-01 20:28:16 +02:00
Unify PillButton, CursorPill, ChoiceButton into qs.Ui.Button + ButtonGroup
The three components were three takes on the same shape \u2014 a clickable rectangle with text/icon, a hot/hover state, an optional persistent border, and an optional 'selected' or 'active' highlight. CursorPill was a 30-line PillButton wrapper that added a hovered() signal; ChoiceButton was effectively PillButton with selected: bool painting an accent fill+border. Collapse them into a single qs.Ui.Button. State flags compose independently: hasCursor / hover hot fill active persistent foreground-tint fill selected accent fill + accent border bordered: true persistent 1px idle border (form primaries) focusable: true Tab focus paints the accent ring pressed pressed fill The hovered(bool) signal is now built-in, so CursorPill's wrapper is unnecessary. ButtonGroup wraps a Row+Repeater for the form-style 'pick one of N' pattern; panel-cursor-driven cases still compose Buttons directly in a Row with per-instance hasCursor wiring. Theme tokens move into a new [style] section in shell.toml: border-width = 1 focus-border-width = 3 idle-border-alpha = 0.4 hot-fill-alpha = 0.08 selected-fill-alpha = 0.18 pressed-fill-alpha = 0.22 focus-fill-alpha = 0.22 Style.qml parses these out of the same shell.toml [font] / [bar] already reads, and exposes pre-computed Style.hotFill / selectedFill / pressedFill / idleBorderColor / selectedAccentFill / borderWidth + the existing focusBorder* tokens. Themes that don't ship a [style] section get the previous defaults unchanged. Dev gallery consolidates three sections (PillButton, CursorPill, ChoiceButton) into Button + ButtonGroup, with the cursor model sections renamed accordingly.
This commit is contained in:
@@ -12,6 +12,17 @@ active = "{{ color1 }}"
|
||||
size-horizontal = 26
|
||||
size-vertical = 28
|
||||
|
||||
[style]
|
||||
# State alphas used by every interactive surface in the kit (Button,
|
||||
# Toggle, TextField, etc.). Foreground-tinted unless noted.
|
||||
border-width = 1 # idle 1px border on inputs and bordered buttons
|
||||
focus-border-width = 3 # accent ring on Tab focus
|
||||
idle-border-alpha = 0.4 # alpha for the idle 1px foreground border
|
||||
hot-fill-alpha = 0.08 # cursor / hover fill
|
||||
selected-fill-alpha = 0.18 # selected / active / current fill
|
||||
pressed-fill-alpha = 0.22 # button pressed
|
||||
focus-fill-alpha = 0.22 # accent fill behind Tab focus ring
|
||||
|
||||
[font]
|
||||
# base-size is the rem root for the type scale. Every Style.font.<token>
|
||||
# derives from it (e.g. body = base, subtitle ≈ base * 1.083,
|
||||
|
||||
+52
-14
@@ -25,18 +25,48 @@ QtObject {
|
||||
|
||||
property int cornerRadius: 0
|
||||
|
||||
// ---------------------------------------------------------- state alphas
|
||||
//
|
||||
// Foreground-tinted fills + border alphas used by every interactive
|
||||
// surface in the kit. Themes can override these via [style] in
|
||||
// shell.toml; otherwise the kit defaults below apply.
|
||||
property var styleOverrides: ({})
|
||||
|
||||
function styleNum(key, fallback) {
|
||||
var v = styleOverrides[key]
|
||||
var n = Number(v)
|
||||
return isFinite(n) ? n : fallback
|
||||
}
|
||||
|
||||
readonly property int borderWidth: Math.max(0, Math.round(styleNum("border-width", 1)))
|
||||
readonly property int focusBorderWidth: Math.max(1, Math.round(styleNum("focus-border-width", 3)))
|
||||
|
||||
readonly property real idleBorderAlpha: styleNum("idle-border-alpha", 0.4)
|
||||
readonly property real hotFillAlpha: styleNum("hot-fill-alpha", 0.08)
|
||||
readonly property real selectedFillAlpha: styleNum("selected-fill-alpha", 0.18)
|
||||
readonly property real pressedFillAlpha: styleNum("pressed-fill-alpha", 0.22)
|
||||
readonly property real focusFillAlpha: styleNum("focus-fill-alpha", 0.22)
|
||||
|
||||
function alphaForeground(a) {
|
||||
return Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, a)
|
||||
}
|
||||
|
||||
function alphaAccent(a) {
|
||||
return Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, a)
|
||||
}
|
||||
|
||||
// Focus affordances. Deliberately distinct from "selected" (which uses an
|
||||
// accent fill) so the keyboard cursor never reads as the chosen value.
|
||||
// The settings panel originated these tokens; promoting them here means
|
||||
// every Ui component picks them up uniformly.
|
||||
readonly property color focusBorderColor: Color.accent
|
||||
readonly property color focusFillColor: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.22)
|
||||
readonly property int focusBorderWidth: 3
|
||||
readonly property color focusFillColor: alphaAccent(focusFillAlpha)
|
||||
|
||||
// Convenience fills used by panel rows and pills. Hot is hover/keyboard
|
||||
// cursor; selected is the persistent chosen/current item state.
|
||||
readonly property color hotFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.08)
|
||||
readonly property color selectedFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.18)
|
||||
// cursor; selected/active is the persistent chosen/current state.
|
||||
readonly property color hotFill: alphaForeground(hotFillAlpha)
|
||||
readonly property color selectedFill: alphaForeground(selectedFillAlpha)
|
||||
readonly property color pressedFill: alphaForeground(pressedFillAlpha)
|
||||
readonly property color idleBorderColor: alphaForeground(idleBorderAlpha)
|
||||
readonly property color selectedAccentFill: alphaAccent(selectedFillAlpha)
|
||||
|
||||
// ---------------------------------------------------------- typography
|
||||
//
|
||||
@@ -57,7 +87,8 @@ QtObject {
|
||||
property int fontBaseSize: 12
|
||||
|
||||
// Parsed maps populated by loadShell. Keep them as plain dicts so
|
||||
// reassigning the whole property fires reactive bindings.
|
||||
// reassigning the whole property fires reactive bindings. styleOverrides
|
||||
// is declared up top next to the helpers that consume it.
|
||||
property var fontOverrides: ({})
|
||||
property var barOverrides: ({})
|
||||
|
||||
@@ -117,10 +148,12 @@ QtObject {
|
||||
|
||||
// Parse [font] base-size + per-token overrides and [bar] size-* keys
|
||||
// out of shell.toml. Color.qml owns the quoted-string side of the same
|
||||
// file; we only care about the unquoted integer values here.
|
||||
// file; we handle the unquoted numeric tokens for [font], [bar] sizes,
|
||||
// and [style] (alphas + border widths).
|
||||
function loadShell(raw) {
|
||||
var fontOut = {}
|
||||
var barOut = {}
|
||||
var styleOut = {}
|
||||
var nextBase = 12
|
||||
var text = String(raw || "")
|
||||
if (text) {
|
||||
@@ -131,15 +164,19 @@ QtObject {
|
||||
if (!line || line.charAt(0) === "#") continue
|
||||
var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/)
|
||||
if (sectionMatch) { section = sectionMatch[1]; continue }
|
||||
var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+)\s*(#.*)?$/)
|
||||
// Accept ints OR floats (alphas are 0..1).
|
||||
var kv = line.match(/^([A-Za-z0-9_-]+)\s*=\s*(-?\d+(?:\.\d+)?)\s*(#.*)?$/)
|
||||
if (!kv) continue
|
||||
var key = kv[1]
|
||||
var val = parseInt(kv[2], 10)
|
||||
var raw = kv[2]
|
||||
if (section === "font") {
|
||||
if (key === "base-size") nextBase = val
|
||||
else fontOut[key] = val
|
||||
var ival = parseInt(raw, 10)
|
||||
if (key === "base-size") nextBase = ival
|
||||
else fontOut[key] = ival
|
||||
} else if (section === "bar" && (key === "size-horizontal" || key === "size-vertical")) {
|
||||
barOut[key] = val
|
||||
barOut[key] = parseInt(raw, 10)
|
||||
} else if (section === "style") {
|
||||
styleOut[key] = parseFloat(raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,6 +187,7 @@ QtObject {
|
||||
fontBaseSize = nextBase
|
||||
fontOverrides = fontOut
|
||||
barOverrides = barOut
|
||||
styleOverrides = styleOut
|
||||
}
|
||||
|
||||
property Process hyprctlProc: Process {
|
||||
|
||||
@@ -2,60 +2,93 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
|
||||
// The button. One component for every clickable thing in the kit.
|
||||
// States compose independently and are applied in priority order:
|
||||
//
|
||||
// pressed (mouse down) pressed fill
|
||||
// activeFocus (Tab focus) accent ring + accent fill
|
||||
// selected accent fill + accent border
|
||||
// active foreground tint fill (highlighted)
|
||||
// hasCursor || hover hot fill
|
||||
// idle transparent or 1px border if `bordered`
|
||||
//
|
||||
// All fills/borders come from `qs.Commons.Style` tokens, so themes
|
||||
// control the look via [style] in shell.toml.
|
||||
//
|
||||
// Emits `hovered(bool)` so panels with their own keyboard cursor model
|
||||
// can update state on mouse enter/leave.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string iconText: ""
|
||||
property string tooltipText: ""
|
||||
|
||||
// State flags (see comment above for paint priority).
|
||||
property bool selected: false
|
||||
property bool active: false
|
||||
property bool hasCursor: false
|
||||
property bool focusable: false
|
||||
property bool bordered: false
|
||||
|
||||
// Colors. Defaults track the theme; per-instance overrides are honored.
|
||||
property color foreground: Color.foreground
|
||||
property color background: "transparent"
|
||||
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08)
|
||||
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
|
||||
property color tooltipBackground: Color.background
|
||||
property color tooltipForeground: foreground
|
||||
property color accent: Color.accent
|
||||
|
||||
// Sizing.
|
||||
property string fontFamily: Style.font.family
|
||||
property real fontSize: Style.font.body
|
||||
property real iconSize: Style.font.icon
|
||||
property real iconRotation: 0
|
||||
property real horizontalPadding: 10
|
||||
property real verticalPadding: 6
|
||||
property bool active: false
|
||||
property bool leftAlign: false
|
||||
property color activeBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
|
||||
|
||||
// Keyboard cursor flag. When true, the pill renders the same fill + border
|
||||
// as a mouse hover — so j/k navigation and pointer hover are visually
|
||||
// indistinguishable. Default false; bind from a panel's cursor state.
|
||||
property bool hasCursor: false
|
||||
// Tooltip palette. Auto-rendered if tooltipText is set.
|
||||
property color tooltipBackground: Color.background
|
||||
property color tooltipForeground: foreground
|
||||
|
||||
// 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 is a fill only, matching CursorSurface's canonical
|
||||
// chrome and the other panel primitives (Toggle, PanelActionButton). The
|
||||
// accent border ring is reserved for Tab focus on `focusable` buttons.
|
||||
property bool bordered: false
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
signal hovered(bool isHovered)
|
||||
|
||||
activeFocusOnTab: focusable
|
||||
Keys.onReturnPressed: if (focusable) root.clicked()
|
||||
Keys.onEnterPressed: if (focusable) root.clicked()
|
||||
Keys.onSpacePressed: if (focusable) root.clicked()
|
||||
|
||||
implicitWidth: row.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: row.implicitHeight + verticalPadding * 2
|
||||
radius: Style.cornerRadius
|
||||
|
||||
readonly property bool hot: mouseArea.containsMouse || hasCursor
|
||||
readonly property bool _showFocusRing: focusable && activeFocus
|
||||
|
||||
color: mouseArea.pressed ? Style.pressedFill
|
||||
: _showFocusRing ? Style.focusFillColor
|
||||
: selected ? Style.selectedAccentFill
|
||||
: hot ? Style.hotFill
|
||||
: active ? Style.selectedFill
|
||||
: background
|
||||
|
||||
border.color: _showFocusRing ? Style.focusBorderColor
|
||||
: selected ? accent
|
||||
: bordered ? foreground
|
||||
: Style.idleBorderColor
|
||||
|
||||
border.width: _showFocusRing ? Style.focusBorderWidth
|
||||
: selected ? Math.max(Style.borderWidth, 2)
|
||||
: bordered ? Style.borderWidth
|
||||
: 0
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
ToolTip {
|
||||
visible: root.tooltipText !== "" && mouseArea.containsMouse
|
||||
text: root.tooltipText
|
||||
delay: 400
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: root.tooltipBackground
|
||||
border.color: root.tooltipForeground
|
||||
@@ -63,7 +96,6 @@ Rectangle {
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: root.tooltipText
|
||||
color: root.tooltipForeground
|
||||
@@ -76,34 +108,6 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
signal clicked()
|
||||
signal rightClicked()
|
||||
|
||||
implicitWidth: row.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: row.implicitHeight + verticalPadding * 2
|
||||
radius: Style.cornerRadius
|
||||
|
||||
// Hot = mouse hover OR keyboard cursor. Pressed wins over both; otherwise
|
||||
// hot beats `active` (so a cursor on an already-active pill still reads
|
||||
// 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: _showFocusRing ? Style.focusBorderWidth
|
||||
: (bordered ? 1 : 0)
|
||||
border.color: _showFocusRing ? Style.focusBorderColor : foreground
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 120 }
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -115,7 +119,7 @@ Rectangle {
|
||||
Text {
|
||||
visible: root.iconText !== ""
|
||||
text: root.iconText
|
||||
color: root.foreground
|
||||
color: root.selected ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.iconSize
|
||||
rotation: root.iconRotation
|
||||
@@ -126,9 +130,10 @@ Rectangle {
|
||||
Text {
|
||||
visible: root.text !== ""
|
||||
text: root.text
|
||||
color: root.foreground
|
||||
color: root.selected ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.fontSize
|
||||
font.bold: root.selected
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
@@ -145,4 +150,8 @@ Rectangle {
|
||||
else root.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.hovered(hovered)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
// Mutually-exclusive row of Buttons — the form-style "pick one of N"
|
||||
// pattern (bar position top/right/bottom/left, theme preset chips, etc.).
|
||||
// Emits `changed(value)` when the user activates a different option.
|
||||
//
|
||||
// `options` is either a plain string[] (label == value) or an array of
|
||||
// { value, label, icon?, tooltip? } objects. Mixing is fine.
|
||||
//
|
||||
// For panel-cursor-driven selection (where j/k walks a row), use bare
|
||||
// `Button { hasCursor: ... }` instances in a Row — ButtonGroup is the
|
||||
// convenience for non-cursor form contexts where you just need
|
||||
// "selected: value === optionValue" wiring.
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property var options: []
|
||||
property string value: ""
|
||||
property color foreground: Color.foreground
|
||||
property color background: Color.background
|
||||
property color accent: Color.accent
|
||||
property string fontFamily: Style.font.family
|
||||
property real fontSize: Style.font.body
|
||||
property bool focusable: false
|
||||
|
||||
signal changed(string value)
|
||||
|
||||
spacing: 6
|
||||
|
||||
function optionValue(o) {
|
||||
return (o && typeof o === "object") ? String(o.value) : String(o)
|
||||
}
|
||||
function optionLabel(o) {
|
||||
return (o && typeof o === "object" && o.label !== undefined) ? String(o.label) : String(o)
|
||||
}
|
||||
function optionIcon(o) {
|
||||
return (o && typeof o === "object" && o.icon) ? String(o.icon) : ""
|
||||
}
|
||||
function optionTooltip(o) {
|
||||
return (o && typeof o === "object" && o.tooltip) ? String(o.tooltip) : ""
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
|
||||
delegate: Button {
|
||||
required property var modelData
|
||||
text: root.optionLabel(modelData)
|
||||
iconText: root.optionIcon(modelData)
|
||||
tooltipText: root.optionTooltip(modelData)
|
||||
selected: root.optionValue(modelData) === root.value
|
||||
foreground: root.foreground
|
||||
background: root.background
|
||||
accent: root.accent
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: root.fontSize
|
||||
focusable: root.focusable
|
||||
onClicked: root.changed(root.optionValue(modelData))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
|
||||
// A single button in a mutually-exclusive choice group (a Row of these
|
||||
// makes a "segmented control"). Distinct from PillButton because it has
|
||||
// a real `selected` state semantic — used for picking between options
|
||||
// (bar position: top/bottom/left/right), not for momentary actions.
|
||||
//
|
||||
// Selected styling uses the accent fill+border; focus styling uses the
|
||||
// Style.focusBorderColor outline so keyboard nav can land on a non-selected
|
||||
// option without it reading as the chosen one. This separation matters in
|
||||
// the settings panel and any future "pick one" UI.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property bool selected: false
|
||||
|
||||
// Panel-cursor flag. Same role as PillButton.hasCursor: panels that own
|
||||
// their own cursor state bind this to drive the keyboard highlight
|
||||
// separately from real activeFocus. Cursor renders as a fill only —
|
||||
// CursorSurface is the canonical chrome — while Tab focus adds the
|
||||
// accent border ring on top.
|
||||
property bool hasCursor: false
|
||||
property bool borderlessHighlight: false
|
||||
|
||||
property color foreground: Color.foreground
|
||||
property color background: Color.background
|
||||
property color accent: Color.accent
|
||||
property string fontFamily: Style.font.family
|
||||
property real fontSize: Style.font.body
|
||||
|
||||
signal clicked()
|
||||
signal hovered(bool isHovered)
|
||||
|
||||
activeFocusOnTab: true
|
||||
Keys.onReturnPressed: root.clicked()
|
||||
Keys.onEnterPressed: root.clicked()
|
||||
Keys.onSpacePressed: root.clicked()
|
||||
|
||||
implicitWidth: Math.max(56, label.implicitWidth + 22)
|
||||
implicitHeight: 28
|
||||
radius: Style.cornerRadius
|
||||
|
||||
color: selected
|
||||
? Qt.rgba(accent.r, accent.g, accent.b, 0.18)
|
||||
: ((mouse.containsMouse || hasCursor) ? Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08) : background)
|
||||
border.color: selected
|
||||
? accent
|
||||
: (activeFocus ? foreground : Qt.rgba(foreground.r, foreground.g, foreground.b, 0.4))
|
||||
border.width: borderlessHighlight ? (activeFocus ? 2 : 0)
|
||||
: (selected ? 2 : (activeFocus ? 2 : 1))
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: root.text
|
||||
color: root.selected ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: root.fontSize
|
||||
font.bold: root.selected
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.forceActiveFocus()
|
||||
root.clicked()
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.hovered(hovered)
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
// PillButton that participates in a panel's single-cursor model. Use
|
||||
// inside a row of pills (DNS providers, bluetooth header actions, choice
|
||||
// chips) where mouse hover and keyboard cursor should land in the same
|
||||
// place.
|
||||
//
|
||||
// Caller binds `hasCursor` to the panel's cursor state and listens to
|
||||
// `hovered(bool)` to update that state when the mouse enters or leaves.
|
||||
// This is structurally a wrapper around PillButton with one extra
|
||||
// HoverHandler — but extracting it lets every panel use the same wiring
|
||||
// idiom and lets plugin authors drop into the same cursor model without
|
||||
// touching internals.
|
||||
//
|
||||
// Why HoverHandler instead of a MouseArea overlay: HoverHandler doesn't
|
||||
// steal pointer events from PillButton's internal click MouseArea, so
|
||||
// clicks still reach the underlying button. An overlay MouseArea with
|
||||
// acceptedButtons: Qt.NoButton works but is fragile around tooltip
|
||||
// timing and event propagation.
|
||||
PillButton {
|
||||
id: root
|
||||
|
||||
signal hovered(bool isHovered)
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: root.hovered(hovered)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import qs.Commons
|
||||
// Defaults bind to qs.Commons.Color so a caller with no theme overrides
|
||||
// just works; foreground / accent / selectionTint can be overridden per
|
||||
// instance. Focus styling uses Style.focusBorderColor (the same accent
|
||||
// ring Toggle and ChoiceButton paint) so keyboard cursor and form-focus
|
||||
// ring Toggle and Button paint) so keyboard cursor and form focus
|
||||
// chrome stay consistent across the shell.
|
||||
//
|
||||
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ Rectangle {
|
||||
property string description: ""
|
||||
property bool checked: false
|
||||
|
||||
// Panel-cursor flag. Same role as PillButton.hasCursor / ChoiceButton.hasCursor:
|
||||
// Panel-cursor flag. Same role as Button.hasCursor:
|
||||
// panels with their own keyboard cursor bind this to drive the highlight
|
||||
// separately from activeFocus. Renders as a tinted fill only — the accent
|
||||
// border ring is reserved for Tab focus (activeFocus).
|
||||
|
||||
+2
-3
@@ -1,7 +1,7 @@
|
||||
module qs.Ui
|
||||
|
||||
ChoiceButton 1.0 ChoiceButton.qml
|
||||
CursorPill 1.0 CursorPill.qml
|
||||
Button 1.0 Button.qml
|
||||
ButtonGroup 1.0 ButtonGroup.qml
|
||||
CursorSurface 1.0 CursorSurface.qml
|
||||
Dropdown 1.0 Dropdown.qml
|
||||
KeyboardPanel 1.0 KeyboardPanel.qml
|
||||
@@ -12,7 +12,6 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
|
||||
PanelSeparator 1.0 PanelSeparator.qml
|
||||
PanelSlider 1.0 PanelSlider.qml
|
||||
PanelToolTip 1.0 PanelToolTip.qml
|
||||
PillButton 1.0 PillButton.qml
|
||||
PopupCard 1.0 PopupCard.qml
|
||||
SearchableDropdown 1.0 SearchableDropdown.qml
|
||||
TextField 1.0 TextField.qml
|
||||
|
||||
@@ -1551,7 +1551,7 @@ Item {
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
id: rowPinBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
@@ -1565,7 +1565,7 @@ Item {
|
||||
onClicked: trayRoot.togglePin(rowRoot.itemId)
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
id: rowHideBtn
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: rowPinBtn.left
|
||||
|
||||
@@ -512,11 +512,11 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Header pill: a CursorPill bound into the panel's "header" cursor
|
||||
// section. CursorPill collapses what used to be a PillButton subclass +
|
||||
// Header pill: a Button bound into the panel's "header" cursor
|
||||
// section. Button collapses what used to be a Button subclass +
|
||||
// overlay MouseArea into one component; we keep the pillIndex / activated
|
||||
// shim here so the three header pill instantiations stay readable.
|
||||
component HeaderPill: CursorPill {
|
||||
component HeaderPill: Button {
|
||||
id: pill
|
||||
required property int pillIndex
|
||||
property bool pillEnabled: true
|
||||
|
||||
@@ -95,7 +95,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: 28
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
id: prevButton
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -115,7 +115,7 @@ Item {
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
id: nextButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -198,7 +198,7 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 6
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
@@ -208,7 +208,7 @@ Item {
|
||||
onClicked: if (root.activePlayer) root.activePlayer.previous()
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
iconText: root.activePlayer && root.activePlayer.isPlaying ? "" : ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 14
|
||||
@@ -219,7 +219,7 @@ Item {
|
||||
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
iconText: ""
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
|
||||
@@ -30,7 +30,7 @@ Item {
|
||||
// "brightness" - single slider row, selectedIndex = -1 sentinel
|
||||
// (mirrors audioPanel's slider rows). Only present if a
|
||||
// controllable backlight was detected.
|
||||
// "scale" - 6 ChoiceButton scale presets; treated as a single
|
||||
// "scale" - 6 Button scale presets; treated as a single
|
||||
// horizontal row from j/k's perspective. h/l moves
|
||||
// between presets, identical to bluetooth's header.
|
||||
// "monitors" - vertical Toggle list for enabling/disabling displays;
|
||||
@@ -499,7 +499,7 @@ Item {
|
||||
Repeater {
|
||||
model: root.scaleValues
|
||||
|
||||
CursorPill {
|
||||
Button {
|
||||
required property string modelData
|
||||
required property int index
|
||||
|
||||
|
||||
@@ -697,7 +697,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
id: refreshBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -915,7 +915,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
// One DNS provider pill. The cursor + current visuals come entirely from
|
||||
// CursorSurface; this component just binds them to the panel's cursor
|
||||
// state and renders the label/tooltip/click target.
|
||||
component DnsProviderPill: CursorPill {
|
||||
component DnsProviderPill: Button {
|
||||
id: pill
|
||||
required property string provider
|
||||
required property int index
|
||||
@@ -928,7 +928,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 6
|
||||
|
||||
// Map the panel's domain semantics onto CursorPill's structural props:
|
||||
// Map the panel's domain semantics onto Button's structural props:
|
||||
// `current DNS` is the pill's `active` fill; the keyboard cursor lights
|
||||
// up `hasCursor`.
|
||||
active: root.dnsProvider === provider
|
||||
|
||||
@@ -95,20 +95,19 @@ Item {
|
||||
property int numberDemoValue: 15
|
||||
|
||||
readonly property var visibleSections: [
|
||||
"cursor-surface", "pill-button", "cursor-pill", "panel-action-button",
|
||||
"panel-tool-tip", "slider", "choice-button", "text-field", "number-field",
|
||||
"cursor-surface", "button", "button-group", "panel-action-button",
|
||||
"panel-tool-tip", "slider", "text-field", "number-field",
|
||||
"toggle", "dropdown", "searchable-dropdown", "composed"
|
||||
]
|
||||
|
||||
function sectionCount(section) {
|
||||
switch (section) {
|
||||
case "cursor-surface": return 3
|
||||
case "pill-button": return 5
|
||||
case "cursor-pill": return 4
|
||||
case "button": return 5
|
||||
case "button-group": return 4
|
||||
case "panel-action-button": return 4
|
||||
case "panel-tool-tip": return 1
|
||||
case "slider": return 1
|
||||
case "choice-button": return 4
|
||||
case "text-field": return 2
|
||||
case "number-field": return 1
|
||||
case "toggle": return 2
|
||||
@@ -120,13 +119,11 @@ Item {
|
||||
}
|
||||
|
||||
// True for sections whose primitives lay out horizontally (a row of
|
||||
// pills, choice buttons, etc.) — j/k jumps to the next/prev section,
|
||||
// h/l walks within the row.
|
||||
// buttons) — j/k jumps to the next/prev section, h/l walks within the row.
|
||||
function sectionIsHorizontal(section) {
|
||||
return section === "pill-button"
|
||||
|| section === "cursor-pill"
|
||||
return section === "button"
|
||||
|| section === "button-group"
|
||||
|| section === "panel-action-button"
|
||||
|| section === "choice-button"
|
||||
}
|
||||
|
||||
// True for sections where h/l should adjust a value rather than walk.
|
||||
@@ -191,7 +188,7 @@ Item {
|
||||
}
|
||||
|
||||
function activateCursor() {
|
||||
if (focusSection === "choice-button") {
|
||||
if (focusSection === "button-group") {
|
||||
var opts = ["top", "right", "bottom", "left"]
|
||||
if (selectedIndex >= 0 && selectedIndex < opts.length)
|
||||
root.choiceDemoValue = opts[selectedIndex]
|
||||
@@ -802,20 +799,20 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PillButton --------------------------------------------------
|
||||
// ---- Button ------------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PillButton"
|
||||
text: "Button"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Compact rounded button with optional icon, label, tooltip, and `active` highlight. Used inside panels for inline actions (Refresh, DNS pills, Bluetooth header)."
|
||||
text: "The kit's only button. State flags compose: hasCursor (panel cursor / hover) paints a tinted fill; active adds a persistent highlight; bordered draws a 1px idle ring for primary form buttons; focusable enables Tab focus with the accent ring. Click below or press h/l to walk the demo cursor."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
@@ -825,100 +822,92 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: pillCol.implicitHeight + 24
|
||||
implicitHeight: buttonRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: pillCol
|
||||
id: buttonRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
spacing: 6
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
text: "DHCP"
|
||||
tooltipText: "Use DNS from DHCP"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 0
|
||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "button"; root.selectedIndex = 0 }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 0 } }
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
text: "Cloudflare"
|
||||
tooltipText: "Set DNS to Cloudflare"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
active: true
|
||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 1
|
||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 1
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "button"; root.selectedIndex = 1 }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 1 } }
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
iconText: ""
|
||||
tooltipText: "Refresh"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 2
|
||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 2
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "button"; root.selectedIndex = 2 }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 2 } }
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
iconText: ""
|
||||
text: "On"
|
||||
tooltipText: "Turn Bluetooth off"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
active: true
|
||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 3
|
||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 3
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "button"; root.selectedIndex = 3 }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 3 } }
|
||||
}
|
||||
|
||||
PillButton {
|
||||
Button {
|
||||
text: "Apply"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
focusable: true
|
||||
bordered: true
|
||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 4
|
||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 4
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "button"; root.selectedIndex = 4 }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 4 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CursorPill --------------------------------------------------
|
||||
// ---- ButtonGroup -------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "CursorPill"
|
||||
text: "ButtonGroup"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "PillButton with panel-cursor wiring. Bind hasCursor to your cursor state and onHovered to update it on mouse enter; clicks come from PillButton's clicked() signal. Use this for any \"pick one in a row\" UI (wifi DNS pills, bluetooth header actions). Click below or press h/l to walk the demo cursor."
|
||||
text: "Mutually-exclusive row of Buttons. Selected option paints the accent fill+border so the chosen value stands out from non-selected options the cursor may pass through. Click to pick or press h/l + Enter."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
@@ -928,38 +917,22 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: cpRow.implicitHeight + 24
|
||||
implicitHeight: choiceRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: cpRow
|
||||
ButtonGroup {
|
||||
id: choiceRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||
CursorPill {
|
||||
required property string modelData
|
||||
required property int index
|
||||
text: modelData
|
||||
foreground: root.foreground
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
tooltipText: "Pick " + modelData
|
||||
hasCursor: root.focusSection === "cursor-pill" && root.selectedIndex === index
|
||||
active: modelData === "Cloudflare"
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "cursor-pill"; root.selectedIndex = index }
|
||||
}
|
||||
onClicked: { root.focusSection = "cursor-pill"; root.selectedIndex = index }
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
}
|
||||
options: ["top", "right", "bottom", "left"]
|
||||
value: root.choiceDemoValue
|
||||
onChanged: function(v) {
|
||||
root.focusSection = "button-group"
|
||||
root.choiceDemoValue = v
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1210,69 +1183,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- ChoiceButton --------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "ChoiceButton"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.subtitle
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "A single button in a mutually-exclusive choice group. Selected styling uses the accent fill+border; focus styling uses the Style.focusBorderColor outline so keyboard nav can land on a non-selected option without it reading as the chosen one."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: choiceRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: choiceRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: ["top", "right", "bottom", "left"]
|
||||
ChoiceButton {
|
||||
required property string modelData
|
||||
required property int index
|
||||
text: modelData
|
||||
foreground: root.foreground
|
||||
background: root.background
|
||||
accent: root.accent
|
||||
fontFamily: root.fontFamily
|
||||
selected: root.choiceDemoValue === modelData
|
||||
hasCursor: root.focusSection === "choice-button" && root.selectedIndex === index
|
||||
onClicked: {
|
||||
root.focusSection = "choice-button"
|
||||
root.selectedIndex = index
|
||||
root.choiceDemoValue = modelData
|
||||
}
|
||||
onHovered: function(h) {
|
||||
if (h) { root.focusSection = "choice-button"; root.selectedIndex = index }
|
||||
}
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TextField -----------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
@@ -1421,7 +1331,7 @@ Item {
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. Same focus tokens as ChoiceButton."
|
||||
text: "Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. Same focus tokens as Button."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
|
||||
@@ -680,14 +680,14 @@ Item {
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
spacing: 8
|
||||
PillButton {
|
||||
Button {
|
||||
text: "Cancel"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
focusable: true
|
||||
onClicked: root.discardWidgetSettings()
|
||||
}
|
||||
PillButton {
|
||||
Button {
|
||||
text: "Apply"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
@@ -788,7 +788,7 @@ Item {
|
||||
|
||||
Row {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
PillButton {
|
||||
Button {
|
||||
text: "Reset bar to defaults"
|
||||
foreground: root.urgent
|
||||
fontFamily: root.fontFamily
|
||||
@@ -829,7 +829,7 @@ Item {
|
||||
Repeater {
|
||||
model: positionGroup.positions
|
||||
|
||||
delegate: ChoiceButton {
|
||||
delegate: Button {
|
||||
required property string modelData
|
||||
|
||||
text: modelData
|
||||
|
||||
Reference in New Issue
Block a user