Files
arthur-os/shell/Ui/ButtonGroup.qml

133 lines
4.7 KiB
QML

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.
//
// Keyboard navigation. The group itself is a single Tab stop, not one
// stop per chip — so in a form that walks `activeFocusOnTab` items with
// Tab / j / k, the cursor enters the group as a unit. Once focused,
// h / l / Left / Right walks between chips and Enter / Space activates
// the current one. The selected chip is the default landing point so
// users see their existing choice on arrival.
//
// Panel-cursor consumers (the bar widget panels) drive `cursorIndex`
// directly and listen on `hovered` to sync the mouse — Tab focus and
// `cursorIndex` are independent; either one paints the chip's hot
// state, and the bar widget panels never give Tab focus to a
// ButtonGroup so they only see the cursorIndex path.
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: true
// -1 disables the external cursor highlight (the panel-cursor case).
// Driven by a containing panel; the group's own Tab-focus h/l
// tracking is internal and lives in _focusedIndex.
property int cursorIndex: -1
// Internal: which chip h / l / Left / Right is currently sitting on
// when the group itself has Tab focus. Reset to the selected option
// each time focus arrives so the user sees their existing choice.
property int _focusedIndex: -1
signal changed(string value)
signal hovered(int index, bool isHovered)
spacing: Style.spacing.md
activeFocusOnTab: focusable
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) : ""
}
function selectedOptionIndex() {
for (var i = 0; i < options.length; i++)
if (optionValue(options[i]) === value) return i
return -1
}
function activateFocused() {
if (_focusedIndex < 0 || _focusedIndex >= options.length) return
var v = optionValue(options[_focusedIndex])
root.changed(v)
}
onActiveFocusChanged: {
if (activeFocus) {
var idx = selectedOptionIndex()
_focusedIndex = idx < 0 ? 0 : idx
} else {
_focusedIndex = -1
}
}
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Left || event.key === Qt.Key_H
|| event.text === "h") {
_focusedIndex = Math.max(0, (_focusedIndex < 0 ? 0 : _focusedIndex) - 1)
event.accepted = true
} else if (event.key === Qt.Key_Right || event.key === Qt.Key_L
|| event.text === "l") {
var max = options.length - 1
var next = (_focusedIndex < 0 ? 0 : _focusedIndex) + 1
_focusedIndex = Math.min(max, next)
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|| event.key === Qt.Key_Space) {
activateFocused()
event.accepted = true
}
}
Repeater {
model: root.options
delegate: Button {
required property var modelData
required property int index
text: root.optionLabel(modelData)
iconText: root.optionIcon(modelData)
tooltipText: root.optionTooltip(modelData)
selected: root.optionValue(modelData) === root.value
// Chip lights up when either the external panel cursor lands here
// or the group has Tab focus and h/l has walked to this index.
hasCursor: root.cursorIndex === index
|| (root.activeFocus && root._focusedIndex === index)
// Every chip carries the standard bordered-button chrome so the
// group reads as a row of distinct options. selected / hover-cursor /
// focus are all painted by Button from Style's shared state tokens.
bordered: true
foreground: root.foreground
background: root.background
accent: root.accent
fontFamily: root.fontFamily
fontSize: root.fontSize
onClicked: root.changed(root.optionValue(modelData))
onHovered: function(h) { root.hovered(index, h) }
}
}
}