Make Button state composition visible in ButtonGroup and the gallery
Three small fixes around the new Button + ButtonGroup so the cursor is
always visible and the section reads as a single Button showing flag
combinations.
ButtonGroup now exposes cursorIndex + hovered(index, isHovered) so
panels can drive a single Button's hasCursor without reaching past
ButtonGroup's API. Every chip gets bordered: true so the row reads as
a real picker — distinct outlines, not just a tinted fill that vanishes
into the background.
Button's border treatment grows two paths so the cursor is always
visible regardless of other state: bordered + hot recolors the border
to the accent and thickens to the focus-ring width, and selected + hot
thickens the existing accent border to the focus-ring width (so the
cursor landing on the chosen option still reads as cursor presence,
instead of disappearing into the selected fill).
Gallery's Button section pairs each demo button with a small caption
('idle', 'active', 'icon only', 'icon + active', 'bordered +
focusable') so the row reads as one Button with its state flags side
by side rather than five unrelated buttons. The ButtonGroup section
gets the cursor wiring (cursorIndex + onHovered) plus an
onFocusedChanged auto-scroll so the cursor visual is reachable via
j/k h/l from the panel cursor model.
This commit is contained in:
+11
-1
@@ -72,13 +72,23 @@ Rectangle {
|
|||||||
: active ? Style.selectedFill
|
: active ? Style.selectedFill
|
||||||
: background
|
: background
|
||||||
|
|
||||||
|
// Border color follows the same precedence as fill: focus ring wins,
|
||||||
|
// then selected, then cursor on bordered (paints accent so the chip
|
||||||
|
// structure clearly reads as "cursor is here"), then plain bordered
|
||||||
|
// (foreground), then nothing.
|
||||||
border.color: _showFocusRing ? Style.focusBorderColor
|
border.color: _showFocusRing ? Style.focusBorderColor
|
||||||
: selected ? accent
|
: selected ? accent
|
||||||
|
: (bordered && hot) ? Style.focusBorderColor
|
||||||
: bordered ? foreground
|
: bordered ? foreground
|
||||||
: Style.idleBorderColor
|
: Style.idleBorderColor
|
||||||
|
|
||||||
|
// selected+hot thickens to the focus-ring width so the cursor remains
|
||||||
|
// visible on the chosen option (otherwise selected's accent fill+border
|
||||||
|
// masks any hot fill). bordered+hot also thickens so the chip cursor
|
||||||
|
// reads as a deliberate state change rather than a faint tint.
|
||||||
border.width: _showFocusRing ? Style.focusBorderWidth
|
border.width: _showFocusRing ? Style.focusBorderWidth
|
||||||
: selected ? Math.max(Style.borderWidth, 2)
|
: selected ? (hot ? Style.focusBorderWidth : Math.max(Style.borderWidth, 2))
|
||||||
|
: (bordered && hot) ? Style.focusBorderWidth
|
||||||
: bordered ? Style.borderWidth
|
: bordered ? Style.borderWidth
|
||||||
: 0
|
: 0
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import qs.Commons
|
|||||||
// `options` is either a plain string[] (label == value) or an array of
|
// `options` is either a plain string[] (label == value) or an array of
|
||||||
// { value, label, icon?, tooltip? } objects. Mixing is fine.
|
// { value, label, icon?, tooltip? } objects. Mixing is fine.
|
||||||
//
|
//
|
||||||
// For panel-cursor-driven selection (where j/k walks a row), use bare
|
// Panels with their own keyboard cursor model bind `cursorIndex` to the
|
||||||
// `Button { hasCursor: ... }` instances in a Row — ButtonGroup is the
|
// currently-focused option (-1 = no cursor) and listen on `hovered` to
|
||||||
// convenience for non-cursor form contexts where you just need
|
// keep that state synced with the mouse. Forms that don't care about
|
||||||
// "selected: value === optionValue" wiring.
|
// the panel cursor model can leave both alone.
|
||||||
Row {
|
Row {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
@@ -24,7 +24,12 @@ Row {
|
|||||||
property real fontSize: Style.font.body
|
property real fontSize: Style.font.body
|
||||||
property bool focusable: false
|
property bool focusable: false
|
||||||
|
|
||||||
|
// -1 disables the cursor highlight (the form case). Set from a panel
|
||||||
|
// to drive Button.hasCursor on the matching index.
|
||||||
|
property int cursorIndex: -1
|
||||||
|
|
||||||
signal changed(string value)
|
signal changed(string value)
|
||||||
|
signal hovered(int index, bool isHovered)
|
||||||
|
|
||||||
spacing: 6
|
spacing: 6
|
||||||
|
|
||||||
@@ -46,10 +51,16 @@ Row {
|
|||||||
|
|
||||||
delegate: Button {
|
delegate: Button {
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
required property int index
|
||||||
text: root.optionLabel(modelData)
|
text: root.optionLabel(modelData)
|
||||||
iconText: root.optionIcon(modelData)
|
iconText: root.optionIcon(modelData)
|
||||||
tooltipText: root.optionTooltip(modelData)
|
tooltipText: root.optionTooltip(modelData)
|
||||||
selected: root.optionValue(modelData) === root.value
|
selected: root.optionValue(modelData) === root.value
|
||||||
|
hasCursor: root.cursorIndex === index
|
||||||
|
// Every chip carries an idle border so the group reads as a row of
|
||||||
|
// distinct options. selected paints accent; the cursor recolors the
|
||||||
|
// chip's border to accent via Button's bordered+hot path.
|
||||||
|
bordered: true
|
||||||
foreground: root.foreground
|
foreground: root.foreground
|
||||||
background: root.background
|
background: root.background
|
||||||
accent: root.accent
|
accent: root.accent
|
||||||
@@ -57,6 +68,7 @@ Row {
|
|||||||
fontSize: root.fontSize
|
fontSize: root.fontSize
|
||||||
focusable: root.focusable
|
focusable: root.focusable
|
||||||
onClicked: root.changed(root.optionValue(modelData))
|
onClicked: root.changed(root.optionValue(modelData))
|
||||||
|
onHovered: function(h) { root.hovered(index, h) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -833,9 +833,16 @@ Item {
|
|||||||
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: 16
|
||||||
|
|
||||||
|
// Each demo Button is paired with a caption labeling the
|
||||||
|
// state(s) it exercises so the section reads as one Button
|
||||||
|
// showing its flag combinations side by side.
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 6
|
||||||
Button {
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: "DHCP"
|
text: "DHCP"
|
||||||
tooltipText: "Use DNS from DHCP"
|
tooltipText: "Use DNS from DHCP"
|
||||||
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
|
hasCursor: root.focusSection === "button" && root.selectedIndex === 0
|
||||||
@@ -844,8 +851,19 @@ Item {
|
|||||||
}
|
}
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "idle"
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 6
|
||||||
Button {
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: "Cloudflare"
|
text: "Cloudflare"
|
||||||
tooltipText: "Set DNS to Cloudflare"
|
tooltipText: "Set DNS to Cloudflare"
|
||||||
active: true
|
active: true
|
||||||
@@ -855,8 +873,19 @@ Item {
|
|||||||
}
|
}
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "active"
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 6
|
||||||
Button {
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
iconText: ""
|
iconText: ""
|
||||||
tooltipText: "Refresh"
|
tooltipText: "Refresh"
|
||||||
horizontalPadding: 8
|
horizontalPadding: 8
|
||||||
@@ -867,8 +896,19 @@ Item {
|
|||||||
}
|
}
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "icon only"
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 6
|
||||||
Button {
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
iconText: ""
|
iconText: ""
|
||||||
text: "On"
|
text: "On"
|
||||||
tooltipText: "Turn Bluetooth off"
|
tooltipText: "Turn Bluetooth off"
|
||||||
@@ -879,8 +919,19 @@ Item {
|
|||||||
}
|
}
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "icon + active"
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 6
|
||||||
Button {
|
Button {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: "Apply"
|
text: "Apply"
|
||||||
focusable: true
|
focusable: true
|
||||||
bordered: true
|
bordered: true
|
||||||
@@ -890,14 +941,25 @@ Item {
|
|||||||
}
|
}
|
||||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(this)
|
||||||
}
|
}
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: "bordered + focusable"
|
||||||
|
color: Qt.darker(root.foreground, 1.5)
|
||||||
|
font.family: root.fontFamily
|
||||||
|
font.pixelSize: Style.font.caption
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- ButtonGroup -------------------------------------------------
|
// ---- ButtonGroup -------------------------------------------------
|
||||||
Column {
|
Column {
|
||||||
|
id: buttonGroupSection
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: 8
|
spacing: 8
|
||||||
|
readonly property bool focused: root.focusSection === "button-group"
|
||||||
|
onFocusedChanged: if (focused) root.ensureCursorVisible(this)
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "ButtonGroup"
|
text: "ButtonGroup"
|
||||||
@@ -930,10 +992,17 @@ Item {
|
|||||||
anchors.leftMargin: 14
|
anchors.leftMargin: 14
|
||||||
options: ["top", "right", "bottom", "left"]
|
options: ["top", "right", "bottom", "left"]
|
||||||
value: root.choiceDemoValue
|
value: root.choiceDemoValue
|
||||||
|
cursorIndex: root.focusSection === "button-group" ? root.selectedIndex : -1
|
||||||
onChanged: function(v) {
|
onChanged: function(v) {
|
||||||
root.focusSection = "button-group"
|
root.focusSection = "button-group"
|
||||||
root.choiceDemoValue = v
|
root.choiceDemoValue = v
|
||||||
}
|
}
|
||||||
|
onHovered: function(index, isHovered) {
|
||||||
|
if (isHovered) {
|
||||||
|
root.focusSection = "button-group"
|
||||||
|
root.selectedIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user