mirror of
https://github.com/arthur-pbty/arthur-os.git
synced 2026-08-04 12:47:51 +02:00
Add qs.Ui.Dropdown, SearchableDropdown, Toggle rounded variant
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
|
||||
// Themed single-select dropdown. Trigger row paints with the kit's focus
|
||||
// chrome; the popup anchors below and uses Color.popups.background +
|
||||
// Color.popups.border so it reads as a panel surface rather than the
|
||||
// platform-native ComboBox look.
|
||||
//
|
||||
// `options` accepts either a plain string[] or an array of
|
||||
// { value, label } objects (label is what we render; value is what we
|
||||
// emit). Mixing is fine — each row is interpreted independently.
|
||||
//
|
||||
// Keyboard: Tab to focus the trigger, Enter/Space opens, Esc closes,
|
||||
// j/k or Up/Down walks options inside the open popup, Enter selects.
|
||||
// A sibling SearchableDropdown reuses the same visuals but adds an
|
||||
// embedded filter input — keep the two separate so each stays simple.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property var options: []
|
||||
|
||||
property color foreground: Color.foreground
|
||||
property color background: Color.popups.background
|
||||
property color popupBorder: Color.popups.border
|
||||
property color accent: Color.accent
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
property int rowHeight: 28
|
||||
property int popupRowHeight: 28
|
||||
property bool showLabel: true
|
||||
|
||||
signal changed(string value)
|
||||
|
||||
function optionValue(o) {
|
||||
return (o && typeof o === "object") ? String(o.value) : String(o)
|
||||
}
|
||||
function optionLabel(o) {
|
||||
return (o && typeof o === "object") ? String(o.label) : String(o)
|
||||
}
|
||||
function currentLabel() {
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
if (optionValue(options[i]) === value) return optionLabel(options[i])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
implicitWidth: 240
|
||||
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: trigger
|
||||
width: parent.width
|
||||
height: root.rowHeight
|
||||
radius: Style.cornerRadius
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
|
||||
trigger.activeFocus ? 0.08 : 0.04)
|
||||
border.color: trigger.activeFocus
|
||||
? Style.focusBorderColor
|
||||
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
|
||||
border.width: trigger.activeFocus ? Style.focusBorderWidth : 1
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|
||||
|| event.key === Qt.Key_Space || event.key === Qt.Key_Down) {
|
||||
popup.opened ? popup.close() : popup.open()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape && popup.opened) {
|
||||
popup.close(); event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 6
|
||||
text: root.currentLabel()
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: chevron
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 8
|
||||
text: ""
|
||||
color: Qt.darker(root.foreground, 1.2)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
trigger.forceActiveFocus()
|
||||
popup.opened ? popup.close() : popup.open()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popup
|
||||
x: 0
|
||||
y: trigger.height + 2
|
||||
width: trigger.width
|
||||
implicitHeight: Math.min(root.options.length * root.popupRowHeight + 2,
|
||||
root.popupRowHeight * 8 + 2)
|
||||
padding: 1
|
||||
focus: true
|
||||
|
||||
background: Rectangle {
|
||||
color: root.background
|
||||
border.color: root.popupBorder
|
||||
border.width: 1
|
||||
radius: Style.cornerRadius
|
||||
}
|
||||
|
||||
onOpened: optionList.currentIndex = Math.max(0, optionList.indexOfValue(root.value))
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) { popup.close(); event.accepted = true }
|
||||
else if (event.key === Qt.Key_Down || event.text === "j") {
|
||||
optionList.currentIndex = Math.min(root.options.length - 1, optionList.currentIndex + 1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up || event.text === "k") {
|
||||
optionList.currentIndex = Math.max(0, optionList.currentIndex - 1)
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
optionList.selectCurrent(); event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: ListView {
|
||||
id: optionList
|
||||
implicitHeight: contentHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: root.options
|
||||
currentIndex: -1
|
||||
|
||||
function indexOfValue(v) {
|
||||
for (var i = 0; i < root.options.length; i++)
|
||||
if (root.optionValue(root.options[i]) === v) return i
|
||||
return -1
|
||||
}
|
||||
|
||||
function selectCurrent() {
|
||||
if (currentIndex < 0 || currentIndex >= root.options.length) return
|
||||
var v = root.optionValue(root.options[currentIndex])
|
||||
root.value = v
|
||||
root.changed(v)
|
||||
popup.close()
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: optionList.width
|
||||
height: root.popupRowHeight
|
||||
color: index === optionList.currentIndex
|
||||
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
|
||||
: "transparent"
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
text: root.optionLabel(modelData)
|
||||
color: index === optionList.currentIndex ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPositionChanged: optionList.currentIndex = parent.index
|
||||
onClicked: optionList.selectCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,10 @@ Grouped by what they're for, not alphabetically.
|
||||
| `PillButton` | Rounded button with optional icon + label + tooltip. Has `active`, `hasCursor` (keyboard cursor), `focusable` (Tab-focus with accent ring), `bordered` (persistent 1px idle border for primary form buttons), and `enabled`. Hover and keyboard cursor render identically (fill + border) via the shared `hot` state; Tab-focus uses an accent ring that wins over both. |
|
||||
| `CursorPill` | `PillButton` that participates in a panel's single-cursor model. Adds a `hovered(bool)` signal so the panel can update its cursor state on mouse enter/leave. Use for DNS-pill / header-pill / segmented-choice patterns. |
|
||||
| `ChoiceButton` | A single button in a mutually-exclusive choice group (segmented control). `selected` uses accent fill+border; focus uses `Style.focusBorderColor` so keyboard nav reads differently from selection. |
|
||||
| `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. |
|
||||
| `Toggle` | Title + description + switch. Click anywhere on the row to flip; caller updates `checked` in response. `rounded` auto-detects from `Style.cornerRadius` so the switch is a pill on round-corners themes and square on sharp; override per-instance to force one or the other. |
|
||||
| `TextField` | Single-line input. Inherits from Qt Quick Controls `TextField` so all of its base API (text, placeholderText, accepted, validator, ...) is available. Adds `password: bool`, `foreground` / `accent` / `selectionTint` color overrides, and `horizontalPadding` / `verticalPadding` size knobs. Focus styling uses `Style.focusBorderColor` to match `Toggle` and `ChoiceButton`. |
|
||||
| `Dropdown` | Single-select dropdown with a themed popup (no platform-native ComboBox chrome). `options` accepts `string[]` or `[{ value, label }]`. Keyboard: Tab to focus trigger, Enter/Space opens, j/k or arrows walk options, Enter selects. |
|
||||
| `SearchableDropdown` | `Dropdown` with an embedded search field at the top of the popup that filters options as you type. Use when the option count is high enough that scanning is friction (e.g. bar settings "+ Add widget"). Options can also carry a `description` string that the filter matches against. |
|
||||
| `PanelActionButton` | 22×22 right-edge action button (confirm, forget, unpair). `hoverColor` swaps between default foreground tint and urgent (red) tint. `focusable: true` enables Tab-focus with an accent ring — used for the bar settings widget-card row controls. |
|
||||
| `PanelSlider` | Volume/progress slider. Drag, click track, or wheel. `moved(value)` fires per change, `released(value)` once at end. (Named to avoid colliding with `QtQuick.Controls.Slider`.) |
|
||||
| `WidgetButton` | Bar widget chrome — for the strip itself, not for inside panels. |
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Commons
|
||||
|
||||
// Searchable single-select dropdown. Same trigger shape as Dropdown, but
|
||||
// the popup leads with an embedded TextField that filters the option
|
||||
// list in real time. Use for pickers with enough options that scanning
|
||||
// is friction (e.g. bar settings "+ Add widget").
|
||||
//
|
||||
// Filtering is case-insensitive substring against each option's label.
|
||||
// Options can be string[] or [{ value, label, description? }] — the same
|
||||
// shape Dropdown accepts. The filter clears whenever the popup closes.
|
||||
//
|
||||
// Keyboard: Tab to focus the trigger, Enter/Space opens (search focused
|
||||
// immediately). Down arrow from the search jumps to the first match;
|
||||
// Up from the first match returns to the search. Enter selects, Esc
|
||||
// closes (and clears the filter).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property var options: []
|
||||
property string placeholderText: "Search..."
|
||||
property string emptyText: "No matches"
|
||||
|
||||
property color foreground: Color.foreground
|
||||
property color background: Color.popups.background
|
||||
property color popupBorder: Color.popups.border
|
||||
property color accent: Color.accent
|
||||
property string fontFamily: "JetBrainsMono Nerd Font"
|
||||
property int rowHeight: 28
|
||||
property int popupRowHeight: 28
|
||||
property int popupMinHeight: 220
|
||||
property bool showLabel: true
|
||||
|
||||
signal changed(string value)
|
||||
|
||||
function optionValue(o) {
|
||||
return (o && typeof o === "object") ? String(o.value) : String(o)
|
||||
}
|
||||
function optionLabel(o) {
|
||||
return (o && typeof o === "object") ? String(o.label) : String(o)
|
||||
}
|
||||
function optionDescription(o) {
|
||||
return (o && typeof o === "object" && o.description) ? String(o.description) : ""
|
||||
}
|
||||
function currentLabel() {
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
if (optionValue(options[i]) === value) return optionLabel(options[i])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
property var filtered: options
|
||||
function recomputeFiltered() {
|
||||
var q = searchField.text.toLowerCase()
|
||||
if (!q) { filtered = options; return }
|
||||
var out = []
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var lbl = optionLabel(options[i]).toLowerCase()
|
||||
var desc = optionDescription(options[i]).toLowerCase()
|
||||
if (lbl.indexOf(q) !== -1 || desc.indexOf(q) !== -1) out.push(options[i])
|
||||
}
|
||||
filtered = out
|
||||
}
|
||||
|
||||
onOptionsChanged: recomputeFiltered()
|
||||
|
||||
implicitWidth: 260
|
||||
implicitHeight: showLabel && label !== "" ? rowHeight + 18 : rowHeight
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
visible: root.showLabel && root.label !== ""
|
||||
text: root.label
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: trigger
|
||||
width: parent.width
|
||||
height: root.rowHeight
|
||||
radius: Style.cornerRadius
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
|
||||
trigger.activeFocus ? 0.08 : 0.04)
|
||||
border.color: trigger.activeFocus
|
||||
? Style.focusBorderColor
|
||||
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
|
||||
border.width: trigger.activeFocus ? Style.focusBorderWidth : 1
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter
|
||||
|| event.key === Qt.Key_Space || event.key === Qt.Key_Down) {
|
||||
popup.opened ? popup.close() : popup.open()
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Escape && popup.opened) {
|
||||
popup.close(); event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: chevron.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 6
|
||||
text: root.currentLabel() || root.placeholderText
|
||||
color: root.currentLabel() ? root.foreground : Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
id: chevron
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 8
|
||||
text: ""
|
||||
color: Qt.darker(root.foreground, 1.2)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
trigger.forceActiveFocus()
|
||||
popup.opened ? popup.close() : popup.open()
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: popup
|
||||
x: 0
|
||||
y: trigger.height + 2
|
||||
width: trigger.width
|
||||
implicitHeight: Math.max(root.popupMinHeight,
|
||||
Math.min(root.filtered.length * root.popupRowHeight + 50,
|
||||
root.popupRowHeight * 6 + 50))
|
||||
padding: 1
|
||||
focus: true
|
||||
|
||||
background: Rectangle {
|
||||
color: root.background
|
||||
border.color: root.popupBorder
|
||||
border.width: 1
|
||||
radius: Style.cornerRadius
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
searchField.text = ""
|
||||
root.recomputeFiltered()
|
||||
Qt.callLater(function() { searchField.forceActiveFocus() })
|
||||
}
|
||||
onClosed: searchField.text = ""
|
||||
|
||||
contentItem: Column {
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 38
|
||||
|
||||
TextField {
|
||||
id: searchField
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
placeholderText: root.placeholderText
|
||||
foreground: root.foreground
|
||||
accent: root.accent
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
|
||||
onTextChanged: {
|
||||
root.recomputeFiltered()
|
||||
if (resultList.count > 0) resultList.currentIndex = 0
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
popup.close(); event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
if (resultList.count > 0) {
|
||||
resultList.currentIndex = 0
|
||||
resultList.forceActiveFocus()
|
||||
}
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (resultList.count > 0) {
|
||||
resultList.currentIndex = 0
|
||||
resultList.selectCurrent()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: popup.height - 38 - 2 - 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: resultList.count === 0
|
||||
text: root.emptyText
|
||||
color: Qt.darker(root.foreground, 1.6)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: resultList
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
model: root.filtered
|
||||
currentIndex: -1
|
||||
keyNavigationEnabled: false
|
||||
|
||||
function selectCurrent() {
|
||||
if (currentIndex < 0 || currentIndex >= root.filtered.length) return
|
||||
var v = root.optionValue(root.filtered[currentIndex])
|
||||
root.value = v
|
||||
root.changed(v)
|
||||
popup.close()
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
popup.close(); event.accepted = true
|
||||
} else if (event.key === Qt.Key_Down || event.text === "j") {
|
||||
if (resultList.currentIndex >= resultList.count - 1) {
|
||||
event.accepted = true; return
|
||||
}
|
||||
resultList.currentIndex = resultList.currentIndex + 1
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Up || event.text === "k") {
|
||||
if (resultList.currentIndex <= 0) {
|
||||
searchField.forceActiveFocus()
|
||||
event.accepted = true; return
|
||||
}
|
||||
resultList.currentIndex = resultList.currentIndex - 1
|
||||
event.accepted = true
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
resultList.selectCurrent(); event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: resultList.width
|
||||
height: root.popupRowHeight
|
||||
color: index === resultList.currentIndex
|
||||
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
|
||||
: "transparent"
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 1
|
||||
|
||||
Text {
|
||||
text: root.optionLabel(modelData)
|
||||
color: index === resultList.currentIndex ? root.accent : root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
visible: text !== ""
|
||||
text: root.optionDescription(modelData)
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPositionChanged: resultList.currentIndex = parent.index
|
||||
onClicked: resultList.selectCurrent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,10 @@ import qs.Commons
|
||||
// Focus styling follows the shared Style tokens (accent border + tinted
|
||||
// fill on activeFocus) so keyboard nav looks the same here as on
|
||||
// ChoiceButton and other focusable Ui components.
|
||||
//
|
||||
// `rounded` auto-detects from Style.cornerRadius so the switch follows
|
||||
// the theme: pill shape on round-corners themes, square on sharp.
|
||||
// Callers can override per-instance.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
@@ -22,6 +26,10 @@ Rectangle {
|
||||
// border + tinted fill via Style tokens) so cursor and Tab focus read the same.
|
||||
property bool hasCursor: false
|
||||
|
||||
// Switch shape follows the theme by default: pill on round, square on sharp.
|
||||
// Override per-instance if a caller wants the opposite.
|
||||
property bool rounded: Style.cornerRadius > 0
|
||||
|
||||
property color foreground: Color.foreground
|
||||
property color accent: Color.accent
|
||||
property string fontFamily: "monospace"
|
||||
@@ -89,7 +97,7 @@ Rectangle {
|
||||
id: track
|
||||
width: 42
|
||||
height: 22
|
||||
radius: height / 2
|
||||
radius: root.rounded ? height / 2 : 0
|
||||
color: root.checked
|
||||
? Qt.rgba(root.accent.r, root.accent.g, root.accent.b, 0.35)
|
||||
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.12)
|
||||
@@ -104,7 +112,7 @@ Rectangle {
|
||||
Rectangle {
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
radius: root.rounded ? 8 : 0
|
||||
x: root.checked ? track.width - width - 3 : 3
|
||||
y: 3
|
||||
color: root.checked ? root.accent : Qt.darker(root.foreground, 1.25)
|
||||
|
||||
@@ -3,6 +3,7 @@ module qs.Ui
|
||||
ChoiceButton 1.0 ChoiceButton.qml
|
||||
CursorPill 1.0 CursorPill.qml
|
||||
CursorSurface 1.0 CursorSurface.qml
|
||||
Dropdown 1.0 Dropdown.qml
|
||||
KeyboardPanel 1.0 KeyboardPanel.qml
|
||||
PanelActionButton 1.0 PanelActionButton.qml
|
||||
PanelKeyCatcher 1.0 PanelKeyCatcher.qml
|
||||
@@ -12,6 +13,7 @@ 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
|
||||
Toggle 1.0 Toggle.qml
|
||||
WidgetButton 1.0 WidgetButton.qml
|
||||
|
||||
@@ -21,7 +21,7 @@ Item {
|
||||
function open(payloadJson) {
|
||||
closingFromHost = false
|
||||
window.visible = true
|
||||
Qt.callLater(function() { if (scrollArea) scrollArea.forceActiveFocus() })
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
|
||||
// Host-initiated close (`shell hide`). Visibility flips without
|
||||
@@ -68,6 +68,9 @@ Item {
|
||||
property int pillDemoIndex: 1
|
||||
property string choiceDemoValue: "top"
|
||||
property bool toggleDemoOn: true
|
||||
property bool toggleSquareOn: false
|
||||
property string dropdownDemoValue: "calendar"
|
||||
property string searchableDemoValue: ""
|
||||
|
||||
FloatingWindow {
|
||||
id: window
|
||||
@@ -86,29 +89,59 @@ Item {
|
||||
id: focusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
// Esc + h/l for the cursor demo. Other keys (arrow keys, Page_Down,
|
||||
// Home/End) propagate down to ScrollView's built-in scroll handling so
|
||||
// keyboard scrolling works. AfterItem priority means a focused inner
|
||||
// control would get its keys first — we don't have any here yet.
|
||||
|
||||
// Scroll the gallery's ScrollView by `dy` pixels. Bound below 0 and
|
||||
// above (1 - thumbSize) so the thumb stays on the track when the
|
||||
// user mashes Page_Down past the end.
|
||||
function scrollBy(dy) {
|
||||
var sb = scrollArea.ScrollBar.vertical
|
||||
if (!sb || scrollArea.contentHeight <= scrollArea.height) return
|
||||
var newPos = sb.position + dy / scrollArea.contentHeight
|
||||
sb.position = Math.max(0, Math.min(1 - sb.size, newPos))
|
||||
}
|
||||
|
||||
// Page/Home/End handled here so they bubble up past keyCatcher
|
||||
// (which only consumes Esc / Enter / j-k-h-l / x / text keys).
|
||||
Keys.priority: Keys.AfterItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.requestClose(); event.accepted = true
|
||||
} else if (event.text === "l") {
|
||||
root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1)
|
||||
if (event.key === Qt.Key_PageDown) {
|
||||
focusScope.scrollBy(300); event.accepted = true
|
||||
} else if (event.key === Qt.Key_PageUp) {
|
||||
focusScope.scrollBy(-300); event.accepted = true
|
||||
} else if (event.key === Qt.Key_Home) {
|
||||
scrollArea.ScrollBar.vertical.position = 0
|
||||
event.accepted = true
|
||||
} else if (event.text === "h") {
|
||||
root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1)
|
||||
} else if (event.key === Qt.Key_End) {
|
||||
var sb = scrollArea.ScrollBar.vertical
|
||||
if (sb) sb.position = Math.max(0, 1 - sb.size)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollArea
|
||||
// Panel-style key dispatch. j/k scroll the gallery; h/l walk the
|
||||
// cursor / pill demos; Esc closes. Same component the wifi, audio,
|
||||
// bluetooth, monitor, and settings panels all use — the gallery
|
||||
// is meant to demonstrate the standard, so it should USE the
|
||||
// standard rather than reimplement its own keyhandler.
|
||||
PanelKeyCatcher {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
anchors.margins: 18
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
onMoveRequested: function(dx, dy) {
|
||||
if (dy !== 0) {
|
||||
focusScope.scrollBy(dy * 60)
|
||||
} else if (dx !== 0) {
|
||||
root.cursorDemoIndex = Math.max(0, Math.min(2, root.cursorDemoIndex + dx))
|
||||
root.pillDemoIndex = Math.max(0, Math.min(3, root.pillDemoIndex + dx))
|
||||
}
|
||||
}
|
||||
onCloseRequested: root.requestClose()
|
||||
|
||||
ScrollView {
|
||||
id: scrollArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: 18
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: scrollArea.availableWidth
|
||||
@@ -161,7 +194,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: shCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -217,7 +250,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: sepCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -262,7 +295,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: csCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -343,7 +376,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: pillCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -431,7 +464,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: cpRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -488,7 +521,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: pabCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -619,7 +652,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: sliderRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -690,7 +723,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: choiceRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -743,7 +776,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: tfCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -809,6 +842,135 @@ Item {
|
||||
checked: root.toggleDemoOn
|
||||
onClicked: root.toggleDemoOn = !root.toggleDemoOn
|
||||
}
|
||||
|
||||
Toggle {
|
||||
width: parent.width
|
||||
label: "Square switch (forced)"
|
||||
description: "`rounded: false` overrides the theme auto-detect so the switch reads square even when corners are round. Set `rounded: Style.cornerRadius > 0` (the default) to follow the theme."
|
||||
foreground: root.foreground
|
||||
accent: root.accent
|
||||
fontFamily: root.fontFamily
|
||||
rounded: false
|
||||
checked: root.toggleSquareOn
|
||||
onClicked: root.toggleSquareOn = !root.toggleSquareOn
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Dropdown -----------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Dropdown"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Themed single-select with a panel-styled popup. Tab to focus the trigger, Enter/Space opens, j/k or arrows walk options, Enter selects. Options can be plain strings or { value, label } objects."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: ddCol.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
|
||||
|
||||
Column {
|
||||
id: ddCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Dropdown {
|
||||
width: 260
|
||||
label: "Center anchor"
|
||||
fontFamily: root.fontFamily
|
||||
options: ["calendar", "weather", "clock", "battery"]
|
||||
value: root.dropdownDemoValue
|
||||
onChanged: function(v) { root.dropdownDemoValue = v }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- SearchableDropdown -------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "SearchableDropdown"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Dropdown with an embedded filter input. Type to narrow the list, Down to jump from the search to the first match, Enter to select. Use this for the bar settings \"+ Add widget\" picker and any other long-list selector."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: sddCol.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
|
||||
|
||||
Column {
|
||||
id: sddCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 6
|
||||
|
||||
SearchableDropdown {
|
||||
width: 280
|
||||
label: "Add widget"
|
||||
fontFamily: root.fontFamily
|
||||
placeholderText: "Search widgets..."
|
||||
options: [
|
||||
{ value: "clock", label: "Clock", description: "Time + date display" },
|
||||
{ value: "weather", label: "Weather", description: "Local conditions and forecast" },
|
||||
{ value: "battery", label: "Battery", description: "Charge level + power profile" },
|
||||
{ value: "audio", label: "Audio", description: "Output sink + volume" },
|
||||
{ value: "network", label: "Network", description: "Wi-Fi + ethernet status" },
|
||||
{ value: "bluetooth", label: "Bluetooth", description: "Paired and nearby devices" },
|
||||
{ value: "monitor", label: "Monitor", description: "Brightness + scale" },
|
||||
{ value: "calendar", label: "Calendar", description: "Month grid flyout" },
|
||||
{ value: "media", label: "Media", description: "Now-playing + transport" },
|
||||
{ value: "workspaces", label: "Workspaces", description: "Hyprland workspace pills" },
|
||||
{ value: "system-tray", label: "System tray", description: "StatusNotifierItem icons" },
|
||||
{ value: "omarchy-menu", label: "Omarchy menu", description: "Launcher / system menu" },
|
||||
{ value: "power-profiles", label: "Power profiles", description: "Performance / balanced / saver" },
|
||||
{ value: "hardware", label: "Hardware", description: "CPU, GPU, mem utilization" },
|
||||
{ value: "notifications", label: "Notifications", description: "Recent notification history" }
|
||||
]
|
||||
value: root.searchableDemoValue
|
||||
onChanged: function(v) { root.searchableDemoValue = v }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Composed example -------------------------------------------
|
||||
@@ -836,7 +998,7 @@ Item {
|
||||
width: parent.width
|
||||
implicitHeight: composedCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
radius: Style.cornerRadius
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
@@ -965,6 +1127,7 @@ Item {
|
||||
Item { width: 1; height: 12 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user