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-horizontal = 26
|
||||||
size-vertical = 28
|
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]
|
[font]
|
||||||
# base-size is the rem root for the type scale. Every Style.font.<token>
|
# 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,
|
# derives from it (e.g. body = base, subtitle ≈ base * 1.083,
|
||||||
|
|||||||
+52
-14
@@ -25,18 +25,48 @@ QtObject {
|
|||||||
|
|
||||||
property int cornerRadius: 0
|
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
|
// Focus affordances. Deliberately distinct from "selected" (which uses an
|
||||||
// accent fill) so the keyboard cursor never reads as the chosen value.
|
// 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 focusBorderColor: Color.accent
|
||||||
readonly property color focusFillColor: Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.22)
|
readonly property color focusFillColor: alphaAccent(focusFillAlpha)
|
||||||
readonly property int focusBorderWidth: 3
|
|
||||||
|
|
||||||
// Convenience fills used by panel rows and pills. Hot is hover/keyboard
|
// Convenience fills used by panel rows and pills. Hot is hover/keyboard
|
||||||
// cursor; selected is the persistent chosen/current item state.
|
// cursor; selected/active is the persistent chosen/current state.
|
||||||
readonly property color hotFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.08)
|
readonly property color hotFill: alphaForeground(hotFillAlpha)
|
||||||
readonly property color selectedFill: Qt.rgba(Color.foreground.r, Color.foreground.g, Color.foreground.b, 0.18)
|
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
|
// ---------------------------------------------------------- typography
|
||||||
//
|
//
|
||||||
@@ -57,7 +87,8 @@ QtObject {
|
|||||||
property int fontBaseSize: 12
|
property int fontBaseSize: 12
|
||||||
|
|
||||||
// Parsed maps populated by loadShell. Keep them as plain dicts so
|
// 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 fontOverrides: ({})
|
||||||
property var barOverrides: ({})
|
property var barOverrides: ({})
|
||||||
|
|
||||||
@@ -117,10 +148,12 @@ QtObject {
|
|||||||
|
|
||||||
// Parse [font] base-size + per-token overrides and [bar] size-* keys
|
// 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
|
// 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) {
|
function loadShell(raw) {
|
||||||
var fontOut = {}
|
var fontOut = {}
|
||||||
var barOut = {}
|
var barOut = {}
|
||||||
|
var styleOut = {}
|
||||||
var nextBase = 12
|
var nextBase = 12
|
||||||
var text = String(raw || "")
|
var text = String(raw || "")
|
||||||
if (text) {
|
if (text) {
|
||||||
@@ -131,15 +164,19 @@ QtObject {
|
|||||||
if (!line || line.charAt(0) === "#") continue
|
if (!line || line.charAt(0) === "#") continue
|
||||||
var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/)
|
var sectionMatch = line.match(/^\[([A-Za-z0-9_-]+)\]\s*(#.*)?$/)
|
||||||
if (sectionMatch) { section = sectionMatch[1]; continue }
|
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
|
if (!kv) continue
|
||||||
var key = kv[1]
|
var key = kv[1]
|
||||||
var val = parseInt(kv[2], 10)
|
var raw = kv[2]
|
||||||
if (section === "font") {
|
if (section === "font") {
|
||||||
if (key === "base-size") nextBase = val
|
var ival = parseInt(raw, 10)
|
||||||
else fontOut[key] = val
|
if (key === "base-size") nextBase = ival
|
||||||
|
else fontOut[key] = ival
|
||||||
} else if (section === "bar" && (key === "size-horizontal" || key === "size-vertical")) {
|
} 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
|
fontBaseSize = nextBase
|
||||||
fontOverrides = fontOut
|
fontOverrides = fontOut
|
||||||
barOverrides = barOut
|
barOverrides = barOut
|
||||||
|
styleOverrides = styleOut
|
||||||
}
|
}
|
||||||
|
|
||||||
property Process hyprctlProc: Process {
|
property Process hyprctlProc: Process {
|
||||||
|
|||||||
@@ -2,60 +2,93 @@ import QtQuick
|
|||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import qs.Commons
|
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 {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property string text: ""
|
property string text: ""
|
||||||
property string iconText: ""
|
property string iconText: ""
|
||||||
property string tooltipText: ""
|
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 foreground: Color.foreground
|
||||||
property color background: "transparent"
|
property color background: "transparent"
|
||||||
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.08)
|
property color accent: Color.accent
|
||||||
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
|
|
||||||
property color tooltipBackground: Color.background
|
// Sizing.
|
||||||
property color tooltipForeground: foreground
|
|
||||||
property string fontFamily: Style.font.family
|
property string fontFamily: Style.font.family
|
||||||
property real fontSize: Style.font.body
|
property real fontSize: Style.font.body
|
||||||
property real iconSize: Style.font.icon
|
property real iconSize: Style.font.icon
|
||||||
property real iconRotation: 0
|
property real iconRotation: 0
|
||||||
property real horizontalPadding: 10
|
property real horizontalPadding: 10
|
||||||
property real verticalPadding: 6
|
property real verticalPadding: 6
|
||||||
property bool active: false
|
|
||||||
property bool leftAlign: 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
|
// Tooltip palette. Auto-rendered if tooltipText is set.
|
||||||
// as a mouse hover — so j/k navigation and pointer hover are visually
|
property color tooltipBackground: Color.background
|
||||||
// indistinguishable. Default false; bind from a panel's cursor state.
|
property color tooltipForeground: foreground
|
||||||
property bool hasCursor: false
|
|
||||||
|
|
||||||
// Tab-focusable form-button mode. Enables activeFocusOnTab and
|
signal clicked()
|
||||||
// Enter/Return/Space activation, and uses Style.focusBorderColor / FillColor
|
signal rightClicked()
|
||||||
// for the focus ring (distinct from the panel-cursor `hot` state). Set
|
signal hovered(bool isHovered)
|
||||||
// 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
|
|
||||||
|
|
||||||
activeFocusOnTab: focusable
|
activeFocusOnTab: focusable
|
||||||
Keys.onReturnPressed: if (focusable) root.clicked()
|
Keys.onReturnPressed: if (focusable) root.clicked()
|
||||||
Keys.onEnterPressed: if (focusable) root.clicked()
|
Keys.onEnterPressed: if (focusable) root.clicked()
|
||||||
Keys.onSpacePressed: 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 {
|
ToolTip {
|
||||||
visible: root.tooltipText !== "" && mouseArea.containsMouse
|
visible: root.tooltipText !== "" && mouseArea.containsMouse
|
||||||
text: root.tooltipText
|
text: root.tooltipText
|
||||||
delay: 400
|
delay: 400
|
||||||
padding: 0
|
padding: 0
|
||||||
|
|
||||||
background: Rectangle {
|
background: Rectangle {
|
||||||
color: root.tooltipBackground
|
color: root.tooltipBackground
|
||||||
border.color: root.tooltipForeground
|
border.color: root.tooltipForeground
|
||||||
@@ -63,7 +96,6 @@ Rectangle {
|
|||||||
radius: 0
|
radius: 0
|
||||||
opacity: 0.97
|
opacity: 0.97
|
||||||
}
|
}
|
||||||
|
|
||||||
contentItem: Text {
|
contentItem: Text {
|
||||||
text: root.tooltipText
|
text: root.tooltipText
|
||||||
color: root.tooltipForeground
|
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 {
|
Row {
|
||||||
id: row
|
id: row
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -115,7 +119,7 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
visible: root.iconText !== ""
|
visible: root.iconText !== ""
|
||||||
text: root.iconText
|
text: root.iconText
|
||||||
color: root.foreground
|
color: root.selected ? root.accent : root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: root.iconSize
|
font.pixelSize: root.iconSize
|
||||||
rotation: root.iconRotation
|
rotation: root.iconRotation
|
||||||
@@ -126,9 +130,10 @@ Rectangle {
|
|||||||
Text {
|
Text {
|
||||||
visible: root.text !== ""
|
visible: root.text !== ""
|
||||||
text: root.text
|
text: root.text
|
||||||
color: root.foreground
|
color: root.selected ? root.accent : root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: root.fontSize
|
font.pixelSize: root.fontSize
|
||||||
|
font.bold: root.selected
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,4 +150,8 @@ Rectangle {
|
|||||||
else root.clicked()
|
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
|
// Defaults bind to qs.Commons.Color so a caller with no theme overrides
|
||||||
// just works; foreground / accent / selectionTint can be overridden per
|
// just works; foreground / accent / selectionTint can be overridden per
|
||||||
// instance. Focus styling uses Style.focusBorderColor (the same accent
|
// 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.
|
// chrome stay consistent across the shell.
|
||||||
//
|
//
|
||||||
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
|
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ Rectangle {
|
|||||||
property string description: ""
|
property string description: ""
|
||||||
property bool checked: false
|
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
|
// panels with their own keyboard cursor bind this to drive the highlight
|
||||||
// separately from activeFocus. Renders as a tinted fill only — the accent
|
// separately from activeFocus. Renders as a tinted fill only — the accent
|
||||||
// border ring is reserved for Tab focus (activeFocus).
|
// border ring is reserved for Tab focus (activeFocus).
|
||||||
|
|||||||
+2
-3
@@ -1,7 +1,7 @@
|
|||||||
module qs.Ui
|
module qs.Ui
|
||||||
|
|
||||||
ChoiceButton 1.0 ChoiceButton.qml
|
Button 1.0 Button.qml
|
||||||
CursorPill 1.0 CursorPill.qml
|
ButtonGroup 1.0 ButtonGroup.qml
|
||||||
CursorSurface 1.0 CursorSurface.qml
|
CursorSurface 1.0 CursorSurface.qml
|
||||||
Dropdown 1.0 Dropdown.qml
|
Dropdown 1.0 Dropdown.qml
|
||||||
KeyboardPanel 1.0 KeyboardPanel.qml
|
KeyboardPanel 1.0 KeyboardPanel.qml
|
||||||
@@ -12,7 +12,6 @@ PanelSectionHeader 1.0 PanelSectionHeader.qml
|
|||||||
PanelSeparator 1.0 PanelSeparator.qml
|
PanelSeparator 1.0 PanelSeparator.qml
|
||||||
PanelSlider 1.0 PanelSlider.qml
|
PanelSlider 1.0 PanelSlider.qml
|
||||||
PanelToolTip 1.0 PanelToolTip.qml
|
PanelToolTip 1.0 PanelToolTip.qml
|
||||||
PillButton 1.0 PillButton.qml
|
|
||||||
PopupCard 1.0 PopupCard.qml
|
PopupCard 1.0 PopupCard.qml
|
||||||
SearchableDropdown 1.0 SearchableDropdown.qml
|
SearchableDropdown 1.0 SearchableDropdown.qml
|
||||||
TextField 1.0 TextField.qml
|
TextField 1.0 TextField.qml
|
||||||
|
|||||||
@@ -1551,7 +1551,7 @@ Item {
|
|||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
id: rowPinBtn
|
id: rowPinBtn
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
@@ -1565,7 +1565,7 @@ Item {
|
|||||||
onClicked: trayRoot.togglePin(rowRoot.itemId)
|
onClicked: trayRoot.togglePin(rowRoot.itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
id: rowHideBtn
|
id: rowHideBtn
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.right: rowPinBtn.left
|
anchors.right: rowPinBtn.left
|
||||||
|
|||||||
@@ -512,11 +512,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Header pill: a CursorPill bound into the panel's "header" cursor
|
// Header pill: a Button bound into the panel's "header" cursor
|
||||||
// section. CursorPill collapses what used to be a PillButton subclass +
|
// section. Button collapses what used to be a Button subclass +
|
||||||
// overlay MouseArea into one component; we keep the pillIndex / activated
|
// overlay MouseArea into one component; we keep the pillIndex / activated
|
||||||
// shim here so the three header pill instantiations stay readable.
|
// shim here so the three header pill instantiations stay readable.
|
||||||
component HeaderPill: CursorPill {
|
component HeaderPill: Button {
|
||||||
id: pill
|
id: pill
|
||||||
required property int pillIndex
|
required property int pillIndex
|
||||||
property bool pillEnabled: true
|
property bool pillEnabled: true
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ Item {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
implicitHeight: 28
|
implicitHeight: 28
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
id: prevButton
|
id: prevButton
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
@@ -115,7 +115,7 @@ Item {
|
|||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
id: nextButton
|
id: nextButton
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
iconText: ""
|
iconText: ""
|
||||||
foreground: root.bar.foreground
|
foreground: root.bar.foreground
|
||||||
horizontalPadding: 10
|
horizontalPadding: 10
|
||||||
@@ -208,7 +208,7 @@ Item {
|
|||||||
onClicked: if (root.activePlayer) root.activePlayer.previous()
|
onClicked: if (root.activePlayer) root.activePlayer.previous()
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
iconText: root.activePlayer && root.activePlayer.isPlaying ? "" : ""
|
iconText: root.activePlayer && root.activePlayer.isPlaying ? "" : ""
|
||||||
foreground: root.bar.foreground
|
foreground: root.bar.foreground
|
||||||
horizontalPadding: 14
|
horizontalPadding: 14
|
||||||
@@ -219,7 +219,7 @@ Item {
|
|||||||
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
|
onClicked: if (root.activePlayer) root.activePlayer.togglePlaying()
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
iconText: ""
|
iconText: ""
|
||||||
foreground: root.bar.foreground
|
foreground: root.bar.foreground
|
||||||
horizontalPadding: 10
|
horizontalPadding: 10
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Item {
|
|||||||
// "brightness" - single slider row, selectedIndex = -1 sentinel
|
// "brightness" - single slider row, selectedIndex = -1 sentinel
|
||||||
// (mirrors audioPanel's slider rows). Only present if a
|
// (mirrors audioPanel's slider rows). Only present if a
|
||||||
// controllable backlight was detected.
|
// 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
|
// horizontal row from j/k's perspective. h/l moves
|
||||||
// between presets, identical to bluetooth's header.
|
// between presets, identical to bluetooth's header.
|
||||||
// "monitors" - vertical Toggle list for enabling/disabling displays;
|
// "monitors" - vertical Toggle list for enabling/disabling displays;
|
||||||
@@ -499,7 +499,7 @@ Item {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: root.scaleValues
|
model: root.scaleValues
|
||||||
|
|
||||||
CursorPill {
|
Button {
|
||||||
required property string modelData
|
required property string modelData
|
||||||
required property int index
|
required property int index
|
||||||
|
|
||||||
|
|||||||
@@ -697,7 +697,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
id: refreshBtn
|
id: refreshBtn
|
||||||
anchors.right: parent.right
|
anchors.right: parent.right
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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
|
// One DNS provider pill. The cursor + current visuals come entirely from
|
||||||
// CursorSurface; this component just binds them to the panel's cursor
|
// CursorSurface; this component just binds them to the panel's cursor
|
||||||
// state and renders the label/tooltip/click target.
|
// state and renders the label/tooltip/click target.
|
||||||
component DnsProviderPill: CursorPill {
|
component DnsProviderPill: Button {
|
||||||
id: pill
|
id: pill
|
||||||
required property string provider
|
required property string provider
|
||||||
required property int index
|
required property int index
|
||||||
@@ -928,7 +928,7 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
|||||||
horizontalPadding: 10
|
horizontalPadding: 10
|
||||||
verticalPadding: 6
|
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
|
// `current DNS` is the pill's `active` fill; the keyboard cursor lights
|
||||||
// up `hasCursor`.
|
// up `hasCursor`.
|
||||||
active: root.dnsProvider === provider
|
active: root.dnsProvider === provider
|
||||||
|
|||||||
@@ -95,20 +95,19 @@ Item {
|
|||||||
property int numberDemoValue: 15
|
property int numberDemoValue: 15
|
||||||
|
|
||||||
readonly property var visibleSections: [
|
readonly property var visibleSections: [
|
||||||
"cursor-surface", "pill-button", "cursor-pill", "panel-action-button",
|
"cursor-surface", "button", "button-group", "panel-action-button",
|
||||||
"panel-tool-tip", "slider", "choice-button", "text-field", "number-field",
|
"panel-tool-tip", "slider", "text-field", "number-field",
|
||||||
"toggle", "dropdown", "searchable-dropdown", "composed"
|
"toggle", "dropdown", "searchable-dropdown", "composed"
|
||||||
]
|
]
|
||||||
|
|
||||||
function sectionCount(section) {
|
function sectionCount(section) {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
case "cursor-surface": return 3
|
case "cursor-surface": return 3
|
||||||
case "pill-button": return 5
|
case "button": return 5
|
||||||
case "cursor-pill": return 4
|
case "button-group": return 4
|
||||||
case "panel-action-button": return 4
|
case "panel-action-button": return 4
|
||||||
case "panel-tool-tip": return 1
|
case "panel-tool-tip": return 1
|
||||||
case "slider": return 1
|
case "slider": return 1
|
||||||
case "choice-button": return 4
|
|
||||||
case "text-field": return 2
|
case "text-field": return 2
|
||||||
case "number-field": return 1
|
case "number-field": return 1
|
||||||
case "toggle": return 2
|
case "toggle": return 2
|
||||||
@@ -120,13 +119,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// True for sections whose primitives lay out horizontally (a row of
|
// True for sections whose primitives lay out horizontally (a row of
|
||||||
// pills, choice buttons, etc.) — j/k jumps to the next/prev section,
|
// buttons) — j/k jumps to the next/prev section, h/l walks within the row.
|
||||||
// h/l walks within the row.
|
|
||||||
function sectionIsHorizontal(section) {
|
function sectionIsHorizontal(section) {
|
||||||
return section === "pill-button"
|
return section === "button"
|
||||||
|| section === "cursor-pill"
|
|| section === "button-group"
|
||||||
|| section === "panel-action-button"
|
|| section === "panel-action-button"
|
||||||
|| section === "choice-button"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// True for sections where h/l should adjust a value rather than walk.
|
// True for sections where h/l should adjust a value rather than walk.
|
||||||
@@ -191,7 +188,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function activateCursor() {
|
function activateCursor() {
|
||||||
if (focusSection === "choice-button") {
|
if (focusSection === "button-group") {
|
||||||
var opts = ["top", "right", "bottom", "left"]
|
var opts = ["top", "right", "bottom", "left"]
|
||||||
if (selectedIndex >= 0 && selectedIndex < opts.length)
|
if (selectedIndex >= 0 && selectedIndex < opts.length)
|
||||||
root.choiceDemoValue = opts[selectedIndex]
|
root.choiceDemoValue = opts[selectedIndex]
|
||||||
@@ -802,20 +799,20 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- PillButton --------------------------------------------------
|
// ---- Button ------------------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "PillButton"
|
text: "Button"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.subtitle
|
font.pixelSize: Style.font.subtitle
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
Text {
|
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)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
@@ -825,100 +822,92 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
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)
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||||
radius: Style.cornerRadius
|
radius: Style.cornerRadius
|
||||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: pillCol
|
id: buttonRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: 14
|
anchors.leftMargin: 14
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
text: "DHCP"
|
text: "DHCP"
|
||||||
tooltipText: "Use DNS from DHCP"
|
tooltipText: "Use DNS from DHCP"
|
||||||
tooltipBackground: root.background
|
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
|
||||||
tooltipForeground: root.foreground
|
onHovered: function(h) {
|
||||||
foreground: root.foreground
|
if (h) { root.focusSection = "button"; root.selectedIndex = 0 }
|
||||||
fontFamily: root.fontFamily
|
}
|
||||||
hasCursor: root.focusSection === "pill-button" && root.selectedIndex === 0
|
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 0 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
text: "Cloudflare"
|
text: "Cloudflare"
|
||||||
tooltipText: "Set DNS to Cloudflare"
|
tooltipText: "Set DNS to Cloudflare"
|
||||||
tooltipBackground: root.background
|
|
||||||
tooltipForeground: root.foreground
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
active: true
|
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)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 1 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
iconText: ""
|
iconText: ""
|
||||||
tooltipText: "Refresh"
|
tooltipText: "Refresh"
|
||||||
tooltipBackground: root.background
|
|
||||||
tooltipForeground: root.foreground
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
horizontalPadding: 8
|
horizontalPadding: 8
|
||||||
verticalPadding: 4
|
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)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 2 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
iconText: ""
|
iconText: ""
|
||||||
text: "On"
|
text: "On"
|
||||||
tooltipText: "Turn Bluetooth off"
|
tooltipText: "Turn Bluetooth off"
|
||||||
tooltipBackground: root.background
|
|
||||||
tooltipForeground: root.foreground
|
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
active: true
|
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)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 3 } }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PillButton {
|
Button {
|
||||||
text: "Apply"
|
text: "Apply"
|
||||||
foreground: root.foreground
|
|
||||||
fontFamily: root.fontFamily
|
|
||||||
focusable: true
|
focusable: true
|
||||||
bordered: 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)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
HoverHandler { onHoveredChanged: if (hovered) { root.focusSection = "pill-button"; root.selectedIndex = 4 } }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- CursorPill --------------------------------------------------
|
// ---- ButtonGroup -------------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "CursorPill"
|
text: "ButtonGroup"
|
||||||
color: root.foreground
|
color: root.foreground
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.subtitle
|
font.pixelSize: Style.font.subtitle
|
||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
Text {
|
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)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
@@ -928,38 +917,22 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: parent.width
|
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)
|
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||||
radius: Style.cornerRadius
|
radius: Style.cornerRadius
|
||||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
Row {
|
ButtonGroup {
|
||||||
id: cpRow
|
id: choiceRow
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.leftMargin: 14
|
anchors.leftMargin: 14
|
||||||
spacing: 6
|
options: ["top", "right", "bottom", "left"]
|
||||||
|
value: root.choiceDemoValue
|
||||||
Repeater {
|
onChanged: function(v) {
|
||||||
model: ["DHCP", "Cloudflare", "Google", "Custom"]
|
root.focusSection = "button-group"
|
||||||
CursorPill {
|
root.choiceDemoValue = v
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 -----------------------------------------------------
|
// ---- TextField -----------------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -1421,7 +1331,7 @@ Item {
|
|||||||
font.bold: true
|
font.bold: true
|
||||||
}
|
}
|
||||||
Text {
|
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)
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
font.family: root.fontFamily
|
font.family: root.fontFamily
|
||||||
font.pixelSize: Style.font.caption
|
font.pixelSize: Style.font.caption
|
||||||
|
|||||||
@@ -680,14 +680,14 @@ Item {
|
|||||||
Row {
|
Row {
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
spacing: 8
|
spacing: 8
|
||||||
PillButton {
|
Button {
|
||||||
text: "Cancel"
|
text: "Cancel"
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
fontFamily: root.fontFamily
|
fontFamily: root.fontFamily
|
||||||
focusable: true
|
focusable: true
|
||||||
onClicked: root.discardWidgetSettings()
|
onClicked: root.discardWidgetSettings()
|
||||||
}
|
}
|
||||||
PillButton {
|
Button {
|
||||||
text: "Apply"
|
text: "Apply"
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
fontFamily: root.fontFamily
|
fontFamily: root.fontFamily
|
||||||
@@ -788,7 +788,7 @@ Item {
|
|||||||
|
|
||||||
Row {
|
Row {
|
||||||
Layout.alignment: Qt.AlignRight
|
Layout.alignment: Qt.AlignRight
|
||||||
PillButton {
|
Button {
|
||||||
text: "Reset bar to defaults"
|
text: "Reset bar to defaults"
|
||||||
foreground: root.urgent
|
foreground: root.urgent
|
||||||
fontFamily: root.fontFamily
|
fontFamily: root.fontFamily
|
||||||
@@ -829,7 +829,7 @@ Item {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: positionGroup.positions
|
model: positionGroup.positions
|
||||||
|
|
||||||
delegate: ChoiceButton {
|
delegate: Button {
|
||||||
required property string modelData
|
required property string modelData
|
||||||
|
|
||||||
text: modelData
|
text: modelData
|
||||||
|
|||||||
Reference in New Issue
Block a user