Promote shell to its own top-level directory

This commit is contained in:
David Heinemeier Hansson
2026-05-18 14:56:59 +02:00
parent d782705878
commit 0fe985b45d
83 changed files with 50 additions and 28 deletions
+77
View File
@@ -0,0 +1,77 @@
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. Visuals match the activeFocus look
// (foreground 2px border) so cursor and Tab focus read the same.
property bool hasCursor: false
property color foreground: Color.foreground
property color background: Color.background
property color accent: Color.accent
property string fontFamily: "monospace"
property real fontSize: 12
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 ? Style.hotFill : background)
border.color: selected
? accent
: (activeFocus || hasCursor ? foreground : Qt.rgba(foreground.r, foreground.g, foreground.b, 0.4))
border.width: selected ? 2 : (activeFocus || hasCursor ? 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)
}
}
+28
View File
@@ -0,0 +1,28 @@
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)
}
}
+26
View File
@@ -0,0 +1,26 @@
import QtQuick
import qs.Commons
// Shared visual chrome for keyboard-and-mouse-navigable items inside a panel.
// Contract: items must NOT read `containsMouse` for color/border. Mouse
// hover updates the panel's cursor state at the root; visuals derive from
// `hasCursor` / `current`. That's what guarantees a single highlight on
// screen at any time across both keyboard and mouse interaction.
Rectangle {
id: root
property bool hasCursor: false
property bool current: false
property color foreground: "#cacccc"
property color fill: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.18)
radius: Style.cornerRadius
color: (hasCursor || current) ? fill : "transparent"
border.width: hasCursor ? 1 : 0
border.color: foreground
Behavior on color {
ColorAnimation { duration: 60 }
}
}
+238
View File
@@ -0,0 +1,238 @@
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
// Panel-cursor flag. When true, the trigger renders the same focus ring
// as Tab-focus so a panel's keyboard cursor lands here identically.
// Emits `hovered(bool)` on pointer enter/leave so the panel can keep
// its cursor state in sync with the mouse.
property bool hasCursor: false
// popupOpen + open()/close()/toggle() let a parent panel know when the
// dropdown owns keys (its embedded ListView is active) and suspend its
// own keyCatcher so j/k inside the popup don't double-drive the panel
// cursor.
readonly property bool popupOpen: popup.opened
function open() { popup.open() }
function close() { popup.close() }
function toggle() { popup.opened ? popup.close() : popup.open() }
signal changed(string value)
signal hovered(bool isHovered)
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
readonly property bool _focused: trigger.activeFocus || root.hasCursor
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger._focused ? 0.08 : 0.04)
border.color: trigger._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger._focused ? Style.focusBorderWidth : 1
activeFocusOnTab: true
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
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 + Math.max(0, root.options.length - 1) * 4 + 2,
root.popupRowHeight * 8 + 7 * 4 + 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))
optionList.forceActiveFocus()
}
contentItem: ListView {
id: optionList
spacing: 4
Keys.priority: Keys.BeforeItem
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
}
}
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()
}
}
}
}
}
}
}
+212
View File
@@ -0,0 +1,212 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
// Layer-shell popup attached to a bar widget icon, designed for
// click-driven AND keyboard-driven panels (e.g. SUPER+CTRL+W summon).
//
// Built on PanelWindow with WlrKeyboardFocus.Exclusive rather than
// PopupWindow (xdg-popup). Layer-shell surfaces declared Exclusive get
// keyboard focus from Hyprland *at map time*, which is the protocol-level
// equivalent of focus-on-launch for xdg-toplevels. xdg-popups don't get
// that — they only receive keys after a click/hover routes focus through
// their parent surface — so keyboard-summoned popups fell flat without it.
//
// API is a subset of Common.PopupCard: anchorItem, owner, bar, open,
// padding, margin, contentWidth/Height, default contentItem. Missing on
// purpose (for now): centerOnBar, triggerMode ("hover"), containsMouse.
// Hover-mode popups (system-stats, weather-flyout) and centered popups
// (calendar week-view) need extra plumbing before migrating; converting
// them is a follow-up.
//
// Positioning: full-screen layer-shell with the card placed inside at
// `cardOrigin`. We use the bar window's height/width for the perpendicular
// axis (away-from-bar) because mapToItem on the anchor returns
// bar-content-relative coords with internal layout offsets baked in
// (e.g. ~13px from the bar's vertical centering of its widget row). The
// parallel axis (along-the-bar) uses the anchor's content x/y since the
// bar spans full screen on that axis.
//
// Outside-click dismissal: an overlay MouseArea catches clicks, with the
// QsWindow.mask subtracting the bar strip so clicks on the bar still
// reach the bar widgets (activePopout coordinator hands off to another
// popup if the user clicks a different bar icon).
PanelWindow {
id: root
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int contentWidth: 280
property int contentHeight: 200
property bool open: false
property int gap: 10 // distance between bar edge and panel
default property alias contentItem: contentHolder.children
readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property string barPos: bar ? bar.position : "top"
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
else root.open = false
}
// --- screen + lifetime ---------------------------------------------------
screen: anchorWindow ? anchorWindow.screen : null
visible: open || card.opacity > 0
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "omarchy-keyboard-panel"
WlrLayershell.layer: WlrLayer.Overlay
// Keyboard focus follows `open` (NOT `visible`). The window remains
// mapped during the fade-out so the opacity animation has something to
// animate, but keyboard/click ownership must release the moment the
// logical close fires — otherwise the user is locked out for 140ms.
WlrLayershell.keyboardFocus: open ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
// Full-screen layer-shell. The visible card is positioned inside via
// `cardOrigin`. The `mask` below makes the bar area click-through (so
// the user can click another bar icon while the panel is open and the
// activePopout coordinator swaps to that popup); everywhere else, the
// overlay catches the click and dismisses via the MouseArea below.
anchors {
top: true
bottom: true
left: true
right: true
}
// Clickable region = whole screen MINUS the bar's strip. Clicks on the
// bar pass through to the bar layer; clicks anywhere else are caught
// by us and either land on the card (no-op) or trigger dismissal.
readonly property real _barStripSize: bar ? bar.barSize : 0
mask: Region {
width: root.screenW
height: root.screenH
Region {
x: root.barPos === "right" ? root.screenW - root._barStripSize : 0
y: root.barPos === "bottom" ? root.screenH - root._barStripSize : 0
width: (root.barPos === "top" || root.barPos === "bottom") ? root.screenW : root._barStripSize
height: (root.barPos === "top" || root.barPos === "bottom") ? root._barStripSize : root.screenH
intersection: Intersection.Subtract
}
}
// Track every layout change between the bar's contentItem and the
// anchor item. `transform` updates whenever any item in that chain
// moves/resizes, which is what makes the position binding below
// actually reactive — mapToItem on its own is a one-shot.
TransformWatcher {
id: anchorWatcher
a: anchorWindow ? anchorWindow.contentItem : null
b: anchorItem
}
// Anchor item's position within the bar's content surface. For a
// full-width top bar, the content x maps directly to screen x; the y
// returned here has the bar's internal padding baked in (e.g. ~13px
// from vertical centering of the widget row), which is why `cardOrigin`
// below uses `barH` for the perpendicular axis instead of this y.
readonly property point anchorScreenPos: {
anchorWatcher.transform // reactive dependency
if (!anchorItem || !anchorWindow) return Qt.point(0, 0)
return anchorItem.mapToItem(anchorWindow.contentItem, 0, 0)
}
readonly property real anchorW: anchorItem ? anchorItem.width : 0
readonly property real anchorH: anchorItem ? anchorItem.height : 0
readonly property real screenW: screen ? screen.width : 0
readonly property real screenH: screen ? screen.height : 0
// Desired top-left of the card in screen coordinates. For the
// perpendicular axis (away-from-bar) we anchor to the bar window's edge
// directly — not the anchor item's y/x — because mapToItem(barContent)
// returns coordinates in the bar's content space, which can be offset
// from the bar surface's screen-anchored corner by internal layout
// (centering wrappers, padding). The bar's surface IS aligned to its
// anchored screen edge, so using `barW`/`barH` gives the right edge
// regardless of how the bar's internal widgets are positioned. For the
// parallel axis (along the bar) the anchor item's reported position is
// still consistent with the bar content origin, so it's accurate for
// centering the card under the icon.
readonly property real barW: anchorWindow ? anchorWindow.width : screenW
readonly property real barH: anchorWindow ? anchorWindow.height : 0
readonly property point cardOrigin: {
if (!anchorItem || !bar) return Qt.point(margin, margin)
var x = 0, y = 0
if (barPos === "bottom") {
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
y = screenH - barH - contentHeight - gap
} else if (barPos === "left") {
x = barW + gap
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
} else if (barPos === "right") {
x = screenW - barW - contentWidth - gap
y = anchorScreenPos.y + anchorH / 2 - contentHeight / 2
} else { // "top" (default)
x = anchorScreenPos.x + anchorW / 2 - contentWidth / 2
y = barH + gap
}
x = Math.max(margin, Math.min(x, screenW - contentWidth - margin))
y = Math.max(margin, Math.min(y, screenH - contentHeight - margin))
return Qt.point(Math.round(x), Math.round(y))
}
// --- popout coordination (same-bar single-popout model) -----------------
// Coordinate on `open`, not `visible`. `visible` lags into the fade-out
// animation, which made ownership transfer to a sibling popup race.
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
}
// --- outside-click dismissal --------------------------------------------
// Catches clicks anywhere in the clickable region (i.e. everywhere on
// screen except the bar strip, which is masked out). The card has its
// own MouseArea below so clicks on it don't bubble up here. Disabled
// during the fade-out so the dying overlay doesn't swallow clicks that
// were meant for the apps behind it.
MouseArea {
anchors.fill: parent
enabled: root.open
onClicked: root.closePopout()
}
// --- card ----------------------------------------------------------------
Rectangle {
id: card
x: root.cardOrigin.x
y: root.cardOrigin.y
width: root.contentWidth
height: root.contentHeight
color: Color.popups.background
border.color: Color.popups.border
border.width: 2
radius: Style.cornerRadius
opacity: root.open ? 1.0 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
// Swallow clicks on the card so they don't bubble to the dismissal
// MouseArea behind us.
MouseArea { anchors.fill: parent }
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Controls as QQC
import qs.Commons
Column {
id: root
property string label: ""
property int value: 0
property int from: 0
property int to: 100
property int stepSize: 1
property color foreground: Color.foreground
property color accent: Color.accent
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 12
property real fieldWidth: 120
property bool hasCursor: false
property alias field: spin
signal modified(int value)
signal hovered(bool on)
spacing: 6
Text {
visible: root.label !== ""
text: root.label
color: Qt.darker(root.foreground, 1.4)
font.family: root.fontFamily
font.pixelSize: 11
}
QQC.SpinBox {
id: spin
width: root.fieldWidth
from: root.from
to: root.to
stepSize: root.stepSize
value: root.value
editable: true
font.family: root.fontFamily
font.pixelSize: root.fontSize
onValueModified: root.modified(value)
background: Rectangle {
readonly property bool _hot: spin.activeFocus || (root.hasCursor && !spin.activeFocus)
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, _hot ? 0.10 : 0.05)
border.color: _hot
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.3)
border.width: _hot ? Style.focusBorderWidth : 1
radius: Style.cornerRadius
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}
contentItem: TextInput {
text: spin.displayText
font: spin.font
color: root.foreground
selectionColor: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.35)
selectedTextColor: root.foreground
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
readOnly: !spin.editable
validator: spin.validator
inputMethodHints: Qt.ImhFormattedNumbersOnly
}
}
}
+97
View File
@@ -0,0 +1,97 @@
import QtQuick
import qs.Commons
// Small (22×22 by default) icon button used at the right edge of panel rows
// for inline actions — forget network, confirm passphrase, unpair device,
// etc. Two visual modes are supported via `hoverColor`:
// - default: hoverColor === foreground → subtle foreground-tint hover
// - urgent: hoverColor === bar.urgent → red-tint hover for destructive
// actions like forget/unpair
//
// `enabled` gates clicks and dims the icon. The component owns its own
// hover state visuals; mouse hover does NOT update any panel cursor state
// here because action buttons are not cursor targets — the row they live
// in is.
//
// Set `focusable: true` to make the button keyboard-tabbable with an
// accent focus ring (Style.focusBorderColor / FillColor / Width). Use this
// in form contexts (the bar settings widget cards) where Tab walks a list
// of controls; leave it false for the right-edge actions on panel rows
// where the row's CursorSurface owns the keyboard cursor.
//
// Set `hasCursor: true` to have the button render the same fill as a
// mouse hover — so a panel's keyboard cursor lands on it identically.
// Use this when a PanelActionButton is itself the cursor target (rather
// than living inside a CursorSurface row). Emits `hovered(bool)` on
// pointer enter/leave so the panel can update its cursor state to match.
Rectangle {
id: root
property string iconText: ""
property string tooltipText: ""
property color foreground: "#cacccc"
property color hoverColor: foreground
property color panelBackground: "#101315"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 14
property real size: 22
property bool focusable: false
property bool hasCursor: false
signal clicked()
signal hovered(bool isHovered)
activeFocusOnTab: focusable
Keys.onReturnPressed: if (focusable) root.clicked()
Keys.onEnterPressed: if (focusable) root.clicked()
Keys.onSpacePressed: if (focusable) root.clicked()
implicitWidth: size
implicitHeight: size
radius: Style.cornerRadius
readonly property bool _showFocusRing: focusable && activeFocus
readonly property bool _hot: (mouse.containsMouse || root.hasCursor) && root.enabled
color: _showFocusRing
? Style.focusFillColor
: (_hot
? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20)
: "transparent")
border.width: _showFocusRing ? Style.focusBorderWidth : 0
border.color: _showFocusRing ? Style.focusBorderColor : "transparent"
Behavior on color { ColorAnimation { duration: 60 } }
Text {
anchors.centerIn: parent
text: root.iconText
color: root.enabled
? (root._hot ? root.hoverColor : Qt.darker(root.foreground, 1.3))
: Qt.darker(root.foreground, 2.0)
font.family: root.fontFamily
font.pixelSize: root.fontSize
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: root.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.enabled
onContainsMouseChanged: root.hovered(containsMouse)
onClicked: {
if (root.focusable) root.forceActiveFocus()
root.clicked()
}
}
PanelToolTip {
visible: root.tooltipText !== "" && mouse.containsMouse
text: root.tooltipText
panelForeground: root.foreground
panelBackground: root.panelBackground
fontFamily: root.fontFamily
}
}
+74
View File
@@ -0,0 +1,74 @@
import QtQuick
// Drop-in key dispatcher for keyboard-driven panels. Wraps panel content
// and emits semantic signals so each panel keeps its own state machine
// (focusSection, selectedIndex, activation rules) while the boilerplate
// key handling lives here.
//
// Usage:
// Common.KeyboardPanel {
// ...
// PanelKeyCatcher {
// anchors.fill: parent
// onMoveRequested: function(dx, dy) { root.moveCursor(dx, dy) }
// onActivateRequested: root.activateCursor()
// onCloseRequested: root.closePopout()
// onDeleteRequested: root.deleteSelected()
// onTextKey: function(t) { if (t === "r") root.refresh() }
//
// Column { ... panel content ... }
// }
// }
//
// Keys.priority: Keys.BeforeItem means this handler gets keys first,
// even when a descendant has activeFocus. That's what lets Up/Down
// arrows drive the cursor instead of being consumed by an inner
// Flickable's built-in scroll handling. When a panel has an inline
// editor (wifi passphrase, gallery TextField demo) the panel must
// set `blocked: editor.activeFocus` so this handler short-circuits
// and the editor receives keys normally.
//
// blocked: when true, ALL keys are forwarded to descendants without
// triggering signals.
Item {
id: root
property bool blocked: false
signal moveRequested(int dx, int dy)
signal activateRequested()
signal closeRequested()
signal deleteRequested()
signal textKey(string text)
focus: true
Keys.priority: Keys.BeforeItem
Keys.onPressed: function(event) {
if (blocked) return
if (event.key === Qt.Key_Escape) {
closeRequested(); event.accepted = true; return
}
if (event.key === Qt.Key_Down || event.text === "j") {
moveRequested(0, 1); event.accepted = true; return
}
if (event.key === Qt.Key_Up || event.text === "k") {
moveRequested(0, -1); event.accepted = true; return
}
if (event.key === Qt.Key_Right || event.text === "l") {
moveRequested(1, 0); event.accepted = true; return
}
if (event.key === Qt.Key_Left || event.text === "h") {
moveRequested(-1, 0); event.accepted = true; return
}
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
activateRequested(); event.accepted = true; return
}
if (event.text === "x" || event.text === "X") {
deleteRequested(); event.accepted = true; return
}
if (event.text && event.text.length === 1) {
textKey(event.text)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import QtQuick
// Small-caps-style label that introduces a panel section ("DNS provider",
// "Wi-Fi networks", "Output device", "Paired devices"). Sits between a
// PanelSeparator and the content rows.
Text {
id: root
property color foreground: "#cacccc"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 10
color: Qt.darker(foreground, 1.4)
font.family: fontFamily
font.pixelSize: fontSize
font.bold: true
}
+17
View File
@@ -0,0 +1,17 @@
import QtQuick
// 1px horizontal divider for panel sections. The alpha-on-foreground tint
// keeps the rule legible against the panel background without competing
// with text or borders.
Rectangle {
id: root
property color foreground: "#cacccc"
property real strength: 0.12
width: parent ? parent.width : implicitWidth
implicitWidth: 100
implicitHeight: 1
height: 1
color: Qt.rgba(foreground.r, foreground.g, foreground.b, strength)
}
+117
View File
@@ -0,0 +1,117 @@
import QtQuick
Item {
id: root
property QtObject bar: null
property real value: 0
property real minimum: 0
property real maximum: 1
property real step: 0.05
property bool integer: false
property color trackColor: bar ? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18) : "#333"
property color fillColor: bar ? bar.foreground : "#cacccc"
property color knobColor: bar ? bar.foreground : "#cacccc"
property bool dragging: false
property real trackHeight: 4
property real liveValue: value
onValueChanged: if (!dragging) liveValue = value
signal moved(real value)
signal released(real value)
implicitWidth: 200
implicitHeight: 22
readonly property real range: Math.max(0.0001, maximum - minimum)
readonly property real progress: Math.max(0, Math.min(1, (liveValue - minimum) / range))
Rectangle {
id: track
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
height: root.trackHeight
radius: height / 2
color: root.trackColor
}
Rectangle {
id: fill
anchors.verticalCenter: track.verticalCenter
anchors.left: track.left
height: track.height
radius: track.radius
color: root.fillColor
width: track.width * root.progress
Behavior on width {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
}
Rectangle {
id: knob
width: 14
height: 14
radius: 7
color: root.knobColor
border.color: root.bar ? root.bar.background : "#101315"
border.width: 2
anchors.verticalCenter: track.verticalCenter
x: Math.max(0, Math.min(track.width - width, track.width * root.progress - width / 2))
scale: mouseArea.containsMouse || root.dragging ? 1.15 : 1.0
Behavior on x {
enabled: !root.dragging
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Behavior on scale {
NumberAnimation { duration: 110; easing.type: Easing.OutCubic }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton
function valueFromX(x) {
var clamped = Math.max(0, Math.min(track.width, x))
var raw = root.minimum + (clamped / track.width) * root.range
if (root.integer) raw = Math.round(raw)
return Math.max(root.minimum, Math.min(root.maximum, raw))
}
onPressed: function(mouse) {
root.dragging = true
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onPositionChanged: function(mouse) {
if (!root.dragging) return
var next = valueFromX(mouse.x)
root.liveValue = next
root.moved(next)
}
onReleased: function(mouse) {
root.dragging = false
root.released(root.liveValue)
root.liveValue = root.value
}
onWheel: function(wheel) {
var delta = wheel.angleDelta.y > 0 ? root.step : -root.step
var next = Math.max(root.minimum, Math.min(root.maximum, root.liveValue + delta))
if (root.integer) next = Math.round(next)
root.liveValue = next
root.moved(next)
root.released(next)
}
}
}
+45
View File
@@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Controls
// Styled wrapper around Qt Quick Controls ToolTip. Drop-in: declare inside
// the hovered item and bind `visible` to the hover state, e.g.
// Common.PanelToolTip {
// visible: mouse.containsMouse
// text: "Forget network"
// panelForeground: bar.foreground
// panelBackground: bar.background
// fontFamily: bar.fontFamily
// }
//
// Property names are prefixed `panel*` to avoid clashing with ToolTip's
// built-in `background`/`font` properties.
ToolTip {
id: root
property color panelForeground: "#cacccc"
property color panelBackground: "#101315"
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 11
delay: 400
padding: 0
background: Rectangle {
color: root.panelBackground
border.color: root.panelForeground
border.width: 1
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.text
color: root.panelForeground
font.family: root.fontFamily
font.pixelSize: root.fontSize
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
}
}
+143
View File
@@ -0,0 +1,143 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
Rectangle {
id: root
property string text: ""
property string iconText: ""
property string tooltipText: ""
property color foreground: "#cacccc"
property color background: "transparent"
property color hoverBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
property color pressedBackground: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.22)
property color tooltipBackground: "#101315"
property color tooltipForeground: foreground
property string fontFamily: "JetBrainsMono Nerd Font"
property real fontSize: 12
property real iconSize: 14
property real horizontalPadding: 10
property real verticalPadding: 6
property bool active: 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
// as a mouse hover — so j/k navigation and pointer hover are visually
// indistinguishable. Default false; bind from a panel's cursor state.
property bool hasCursor: false
// Tab-focusable form-button mode. Enables activeFocusOnTab and
// Enter/Return/Space activation, and uses Style.focusBorderColor / FillColor
// for the focus ring (distinct from the panel-cursor `hot` state). Set
// 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 paints its own 1px border, so changing
// this doesn't affect panel-pill visuals.
property bool bordered: false
activeFocusOnTab: focusable
Keys.onReturnPressed: if (focusable) root.clicked()
Keys.onEnterPressed: if (focusable) root.clicked()
Keys.onSpacePressed: if (focusable) root.clicked()
ToolTip {
visible: root.tooltipText !== "" && mouseArea.containsMouse
text: root.tooltipText
delay: 400
padding: 0
background: Rectangle {
color: root.tooltipBackground
border.color: root.tooltipForeground
border.width: 1
radius: 0
opacity: 0.97
}
contentItem: Text {
text: root.tooltipText
color: root.tooltipForeground
font.family: root.fontFamily
font.pixelSize: 11
leftPadding: 10
rightPadding: 10
topPadding: 6
bottomPadding: 6
}
}
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
: hot ? 1
: (bordered ? 1 : 0)
border.color: _showFocusRing ? Style.focusBorderColor : foreground
Behavior on color {
ColorAnimation { duration: 120 }
}
Row {
id: row
anchors.verticalCenter: parent.verticalCenter
anchors.left: root.leftAlign ? parent.left : undefined
anchors.leftMargin: root.leftAlign ? root.horizontalPadding : 0
anchors.horizontalCenter: root.leftAlign ? undefined : parent.horizontalCenter
spacing: 8
Text {
visible: root.iconText !== ""
text: root.iconText
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.iconSize
anchors.verticalCenter: parent.verticalCenter
}
Text {
visible: root.text !== ""
text: root.text
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.fontSize
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (root.focusable) root.forceActiveFocus()
if (mouse.button === Qt.RightButton) root.rightClicked()
else root.clicked()
}
}
}
+140
View File
@@ -0,0 +1,140 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Commons
PopupWindow {
id: root
required property Item anchorItem
required property QtObject bar
property var owner: null
property int margin: 10
property int padding: 14
property int contentWidth: 280
property int contentHeight: 200
property color borderColor: Color.popups.border
property bool open: false
property bool centerOnBar: false
// "click" — uses HyprlandFocusGrab so clicking outside dismisses the popup.
// "hover" — passive overlay; the owning widget controls open via hover.
property string triggerMode: "click"
readonly property var coordinatorKey: owner || root
readonly property var anchorWindow: anchorItem ? anchorItem.QsWindow.window : null
readonly property bool containsMouse: cardHover.hovered
function closePopout() {
if (owner && "closePopout" in owner) owner.closePopout()
else root.open = false
}
default property alias contentItem: contentHolder.children
visible: open || card.opacity > 0
color: "transparent"
implicitWidth: contentWidth
implicitHeight: contentHeight
onOpenChanged: {
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
}
// Outside-click dismissal via Hyprland's focus grab. While `active`, input
// is routed only to the listed windows; clicking anywhere else clears the
// grab and we close the popup. Skipped for hover-mode popups so the cursor
// can move freely between the trigger and the popup.
HyprlandFocusGrab {
active: root.open && root.triggerMode === "click"
windows: root.anchorWindow ? [root, root.anchorWindow] : [root]
onCleared: root.closePopout()
}
anchor {
id: popupAnchor
window: anchorItem ? anchorItem.QsWindow.window : null
adjustment: PopupAdjustment.Slide
edges: Edges.Top | Edges.Left
gravity: Edges.Bottom | Edges.Right
rect.width: 1
rect.height: 1
onAnchoring: {
if (!root.anchorItem || !root.bar) return
var target = root.anchorItem
var popupWidth = root.implicitWidth
var popupHeight = root.implicitHeight
var localX = target.width / 2 - popupWidth / 2
var localY = target.height + root.margin
if (root.bar.position === "bottom") {
localY = -popupHeight - root.margin
} else if (root.bar.position === "left") {
localX = target.width + root.margin
localY = target.height / 2 - popupHeight / 2
} else if (root.bar.position === "right") {
localX = -popupWidth - root.margin
localY = target.height / 2 - popupHeight / 2
}
var window = target.QsWindow.window
if (!window) return
if (root.centerOnBar) {
var cx = 0;
var cy = 0;
if (root.bar.position === "top" || root.bar.position === "bottom") {
cx = window.width / 2 - popupWidth / 2
cy = root.bar.position === "bottom" ? -popupHeight - root.margin : window.height + root.margin
cx = Math.max(root.margin, Math.min(cx, window.width - popupWidth - root.margin))
} else {
cx = root.bar.position === "left" ? window.width + root.margin : -popupWidth - root.margin
cy = window.height / 2 - popupHeight / 2
cy = Math.max(root.margin, Math.min(cy, window.height - popupHeight - root.margin))
}
popupAnchor.rect.x = Math.round(cx)
popupAnchor.rect.y = Math.round(cy)
return
}
var point = window.contentItem.mapFromItem(target, localX, localY)
if (root.bar.position === "top" || root.bar.position === "bottom") {
point.x = Math.max(root.margin, Math.min(point.x, window.width - popupWidth - root.margin))
} else {
point.y = Math.max(root.margin, Math.min(point.y, window.height - popupHeight - root.margin))
}
popupAnchor.rect.x = Math.round(point.x)
popupAnchor.rect.y = Math.round(point.y)
}
}
Rectangle {
id: card
anchors.fill: parent
color: Color.popups.background
border.color: root.borderColor
border.width: 2
radius: Style.cornerRadius
opacity: root.open ? 1.0 : 0
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Item {
id: contentHolder
anchors.fill: parent
anchors.margins: root.padding
}
HoverHandler {
id: cardHover
}
}
}
+344
View File
@@ -0,0 +1,344 @@
import QtQuick
import QtQuick.Controls as QQC
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 string triggerLabel: ""
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
// Panel-cursor flag. When true, the trigger renders the same focus ring
// as Tab-focus so a panel's keyboard cursor lands here identically.
// Emits `hovered(bool)` on pointer enter/leave so the panel can keep
// its cursor state in sync with the mouse.
property bool hasCursor: false
// popupOpen + open()/close()/toggle() let a parent panel know when the
// dropdown owns keys (search field + result list are active) and
// suspend its own keyCatcher so typing into the filter doesn't drive
// the panel cursor.
readonly property bool popupOpen: popup.opened
function open() { popup.open() }
function close() { popup.close() }
function toggle() { popup.opened ? popup.close() : popup.open() }
signal changed(string value)
signal hovered(bool isHovered)
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
readonly property bool _focused: trigger.activeFocus || root.hasCursor
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
trigger._focused ? 0.08 : 0.04)
border.color: trigger._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.4)
border.width: trigger._focused ? Style.focusBorderWidth : 1
activeFocusOnTab: true
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
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.triggerLabel || root.placeholderText
color: (root.currentLabel() || root.triggerLabel) ? 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()
}
}
QQC.Popup {
id: popup
x: 0
y: trigger.height + 2
width: trigger.width
implicitHeight: Math.max(root.popupMinHeight,
Math.min(resultList.contentHeight + 50,
root.popupRowHeight * 6 + 5 * 4 + 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
spacing: 4
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.priority: Keys.BeforeItem
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: Math.max(root.popupRowHeight, rowContent.implicitHeight + 12)
color: index === resultList.currentIndex
? Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.14)
: "transparent"
Column {
id: rowContent
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 2
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()
}
}
}
}
}
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import QtQuick
import QtQuick.Controls
import qs.Commons
// Single-line text input with the kit's focus + selection styling. Inherits
// from Qt Quick Controls TextField so the underlying type's API (text,
// placeholderText, accepted, editingFinished, validator, ...) is available
// to callers without re-exposing each property.
//
// Defaults bind to qs.Commons.Color so a caller with no theme overrides
// just works; foreground / accent / selectionTint can be overridden per
// instance. Focus styling uses Style.focusBorderColor (the same accent
// ring Toggle and ChoiceButton paint) so keyboard cursor and form-focus
// chrome stay consistent across the shell.
//
// Sizing is driven by font.pixelSize + verticalPadding. The default 30px
// implicitHeight fits dialog forms; inline callers (wifi's row-embedded
// passphrase prompt) drop verticalPadding to match a 22-26px row.
TextField {
id: root
property color foreground: Color.foreground
property color accent: Color.accent
property color selectionTint: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.35)
property bool password: false
property real horizontalPadding: 10
property real verticalPadding: 7
// Panel-cursor flag. When true (and the field isn't already focused),
// the background paints the same accent ring as activeFocus so the
// panel's keyboard cursor lands here identically to a mouse hover.
// For mouse-enter/leave the consumer reads QQC TextField's inherited
// `hovered` property (via onHoveredChanged) — we don't add a sibling
// signal because the inherited property would shadow it.
property bool hasCursor: false
readonly property bool _focused: activeFocus || hasCursor
echoMode: password ? TextInput.Password : TextInput.Normal
color: foreground
selectionColor: selectionTint
selectedTextColor: foreground
placeholderTextColor: Qt.darker(foreground, 1.6)
leftPadding: horizontalPadding
rightPadding: horizontalPadding
topPadding: verticalPadding
bottomPadding: verticalPadding
background: Rectangle {
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b,
root._focused ? 0.08 : 0.04)
border.color: root._focused
? Style.focusBorderColor
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18)
border.width: root._focused ? Style.focusBorderWidth : 1
radius: Style.cornerRadius
}
}
+137
View File
@@ -0,0 +1,137 @@
import QtQuick
import qs.Commons
// Labeled toggle row: title + optional description on the left, a switch
// on the right. Clicking anywhere on the row emits `clicked()`; consumers
// flip `checked` in response (the component is stateless about the actual
// value so it composes cleanly with model-driven UI).
//
// 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
property string label: ""
property string description: ""
property bool checked: false
// Panel-cursor flag. Same role as PillButton.hasCursor / ChoiceButton.hasCursor:
// panels with their own keyboard cursor bind this to drive the highlight
// separately from activeFocus. Visuals match the activeFocus look (accent
// 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"
property real titleSize: 13
property real descriptionSize: 10
signal clicked()
signal hovered(bool isHovered)
activeFocusOnTab: true
Keys.onReturnPressed: root.clicked()
Keys.onEnterPressed: root.clicked()
Keys.onSpacePressed: root.clicked()
implicitHeight: Math.max(54, content.implicitHeight + 18)
implicitWidth: 240
radius: Style.cornerRadius
color: activeFocus || hasCursor
? Style.focusFillColor
: (mouse.containsMouse ? Style.hotFill : Qt.rgba(foreground.r, foreground.g, foreground.b, 0.03))
border.color: activeFocus || hasCursor
? Style.focusBorderColor
: Qt.rgba(foreground.r, foreground.g, foreground.b, 0.12)
border.width: activeFocus || hasCursor ? Style.focusBorderWidth : 1
Behavior on color { ColorAnimation { duration: 100 } }
Row {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 12
anchors.rightMargin: 12
spacing: 12
Column {
width: parent.width - track.width - parent.spacing
spacing: 3
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.label
color: root.foreground
font.family: root.fontFamily
font.pixelSize: root.titleSize
font.bold: true
elide: Text.ElideRight
width: parent.width
}
Text {
visible: root.description !== ""
text: root.description
color: Qt.darker(root.foreground, 1.5)
font.family: root.fontFamily
font.pixelSize: root.descriptionSize
wrapMode: Text.WordWrap
width: parent.width
}
}
Rectangle {
id: track
width: 42
height: 22
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)
border.color: root.checked
? root.accent
: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.28)
border.width: 1
anchors.verticalCenter: parent.verticalCenter
Behavior on color { ColorAnimation { duration: 120 } }
Rectangle {
width: 16
height: 16
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)
Behavior on x { NumberAnimation { duration: 120; easing.type: Easing.OutCubic } }
Behavior on color { ColorAnimation { duration: 120 } }
}
}
}
MouseArea {
id: mouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.clicked()
}
HoverHandler {
onHoveredChanged: root.hovered(hovered)
}
}
+69
View File
@@ -0,0 +1,69 @@
import QtQuick
Item {
id: root
property var bar: null
property string text: ""
property string fontFamily: bar ? bar.fontFamily : "JetBrainsMono Nerd Font"
property real fontSize: 12
property color foreground: bar ? bar.foreground : "#cacccc"
property color activeColor: bar ? bar.urgent : "#a55555"
property bool active: false
property real horizontalMargin: 8.5
property real rightExtraMargin: 0
property real verticalPadding: 6
property real fixedWidth: -1
property real fixedHeight: -1
property real textRotation: 0
property bool keepSpace: false
property string tooltipText: ""
signal pressed(int button)
signal wheelMoved(int delta)
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : 26
visible: text !== "" || keepSpace
opacity: text === "" ? 0 : 1
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + horizontalMargin * 2 + rightExtraMargin))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + verticalPadding * 2) : barSize)
Behavior on opacity {
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
}
Text {
id: label
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.vertical ? 0 : -root.rightExtraMargin / 2
text: root.text
color: root.active ? root.activeColor : root.foreground
font.family: root.fontFamily
font.pointSize: root.fontSize * 0.75
renderType: Text.NativeRendering
rotation: root.textRotation
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
Behavior on color {
ColorAnimation { duration: 160 }
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: if (root.bar) root.bar.showTooltip(root, root.tooltipText)
onExited: if (root.bar) root.bar.hideTooltip(root)
onClicked: function(mouse) {
if (root.bar) root.bar.hideTooltip(root)
root.pressed(mouse.button)
}
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
}
}
+20
View File
@@ -0,0 +1,20 @@
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
NumberField 1.0 NumberField.qml
PanelActionButton 1.0 PanelActionButton.qml
PanelKeyCatcher 1.0 PanelKeyCatcher.qml
PanelSectionHeader 1.0 PanelSectionHeader.qml
PanelSeparator 1.0 PanelSeparator.qml
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