Files
omarchycn/default/quickshell/omarchy-shell/Ui/Dropdown.qml
T

211 lines
6.8 KiB
QML

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()
}
}
}
}
}
}
}