Panel UI atoms, keyboard-driven audio + bluetooth, dev gallery
This commit is contained in:
@@ -19,6 +19,7 @@ User-installed plugins live alongside these conceptually but on disk under
|
||||
| Notifications | `omarchy.notifications` | `service` | persistent | `notifications/Service.qml` |
|
||||
| OSD | `omarchy.osd` | `panel` | persistent | `osd/Osd.qml` |
|
||||
| Polkit agent | `omarchy.polkit` | `service` | persistent | `polkit/PolkitAgent.qml` |
|
||||
| Dev gallery | `omarchy.dev-gallery` | `panel` | on-demand | `dev-gallery/GalleryPanel.qml` |
|
||||
|
||||
## Bar
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import QtQuick
|
||||
|
||||
// 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.
|
||||
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
|
||||
|
||||
signal clicked()
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
radius: 4
|
||||
|
||||
color: mouse.containsMouse && root.enabled
|
||||
? Qt.rgba(hoverColor.r, hoverColor.g, hoverColor.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 60 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.iconText
|
||||
color: root.enabled
|
||||
? (mouse.containsMouse ? 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
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
PanelToolTip {
|
||||
visible: root.tooltipText !== "" && mouse.containsMouse
|
||||
text: root.tooltipText
|
||||
panelForeground: root.foreground
|
||||
panelBackground: root.panelBackground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,11 @@ Rectangle {
|
||||
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
|
||||
|
||||
ToolTip {
|
||||
visible: root.tooltipText !== "" && mouseArea.containsMouse
|
||||
text: root.tooltipText
|
||||
@@ -54,7 +59,17 @@ Rectangle {
|
||||
implicitWidth: row.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: row.implicitHeight + verticalPadding * 2
|
||||
radius: 4
|
||||
color: mouseArea.pressed ? pressedBackground : (mouseArea.containsMouse ? hoverBackground : (active ? activeBackground : background))
|
||||
|
||||
// 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
|
||||
|
||||
color: mouseArea.pressed ? pressedBackground
|
||||
: hot ? hoverBackground
|
||||
: (active ? activeBackground : background)
|
||||
border.width: hot ? 1 : 0
|
||||
border.color: foreground
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 120 }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import "../common" as Common
|
||||
|
||||
@@ -82,6 +83,171 @@ Item {
|
||||
readonly property real inputVolume: source && source.audio ? source.audio.volume : 0
|
||||
readonly property bool inputMuted: source && source.audio ? source.audio.muted : false
|
||||
|
||||
// Single cursor model shared by keyboard and mouse. Sections:
|
||||
// "output" — output slider + sink device list
|
||||
// "input" — input slider + source device list
|
||||
// "streams" — per-app playback streams
|
||||
// selectedIndex semantics within a section:
|
||||
// -1 → on the slider row (h/l adjusts volume, m/Enter mute)
|
||||
// 0..N-1 → on the Nth device/stream row
|
||||
// Visuals derive from hasCursor/current via Common.CursorSurface, never
|
||||
// from containsMouse — that's what keeps the highlight unique across
|
||||
// keyboard + mouse like wifi does.
|
||||
property string focusSection: "output"
|
||||
property int selectedIndex: -1
|
||||
|
||||
readonly property color activeFill: bar
|
||||
? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18)
|
||||
: "transparent"
|
||||
|
||||
function sectionCount(section) {
|
||||
if (section === "output") return audioSinks.length
|
||||
if (section === "input") return audioSources.length
|
||||
if (section === "streams") return audioStreams.length
|
||||
return 0
|
||||
}
|
||||
|
||||
function sectionVisible(section) {
|
||||
if (section === "output") return true
|
||||
if (section === "input") return audioSources.length > 0 || !!source
|
||||
if (section === "streams") return audioStreams.length > 0
|
||||
return false
|
||||
}
|
||||
|
||||
function sectionHasSlider(section) {
|
||||
if (section === "output") return true
|
||||
if (section === "input") return !!source
|
||||
return false // stream rows carry their own sliders inline; not a section-level slider
|
||||
}
|
||||
|
||||
// Order of visible sections, recomputed reactively so dropping a section
|
||||
// (e.g. no input devices) doesn't leave the cursor pointing at it.
|
||||
readonly property var visibleSections: {
|
||||
var list = []
|
||||
if (sectionVisible("output")) list.push("output")
|
||||
if (sectionVisible("input")) list.push("input")
|
||||
if (sectionVisible("streams")) list.push("streams")
|
||||
return list
|
||||
}
|
||||
|
||||
function moveCursor(delta) {
|
||||
var sections = visibleSections
|
||||
if (sections.length === 0) return
|
||||
var sIdx = sections.indexOf(focusSection)
|
||||
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = sectionHasSlider(focusSection) ? -1 : 0; return }
|
||||
|
||||
var idx = selectedIndex
|
||||
var max = sectionCount(focusSection) - 1 // last device index
|
||||
var hasSlider = sectionHasSlider(focusSection)
|
||||
var floor = hasSlider ? -1 : 0 // -1 = slider row
|
||||
|
||||
if (delta > 0) {
|
||||
if (idx < max) { selectedIndex = idx + 1; return }
|
||||
// Fall through to next section.
|
||||
if (sIdx < sections.length - 1) {
|
||||
focusSection = sections[sIdx + 1]
|
||||
selectedIndex = sectionHasSlider(focusSection) ? -1 : 0
|
||||
}
|
||||
} else {
|
||||
if (idx > floor) { selectedIndex = idx - 1; return }
|
||||
// Escape upward.
|
||||
if (sIdx > 0) {
|
||||
focusSection = sections[sIdx - 1]
|
||||
var prevMax = sectionCount(focusSection) - 1
|
||||
selectedIndex = prevMax >= 0 ? prevMax : (sectionHasSlider(focusSection) ? -1 : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the slider associated with the focused section. Output and
|
||||
// input sliders are real volume controls; on stream rows h/l adjusts
|
||||
// that stream's volume (so keyboard parity with the inline slider).
|
||||
// For device rows (selectedIndex >= 0 in output/input) h/l is a no-op
|
||||
// — the cursor is on a discrete row, not on the slider, and silently
|
||||
// moving the global slider would surprise the user.
|
||||
function adjustVolume(delta) {
|
||||
if (focusSection === "output" && selectedIndex === -1) {
|
||||
setOutputVolume(outputVolume + delta)
|
||||
return
|
||||
}
|
||||
if (focusSection === "input" && selectedIndex === -1) {
|
||||
setInputVolume(inputVolume + delta)
|
||||
return
|
||||
}
|
||||
if (focusSection === "streams" && selectedIndex >= 0 && selectedIndex < audioStreams.length) {
|
||||
var s = audioStreams[selectedIndex]
|
||||
if (s && s.audio) s.audio.volume = Math.max(0, Math.min(1.5, s.audio.volume + delta))
|
||||
}
|
||||
}
|
||||
|
||||
// Enter/Space: activate whatever the cursor is on.
|
||||
function activateCursor() {
|
||||
if (focusSection === "output") {
|
||||
if (selectedIndex === -1) { toggleOutputMute(); return }
|
||||
var sink = audioSinks[selectedIndex]
|
||||
if (sink) setDefaultSink(sink)
|
||||
return
|
||||
}
|
||||
if (focusSection === "input") {
|
||||
if (selectedIndex === -1) { toggleInputMute(); return }
|
||||
var src = audioSources[selectedIndex]
|
||||
if (src) setDefaultSource(src)
|
||||
return
|
||||
}
|
||||
if (focusSection === "streams" && selectedIndex >= 0) {
|
||||
var st = audioStreams[selectedIndex]
|
||||
if (st && st.audio) st.audio.muted = !st.audio.muted
|
||||
}
|
||||
}
|
||||
|
||||
onPopupOpenChanged: {
|
||||
if (popupOpen) {
|
||||
focusSection = "output"
|
||||
selectedIndex = -1 // start on the output slider
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp / repair the cursor whenever any list refreshes underneath us.
|
||||
onAudioSinksChanged: clampCursor()
|
||||
onAudioSourcesChanged: clampCursor()
|
||||
onAudioStreamsChanged: clampCursor()
|
||||
|
||||
// Keep the keyboard-focused row inside the visible viewport of the
|
||||
// ScrollView. Each cursor target (slider rows, SinkRow, SourceRow,
|
||||
// StreamRow) calls this when it gains hasCursor. Without it, j/k can
|
||||
// walk the selection off-screen — wifi uses ListView.positionViewAtIndex
|
||||
// for this; we don't have that affordance with a multi-section Column.
|
||||
function ensureCursorVisible(item) {
|
||||
if (!item || !scrollArea) return
|
||||
var flick = scrollArea.contentItem
|
||||
if (!flick || flick.contentY === undefined) return
|
||||
var pt = item.mapToItem(flick.contentItem || flick, 0, 0)
|
||||
var top = pt.y
|
||||
var bottom = top + (item.height || 0)
|
||||
var viewTop = flick.contentY
|
||||
var viewBottom = viewTop + flick.height
|
||||
var margin = 6
|
||||
if (top < viewTop + margin) flick.contentY = Math.max(0, top - margin)
|
||||
else if (bottom > viewBottom - margin)
|
||||
flick.contentY = bottom + margin - flick.height
|
||||
}
|
||||
|
||||
function clampCursor() {
|
||||
var sections = visibleSections
|
||||
if (!sections || !sections.length) return
|
||||
if (sections.indexOf(focusSection) < 0) {
|
||||
focusSection = visibleSections[0]
|
||||
selectedIndex = sectionHasSlider(focusSection) ? -1 : 0
|
||||
return
|
||||
}
|
||||
var count = sectionCount(focusSection)
|
||||
var hasSlider = sectionHasSlider(focusSection)
|
||||
var floor = hasSlider ? -1 : 0
|
||||
if (selectedIndex > count - 1) selectedIndex = Math.max(floor, count - 1)
|
||||
if (selectedIndex < floor) selectedIndex = floor
|
||||
}
|
||||
|
||||
function outputIcon() {
|
||||
// Match the old Waybar pulseaudio glyph set. The Material Design speaker
|
||||
// icons render visually smaller in JetBrainsMono Nerd Font.
|
||||
@@ -169,6 +335,19 @@ Item {
|
||||
PwObjectTracker { objects: root.candidateSources }
|
||||
PwObjectTracker { objects: root.audioStreams }
|
||||
|
||||
// Lets a Hyprland keybind summon the panel without a click. Mirrors the
|
||||
// networkPanel IpcHandler pattern; KeyboardPanel grants Exclusive focus
|
||||
// at map-time so j/k/h/l work the moment the panel appears.
|
||||
IpcHandler {
|
||||
target: "audioPanel"
|
||||
function toggle(): void {
|
||||
if (root.popupOpen) root.closePopout()
|
||||
else root.popupOpen = true
|
||||
}
|
||||
function show(): void { if (!root.popupOpen) root.popupOpen = true }
|
||||
function hide(): void { root.closePopout() }
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
@@ -187,16 +366,59 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
Common.KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
open: root.popupOpen
|
||||
// Match the width/inset of the window below so the popup outline
|
||||
// overlays cleanly instead of bleeding past the underlying frame.
|
||||
contentWidth: 370
|
||||
contentHeight: Math.min(560, panelColumn.implicitHeight + 28)
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.priority: Keys.AfterItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.closePopout()
|
||||
event.accepted = true
|
||||
return
|
||||
}
|
||||
if (event.key === Qt.Key_Down || event.text === "j") {
|
||||
root.moveCursor(1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Up || event.text === "k") {
|
||||
root.moveCursor(-1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Right || event.text === "l") {
|
||||
root.adjustVolume(0.05); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Left || event.text === "h") {
|
||||
root.adjustVolume(-0.05); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
|
||||
root.activateCursor(); event.accepted = true; return
|
||||
}
|
||||
// 'm' mutes whatever the cursor is on: focused section's slider for
|
||||
// output/input, the focused stream for streams. Without the stream
|
||||
// branch, m would mute the global output while the cursor sat on a
|
||||
// per-app row — surprising and inconsistent with Enter behaviour.
|
||||
if (event.text === "m" || event.text === "M") {
|
||||
if (root.focusSection === "streams" && root.selectedIndex >= 0
|
||||
&& root.selectedIndex < root.audioStreams.length) {
|
||||
var s = root.audioStreams[root.selectedIndex]
|
||||
if (s && s.audio) s.audio.muted = !s.audio.muted
|
||||
} else if (root.focusSection === "input") {
|
||||
root.toggleInputMute()
|
||||
} else {
|
||||
root.toggleOutputMute()
|
||||
}
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
id: scrollArea
|
||||
anchors.fill: parent
|
||||
@@ -218,12 +440,11 @@ Item {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
text: "Output"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -238,8 +459,23 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
// Output slider row — itself a cursor target (selectedIndex === -1
|
||||
// when focusSection === "output"). h/l adjust the value via
|
||||
// root.adjustVolume; m / Enter toggle mute.
|
||||
Common.CursorSurface {
|
||||
id: outputSliderRow
|
||||
width: parent.width
|
||||
height: outputSliderInner.implicitHeight + 8
|
||||
hasCursor: root.focusSection === "output" && root.selectedIndex === -1
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(outputSliderRow)
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
|
||||
Row {
|
||||
id: outputSliderInner
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
@@ -288,72 +524,27 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "output"
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSinks
|
||||
|
||||
Rectangle {
|
||||
SinkRow {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool active: root.sink && modelData && root.sink.id === modelData.id
|
||||
|
||||
required property int index
|
||||
width: panelColumn.width
|
||||
height: deviceRow.implicitHeight + 10
|
||||
radius: 0
|
||||
color: deviceArea.pressed
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.22)
|
||||
: deviceArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
id: deviceRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sinkGlyph(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: active ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.setDefaultSink(modelData)
|
||||
}
|
||||
node: modelData
|
||||
rowIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -368,12 +559,11 @@ Item {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
text: "Input"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: 11
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
@@ -388,10 +578,22 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
Common.CursorSurface {
|
||||
id: inputSliderRow
|
||||
visible: !!root.source
|
||||
width: parent.width
|
||||
height: inputSliderInner.implicitHeight + 8
|
||||
hasCursor: root.focusSection === "input" && root.selectedIndex === -1
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(inputSliderRow)
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
|
||||
Row {
|
||||
id: inputSliderInner
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
id: inputIconText
|
||||
@@ -439,72 +641,27 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "input"
|
||||
root.selectedIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioSources
|
||||
|
||||
Rectangle {
|
||||
SourceRow {
|
||||
required property var modelData
|
||||
|
||||
readonly property bool active: root.source && modelData && root.source.id === modelData.id
|
||||
|
||||
required property int index
|
||||
width: panelColumn.width
|
||||
height: sourceRow.implicitHeight + 10
|
||||
radius: 0
|
||||
color: sourceArea.pressed
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.22)
|
||||
: sourceArea.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
: (active ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.18) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Row {
|
||||
id: sourceRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sourceGlyph(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(modelData)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: active ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sourceArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.setDefaultSource(modelData)
|
||||
}
|
||||
node: modelData
|
||||
rowIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,29 +672,191 @@ Item {
|
||||
spacing: 6
|
||||
visible: root.audioStreams.length > 0
|
||||
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
text: "Playing"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: 11
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.audioStreams
|
||||
|
||||
Item {
|
||||
StreamRow {
|
||||
required property var modelData
|
||||
|
||||
readonly property real streamVolume: modelData && modelData.audio ? modelData.audio.volume : 0
|
||||
readonly property bool streamMuted: modelData && modelData.audio ? modelData.audio.muted : false
|
||||
|
||||
required property int index
|
||||
width: panelColumn.width
|
||||
height: streamColumn.implicitHeight + 4
|
||||
node: modelData
|
||||
rowIndex: index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Reusable inline components ----
|
||||
|
||||
// Output device row — cursor target inside the "output" section. Mouse
|
||||
// hover updates the panel cursor at the root; visuals come entirely
|
||||
// from hasCursor/current via CursorSurface, never from containsMouse.
|
||||
component SinkRow: Common.CursorSurface {
|
||||
id: sinkRow
|
||||
required property var node
|
||||
required property int rowIndex
|
||||
|
||||
readonly property bool isActive: root.sink && node && root.sink.id === node.id
|
||||
hasCursor: root.focusSection === "output" && root.selectedIndex === rowIndex
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(sinkRow)
|
||||
current: isActive
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
implicitHeight: sinkInner.implicitHeight + 10
|
||||
|
||||
Row {
|
||||
id: sinkInner
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sinkGlyph(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(sinkRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: sinkRow.isActive ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "output"
|
||||
root.selectedIndex = sinkRow.rowIndex
|
||||
}
|
||||
onClicked: root.setDefaultSink(sinkRow.node)
|
||||
}
|
||||
}
|
||||
|
||||
// Input device row — sibling of SinkRow for the "input" section.
|
||||
component SourceRow: Common.CursorSurface {
|
||||
id: sourceRow
|
||||
required property var node
|
||||
required property int rowIndex
|
||||
|
||||
readonly property bool isActive: root.source && node && root.source.id === node.id
|
||||
hasCursor: root.focusSection === "input" && root.selectedIndex === rowIndex
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(sourceRow)
|
||||
current: isActive
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
implicitHeight: sourceInner.implicitHeight + 10
|
||||
|
||||
Row {
|
||||
id: sourceInner
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: root.sourceGlyph(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
width: 18
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.nodeLabel(sourceRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 18 - 14 - 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: sourceRow.isActive ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 13
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "input"
|
||||
root.selectedIndex = sourceRow.rowIndex
|
||||
}
|
||||
onClicked: root.setDefaultSource(sourceRow.node)
|
||||
}
|
||||
}
|
||||
|
||||
// Per-app stream row — cursor target inside the "streams" section.
|
||||
// The stream has its own slider inline, so h/l from the keyboard
|
||||
// adjusts THIS stream's volume (not the global output) when the cursor
|
||||
// sits on this row. Enter/Space mutes the stream.
|
||||
component StreamRow: Common.CursorSurface {
|
||||
id: streamRow
|
||||
required property var node
|
||||
required property int rowIndex
|
||||
|
||||
readonly property real streamVolume: node && node.audio ? node.audio.volume : 0
|
||||
readonly property bool streamMuted: node && node.audio ? node.audio.muted : false
|
||||
|
||||
hasCursor: root.focusSection === "streams" && root.selectedIndex === rowIndex
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(streamRow)
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
implicitHeight: streamColumn.implicitHeight + 8
|
||||
|
||||
Column {
|
||||
id: streamColumn
|
||||
width: parent.width
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
@@ -546,26 +865,27 @@ Item {
|
||||
|
||||
Text {
|
||||
id: streamMuteIcon
|
||||
text: streamMuted ? "" : ""
|
||||
text: streamRow.streamMuted ? "" : ""
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: 14
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: streamMuted ? 0.5 : 1.0
|
||||
opacity: streamRow.streamMuted ? 0.5 : 1.0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData && modelData.audio) modelData.audio.muted = !modelData.audio.muted
|
||||
if (streamRow.node && streamRow.node.audio)
|
||||
streamRow.node.audio.muted = !streamRow.node.audio.muted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.streamLabel(modelData)
|
||||
text: root.streamLabel(streamRow.node)
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
@@ -576,7 +896,7 @@ Item {
|
||||
|
||||
Text {
|
||||
id: streamPct
|
||||
text: Math.round(streamVolume * 100) + "%"
|
||||
text: Math.round(streamRow.streamVolume * 100) + "%"
|
||||
color: Qt.darker(root.bar.foreground, 1.5)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
@@ -592,17 +912,23 @@ Item {
|
||||
minimum: 0
|
||||
maximum: 1.5
|
||||
step: 0.05
|
||||
value: streamVolume
|
||||
opacity: streamMuted ? 0.5 : 1.0
|
||||
value: streamRow.streamVolume
|
||||
opacity: streamRow.streamMuted ? 0.5 : 1.0
|
||||
|
||||
onMoved: function(v) {
|
||||
if (modelData && modelData.audio) modelData.audio.volume = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if (streamRow.node && streamRow.node.audio) streamRow.node.audio.volume = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "streams"
|
||||
root.selectedIndex = streamRow.rowIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
import "../common" as Common
|
||||
|
||||
@@ -65,12 +66,216 @@ Item {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Single cursor model shared by keyboard and mouse. Sections:
|
||||
// "header" — 3 action pills (scan, tui, toggle); h/l moves between
|
||||
// them, Enter activates.
|
||||
// "known" — paired/known device rows; Enter toggles connect.
|
||||
// "discovered" — unpaired devices visible while scanning; Enter pairs.
|
||||
// Visuals always come from Common.CursorSurface (hasCursor / current),
|
||||
// never from containsMouse. Mouse hover updates root cursor state too,
|
||||
// guaranteeing one highlight on screen.
|
||||
property string focusSection: "header"
|
||||
property int selectedIndex: 2 // default = toggle pill
|
||||
readonly property int headerPillCount: 3
|
||||
|
||||
// Stable identity for the focused known device. The known list is sorted
|
||||
// (connected-first, then alphabetical) so activating a device can shift
|
||||
// its index. We track the BlueZ address here so the cursor follows the
|
||||
// same device across reorders rather than the slot it used to occupy.
|
||||
property string focusedKnownAddress: ""
|
||||
|
||||
readonly property color activeFill: bar
|
||||
? Qt.rgba(bar.foreground.r, bar.foreground.g, bar.foreground.b, 0.18)
|
||||
: "transparent"
|
||||
|
||||
function sectionCount(section) {
|
||||
if (section === "header") return headerPillCount
|
||||
if (section === "known") return knownDevices.length
|
||||
if (section === "discovered") return discoveredDevices.length
|
||||
return 0
|
||||
}
|
||||
|
||||
function sectionVisible(section) {
|
||||
if (section === "header") return true
|
||||
if (section === "known") return knownDevices.length > 0
|
||||
if (section === "discovered") return adapter && adapter.discovering && discoveredDevices.length > 0
|
||||
return false
|
||||
}
|
||||
|
||||
readonly property var visibleSections: {
|
||||
var list = ["header"]
|
||||
if (sectionVisible("known")) list.push("known")
|
||||
if (sectionVisible("discovered")) list.push("discovered")
|
||||
return list
|
||||
}
|
||||
|
||||
// j/k navigates between sections row-by-row. The header is treated as a
|
||||
// SINGLE row (its pills sit on one horizontal line), so j/k from devices
|
||||
// jumps to/from the header as a unit, and h/l moves between the three
|
||||
// pills inside it. This matches wifi's DNS-pill behaviour.
|
||||
function moveCursor(delta) {
|
||||
var sections = visibleSections
|
||||
if (!sections || sections.length === 0) return
|
||||
var sIdx = sections.indexOf(focusSection)
|
||||
if (sIdx < 0) { focusSection = sections[0]; selectedIndex = 0; return }
|
||||
|
||||
var idx = selectedIndex
|
||||
var inHeader = focusSection === "header"
|
||||
var max = inHeader ? 0 : sectionCount(focusSection) - 1
|
||||
|
||||
if (delta > 0) {
|
||||
if (!inHeader && idx < max) { selectedIndex = idx + 1; return }
|
||||
if (sIdx < sections.length - 1) {
|
||||
focusSection = sections[sIdx + 1]
|
||||
// Entering the header from below shouldn't happen (header is first),
|
||||
// but other entries start at 0.
|
||||
selectedIndex = 0
|
||||
}
|
||||
} else {
|
||||
if (!inHeader && idx > 0) { selectedIndex = idx - 1; return }
|
||||
if (sIdx > 0) {
|
||||
focusSection = sections[sIdx - 1]
|
||||
// Entering the header always lands on the toggle pill — the most
|
||||
// common action and consistent with the on-open default. h/l from
|
||||
// there moves to scan/TUI.
|
||||
selectedIndex = focusSection === "header" ? 2 : sectionCount(focusSection) - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// h/l: only meaningful in the header. In device sections it's a no-op
|
||||
// — j/k is the canonical row navigator there.
|
||||
function moveCursorH(delta) {
|
||||
if (focusSection !== "header") return
|
||||
var next = selectedIndex + delta
|
||||
if (next < 0) next = 0
|
||||
if (next > headerPillCount - 1) next = headerPillCount - 1
|
||||
selectedIndex = next
|
||||
}
|
||||
|
||||
function activateCursor() {
|
||||
if (focusSection === "header") {
|
||||
if (selectedIndex === 0) {
|
||||
if (adapter && adapter.enabled) adapter.discovering = !adapter.discovering
|
||||
} else if (selectedIndex === 1) {
|
||||
if (bar) bar.run("omarchy-launch-bluetooth")
|
||||
closePopout()
|
||||
} else if (selectedIndex === 2) {
|
||||
if (adapter) adapter.enabled = !adapter.enabled
|
||||
}
|
||||
return
|
||||
}
|
||||
if (focusSection === "known") {
|
||||
var dev = knownDevices[selectedIndex]
|
||||
if (!dev) return
|
||||
if (!dev.trusted) dev.trusted = true
|
||||
if (dev.connected) dev.disconnect()
|
||||
else dev.connect()
|
||||
return
|
||||
}
|
||||
if (focusSection === "discovered") {
|
||||
var d = discoveredDevices[selectedIndex]
|
||||
if (!d) return
|
||||
pendingPairAddresses[d.address] = true
|
||||
d.pair()
|
||||
}
|
||||
}
|
||||
|
||||
// 'x' on a known row mirrors the row's X button: connected device
|
||||
// disconnects, everything else forgets the pairing. Mismatching this
|
||||
// (e.g. forgetting a connected device) is destructive — the X button
|
||||
// tooltip says "Disconnect" for connected rows, and the keybind has
|
||||
// to agree.
|
||||
function deleteSelected() {
|
||||
if (focusSection !== "known") return
|
||||
var dev = knownDevices[selectedIndex]
|
||||
if (!dev) return
|
||||
if (dev.connected) dev.disconnect()
|
||||
else if (dev.forget) dev.forget()
|
||||
}
|
||||
|
||||
onPopupOpenChanged: {
|
||||
if (popupOpen) {
|
||||
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
|
||||
else { focusSection = "header"; selectedIndex = 2 }
|
||||
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
|
||||
}
|
||||
}
|
||||
|
||||
// When `selectedIndex` changes inside the known section, remember which
|
||||
// address it points at. Updates from re-resolution (below) are idempotent
|
||||
// because we end up setting the same address.
|
||||
onSelectedIndexChanged: {
|
||||
if (focusSection !== "known") return
|
||||
if (selectedIndex < 0 || selectedIndex >= knownDevices.length) return
|
||||
var d = knownDevices[selectedIndex]
|
||||
focusedKnownAddress = d ? (d.address || "") : ""
|
||||
}
|
||||
|
||||
onFocusSectionChanged: {
|
||||
if (focusSection !== "known") focusedKnownAddress = ""
|
||||
}
|
||||
|
||||
onKnownDevicesChanged: {
|
||||
// Try to follow the device by address before clamping. If we can't find
|
||||
// the address (e.g. it was forgotten), fall through to clampCursor()
|
||||
// which will pull selectedIndex back into range.
|
||||
if (focusSection === "known" && focusedKnownAddress !== "") {
|
||||
for (var i = 0; i < knownDevices.length; i++) {
|
||||
if (knownDevices[i] && knownDevices[i].address === focusedKnownAddress) {
|
||||
if (selectedIndex !== i) selectedIndex = i
|
||||
clampCursor()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
clampCursor()
|
||||
}
|
||||
onDiscoveredDevicesChanged: clampCursor()
|
||||
onVisibleSectionsChanged: clampCursor()
|
||||
|
||||
// Keep the keyboard-focused row inside the visible viewport of the device
|
||||
// Flickable. Each DeviceRow calls this when it gains hasCursor. Without
|
||||
// it, j/k can walk the selection off-screen in a long device list.
|
||||
function ensureCursorVisible(item) {
|
||||
if (!item || !deviceFlick) return
|
||||
var pt = item.mapToItem(deviceFlick.contentItem, 0, 0)
|
||||
var top = pt.y
|
||||
var bottom = top + (item.height || 0)
|
||||
var viewTop = deviceFlick.contentY
|
||||
var viewBottom = viewTop + deviceFlick.height
|
||||
var margin = 6
|
||||
if (top < viewTop + margin) deviceFlick.contentY = Math.max(0, top - margin)
|
||||
else if (bottom > viewBottom - margin)
|
||||
deviceFlick.contentY = bottom + margin - deviceFlick.height
|
||||
}
|
||||
|
||||
function clampCursor() {
|
||||
var sections = visibleSections
|
||||
if (!sections || !sections.length) return
|
||||
if (sections.indexOf(focusSection) < 0) {
|
||||
focusSection = sections[0]
|
||||
selectedIndex = 0
|
||||
return
|
||||
}
|
||||
var count = sectionCount(focusSection)
|
||||
if (count === 0) {
|
||||
// Section emptied out — bounce to the previous visible one.
|
||||
var sIdx = sections.indexOf(focusSection)
|
||||
focusSection = sIdx > 0 ? sections[sIdx - 1] : sections[0]
|
||||
selectedIndex = Math.max(0, sectionCount(focusSection) - 1)
|
||||
return
|
||||
}
|
||||
if (selectedIndex > count - 1) selectedIndex = count - 1
|
||||
if (selectedIndex < 0) selectedIndex = 0
|
||||
}
|
||||
|
||||
visible: adapter !== null
|
||||
implicitWidth: button.implicitWidth
|
||||
implicitHeight: button.implicitHeight
|
||||
|
||||
// Non-visual lifecycle watchers, one per device. Survives popup open/close
|
||||
// and the discovered↔known transition that destroys row delegates.
|
||||
// and the discovered-known transition that destroys row delegates.
|
||||
Repeater {
|
||||
model: root.devices
|
||||
Item {
|
||||
@@ -94,6 +299,17 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Lets a Hyprland keybind summon the panel without a click.
|
||||
IpcHandler {
|
||||
target: "bluetoothPanel"
|
||||
function toggle(): void {
|
||||
if (root.popupOpen) root.closePopout()
|
||||
else root.popupOpen = true
|
||||
}
|
||||
function show(): void { if (!root.popupOpen) root.popupOpen = true }
|
||||
function hide(): void { root.closePopout() }
|
||||
}
|
||||
|
||||
Common.WidgetButton {
|
||||
id: button
|
||||
anchors.fill: parent
|
||||
@@ -106,7 +322,8 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Common.PopupCard {
|
||||
Common.KeyboardPanel {
|
||||
id: panel
|
||||
anchorItem: button
|
||||
owner: root
|
||||
bar: root.bar
|
||||
@@ -114,12 +331,41 @@ Item {
|
||||
contentWidth: 320
|
||||
contentHeight: column.implicitHeight + 28
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
Keys.priority: Keys.AfterItem
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.closePopout(); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Down || event.text === "j") {
|
||||
root.moveCursor(1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Up || event.text === "k") {
|
||||
root.moveCursor(-1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Right || event.text === "l") {
|
||||
root.moveCursorH(1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Left || event.text === "h") {
|
||||
root.moveCursorH(-1); event.accepted = true; return
|
||||
}
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_Space) {
|
||||
root.activateCursor(); event.accepted = true; return
|
||||
}
|
||||
if (event.text === "x" || event.text === "X") {
|
||||
root.deleteSelected(); event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: column
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// Header: title left, on/off toggle right.
|
||||
// Header: title left, on/off toggle + actions right.
|
||||
Item {
|
||||
width: parent.width
|
||||
height: titleText.implicitHeight
|
||||
@@ -140,45 +386,29 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Common.PillButton {
|
||||
HeaderPill {
|
||||
pillIndex: 0
|
||||
iconText: ""
|
||||
tooltipText: !root.adapter ? "" : !root.adapter.enabled ? "Bluetooth is off"
|
||||
: root.adapter.discovering ? "Stop scanning" : "Scan for devices"
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 6
|
||||
verticalPadding: 4
|
||||
iconSize: 14
|
||||
enabled: root.adapter !== null && root.adapter.enabled
|
||||
opacity: enabled ? 1 : 0.4
|
||||
pillEnabled: root.adapter !== null && root.adapter.enabled
|
||||
active: root.adapter && root.adapter.discovering
|
||||
onClicked: if (root.adapter) root.adapter.discovering = !root.adapter.discovering
|
||||
onActivated: if (root.adapter) root.adapter.discovering = !root.adapter.discovering
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
HeaderPill {
|
||||
pillIndex: 1
|
||||
iconText: ""
|
||||
tooltipText: "Open Impala (TUI)"
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 6
|
||||
verticalPadding: 4
|
||||
iconSize: 14
|
||||
onClicked: { root.bar.run("omarchy-launch-bluetooth"); root.popupOpen = false }
|
||||
onActivated: { root.bar.run("omarchy-launch-bluetooth"); root.popupOpen = false }
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
HeaderPill {
|
||||
pillIndex: 2
|
||||
iconText: root.adapter && root.adapter.enabled ? "" : ""
|
||||
text: root.adapter && root.adapter.enabled ? "On" : "Off"
|
||||
tooltipText: root.adapter && root.adapter.enabled ? "Turn Bluetooth off" : "Turn Bluetooth on"
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 4
|
||||
active: root.adapter && root.adapter.enabled
|
||||
onClicked: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
|
||||
onActivated: if (root.adapter) root.adapter.enabled = !root.adapter.enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +416,7 @@ Item {
|
||||
// Scrollable device list — capped so a noisy neighborhood doesn't
|
||||
// grow the popup past the screen.
|
||||
Flickable {
|
||||
id: deviceFlick
|
||||
width: parent.width
|
||||
height: Math.min(deviceList.implicitHeight, 400)
|
||||
contentWidth: width
|
||||
@@ -205,28 +436,30 @@ Item {
|
||||
model: root.knownDevices
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: deviceList.width
|
||||
dev: modelData
|
||||
rowIndex: index
|
||||
isDiscovered: false
|
||||
}
|
||||
}
|
||||
|
||||
// Discovered (unpaired) devices, only shown while scanning.
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
visible: root.adapter && root.adapter.discovering && root.discoveredDevices.length > 0
|
||||
text: "Discovered"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.adapter && root.adapter.discovering ? root.discoveredDevices : []
|
||||
DeviceRow {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: deviceList.width
|
||||
dev: modelData
|
||||
rowIndex: index
|
||||
isDiscovered: true
|
||||
}
|
||||
}
|
||||
@@ -248,17 +481,68 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two-line device row showing name + live status (Connected, Connecting…,
|
||||
// Pairing…, Failed). Tracks pending click attempts with a Timer so a
|
||||
// Header pill — wraps Common.PillButton with cursor-state binding so it
|
||||
// participates in the same single-cursor model as device rows. We don't
|
||||
// use Common.CursorSurface here because the pill already provides its
|
||||
// own active/hover visuals and we want them to layer correctly.
|
||||
component HeaderPill: Common.PillButton {
|
||||
id: pill
|
||||
required property int pillIndex
|
||||
property bool pillEnabled: true
|
||||
signal activated()
|
||||
|
||||
tooltipBackground: root.bar.background
|
||||
tooltipForeground: root.bar.foreground
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
horizontalPadding: 6
|
||||
verticalPadding: 4
|
||||
iconSize: 14
|
||||
enabled: pillEnabled
|
||||
opacity: pillEnabled ? 1 : 0.4
|
||||
|
||||
// Hand off to PillButton's built-in cursor visuals so keyboard cursor and
|
||||
// mouse hover render identically (fill + 1px border).
|
||||
hasCursor: root.focusSection === "header" && root.selectedIndex === pillIndex
|
||||
|
||||
onClicked: pill.activated()
|
||||
|
||||
// Mouse hover updates root cursor state so keyboard + mouse share one
|
||||
// selection model.
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = "header"
|
||||
root.selectedIndex = pill.pillIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Two-line device row showing name + live status (Connected, Connecting,
|
||||
// Pairing, Failed). Tracks pending click attempts with a Timer so a
|
||||
// connect that drops back to Disconnected within 10s surfaces as "Failed".
|
||||
component DeviceRow: Rectangle {
|
||||
// Now a cursor target: hasCursor binds to root state, mouse hover updates
|
||||
// root state. The X button on the right is a PanelActionButton.
|
||||
component DeviceRow: Common.CursorSurface {
|
||||
id: row
|
||||
required property var dev
|
||||
required property int rowIndex
|
||||
required property bool isDiscovered
|
||||
|
||||
readonly property bool isConnected: dev && dev.connected
|
||||
readonly property int devState: dev && dev.state !== undefined ? dev.state : -1
|
||||
readonly property string sectionName: isDiscovered ? "discovered" : "known"
|
||||
|
||||
hasCursor: root.focusSection === sectionName && root.selectedIndex === rowIndex
|
||||
onHasCursorChanged: if (hasCursor) root.ensureCursorVisible(row)
|
||||
current: isConnected
|
||||
foreground: root.bar.foreground
|
||||
fill: root.activeFill
|
||||
|
||||
// 0 idle, 1 connecting, 2 disconnecting, 3 pairing, 4 failed.
|
||||
property int pendingAction: 0
|
||||
@@ -300,9 +584,9 @@ Item {
|
||||
readonly property string statusText: {
|
||||
if (!dev) return ""
|
||||
if (pendingAction === 4) return failureReason || "Failed"
|
||||
if (pendingAction === 1 || devState === 3) return "Connecting\u2026"
|
||||
if (pendingAction === 2 || devState === 2) return "Disconnecting\u2026"
|
||||
if (pendingAction === 3 || (dev.pairing === true)) return "Pairing\u2026"
|
||||
if (pendingAction === 1 || devState === 3) return "Connecting…"
|
||||
if (pendingAction === 2 || devState === 2) return "Disconnecting…"
|
||||
if (pendingAction === 3 || (dev.pairing === true)) return "Pairing…"
|
||||
if (isConnected) {
|
||||
if (dev.batteryAvailable) return "Connected · " + Math.round(dev.battery * 100) + "%"
|
||||
return "Connected"
|
||||
@@ -319,12 +603,6 @@ Item {
|
||||
}
|
||||
|
||||
implicitHeight: rowContent.implicitHeight + 12
|
||||
radius: 4
|
||||
color: rowMouse.containsMouse
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.10)
|
||||
: (isConnected ? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.05) : "transparent")
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
MouseArea {
|
||||
id: rowMouse
|
||||
@@ -333,6 +611,11 @@ Item {
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: row.dev ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
|
||||
onContainsMouseChanged: if (containsMouse) {
|
||||
root.focusSection = row.sectionName
|
||||
root.selectedIndex = row.rowIndex
|
||||
}
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (!row.dev) return
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
@@ -363,10 +646,10 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
implicitHeight: Math.max(icon.implicitHeight, info.implicitHeight, disconnectBtn.implicitHeight)
|
||||
implicitHeight: Math.max(deviceIcon.implicitHeight, info.implicitHeight, disconnectBtn.implicitHeight)
|
||||
|
||||
Text {
|
||||
id: icon
|
||||
id: deviceIcon
|
||||
text: row.isConnected ? "" : ""
|
||||
color: row.statusColor
|
||||
font.family: root.bar.fontFamily
|
||||
@@ -377,36 +660,17 @@ Item {
|
||||
|
||||
// Explicit close button on any known device. Action depends on state:
|
||||
// connected -> disconnect, otherwise -> forget the pairing entirely.
|
||||
Rectangle {
|
||||
Common.PanelActionButton {
|
||||
id: disconnectBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
visible: !row.isDiscovered
|
||||
readonly property string action: row.isConnected ? "Disconnect" : "Forget"
|
||||
color: disconnectMouse.containsMouse
|
||||
? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, root.bar.urgent.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 120 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: disconnectMouse.containsMouse ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: disconnectMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
iconText: ""
|
||||
tooltipText: row.isConnected ? "Disconnect" : "Forget"
|
||||
foreground: root.bar.foreground
|
||||
hoverColor: root.bar.urgent
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
onClicked: {
|
||||
if (!row.dev) return
|
||||
if (row.isConnected) {
|
||||
@@ -419,35 +683,10 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
visible: disconnectMouse.containsMouse
|
||||
text: disconnectBtn.action
|
||||
delay: 400
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
contentItem: Text {
|
||||
text: disconnectBtn.action
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: info
|
||||
spacing: 1
|
||||
anchors.left: icon.right
|
||||
anchors.left: deviceIcon.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: disconnectBtn.visible ? disconnectBtn.left : parent.right
|
||||
anchors.rightMargin: disconnectBtn.visible ? 8 : 0
|
||||
@@ -472,6 +711,5 @@ Item {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,11 +681,9 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Common.PanelSeparator {
|
||||
visible: !!root.info.iface
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
// Connection details: IP, gateway, link speed, etc.
|
||||
@@ -778,22 +776,18 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
|
||||
// DNS provider selection.
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
Common.PanelSeparator {
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
text: "DNS provider"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
Row {
|
||||
@@ -831,20 +825,16 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
}
|
||||
|
||||
// Wi-Fi networks (only if a Wi-Fi station is available).
|
||||
Rectangle {
|
||||
Common.PanelSeparator {
|
||||
visible: root.wifiStationAvailable
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.12)
|
||||
foreground: root.bar.foreground
|
||||
}
|
||||
|
||||
Text {
|
||||
Common.PanelSectionHeader {
|
||||
visible: root.wifiStationAvailable
|
||||
text: root.scanning ? "Scanning Wi-Fi…" : "Wi-Fi networks"
|
||||
color: Qt.darker(root.bar.foreground, 1.4)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
|
||||
// Scrollable network list — cap the height so a busy neighbourhood
|
||||
@@ -931,30 +921,12 @@ iwctl station "$station" get-networks rssi-dbms 2>/dev/null \\
|
||||
onClicked: pill.clicked()
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
Common.PanelToolTip {
|
||||
visible: pill.tooltipText !== "" && pillMouse.containsMouse
|
||||
text: pill.tooltipText
|
||||
delay: 400
|
||||
padding: 0
|
||||
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
text: pill.tooltipText
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
panelForeground: root.bar.foreground
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,62 +1042,21 @@ iwctl known-networks list 2>/dev/null \\
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Common.PanelActionButton {
|
||||
id: forgetBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
visible: row.isConnected
|
||||
color: forgetMouse.containsMouse
|
||||
? Qt.rgba(root.bar.urgent.r, root.bar.urgent.g, root.bar.urgent.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 60 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: forgetMouse.containsMouse ? root.bar.urgent : Qt.darker(root.bar.foreground, 1.3)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !root.busy
|
||||
iconText: ""
|
||||
tooltipText: "Forget network"
|
||||
foreground: root.bar.foreground
|
||||
hoverColor: root.bar.urgent
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
onClicked: if (row.net) root.forget(row.net.ssid)
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
visible: forgetMouse.containsMouse
|
||||
text: "Forget network"
|
||||
delay: 400
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
contentItem: Text {
|
||||
text: "Forget network"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shows a lock glyph on the right for protected networks that
|
||||
// aren't currently connected. Once connected, the forget X takes
|
||||
// its place (and 'protected' is implied by the fact we're on it).
|
||||
@@ -1236,61 +1167,17 @@ iwctl known-networks list 2>/dev/null \\
|
||||
// 22×22 right-anchored to line up with forgetBtn and lockIndicator
|
||||
// above. Esc closes the prompt (handled by pwField.Keys.onEscapePressed)
|
||||
// so there's no separate cancel button.
|
||||
Rectangle {
|
||||
Common.PanelActionButton {
|
||||
id: connectPwBtn
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 4
|
||||
property bool clickEnabled: !root.busy && row.net && pwField.text.length > 0
|
||||
color: connectPwMouse.containsMouse && connectPwBtn.clickEnabled
|
||||
? Qt.rgba(root.bar.foreground.r, root.bar.foreground.g, root.bar.foreground.b, 0.20)
|
||||
: "transparent"
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 60 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: connectPwBtn.clickEnabled
|
||||
? (connectPwMouse.containsMouse ? root.bar.foreground : Qt.darker(root.bar.foreground, 1.3))
|
||||
: Qt.darker(root.bar.foreground, 2.0)
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectPwMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: connectPwBtn.clickEnabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (connectPwBtn.clickEnabled) root.connectWithPassphrase(row.net.ssid, pwField.text)
|
||||
}
|
||||
|
||||
ToolTip {
|
||||
visible: connectPwMouse.containsMouse
|
||||
text: "Connect"
|
||||
delay: 400
|
||||
padding: 0
|
||||
background: Rectangle {
|
||||
color: root.bar.background
|
||||
border.color: root.bar.foreground
|
||||
border.width: 1
|
||||
radius: 0
|
||||
opacity: 0.97
|
||||
}
|
||||
contentItem: Text {
|
||||
text: "Connect"
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: 11
|
||||
leftPadding: 10
|
||||
rightPadding: 10
|
||||
topPadding: 6
|
||||
bottomPadding: 6
|
||||
}
|
||||
}
|
||||
enabled: !root.busy && row.net && pwField.text.length > 0
|
||||
iconText: ""
|
||||
tooltipText: "Connect"
|
||||
foreground: root.bar.foreground
|
||||
panelBackground: root.bar.background
|
||||
fontFamily: root.bar.fontFamily
|
||||
onClicked: if (row.net) root.connectWithPassphrase(row.net.ssid, pwField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import "../bar/common" as Common
|
||||
import qs.Commons
|
||||
|
||||
// Visual reference + live playground for omarchy-shell's common UI
|
||||
// components. Summoned via:
|
||||
// omarchy-shell-ipc shell summon omarchy.dev-gallery "{}"
|
||||
//
|
||||
// Every section here renders the REAL component (not a copy) so the
|
||||
// gallery doubles as a smoke test. When you add a new common component,
|
||||
// add a section here. Maintenance discipline: this file should ONLY use
|
||||
// imported common components, never inline reimplementations of them.
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// ---- plugin lifecycle ---------------------------------------------------
|
||||
property bool closingFromHost: false
|
||||
|
||||
function open(payloadJson) {
|
||||
closingFromHost = false
|
||||
window.visible = true
|
||||
Qt.callLater(function() { if (focusSink) focusSink.forceActiveFocus() })
|
||||
}
|
||||
|
||||
// Host-initiated close (`shell hide`). Visibility flips without
|
||||
// notifying the host back — it already knows.
|
||||
function close() {
|
||||
closingFromHost = true
|
||||
window.visible = false
|
||||
closingFromHost = false
|
||||
}
|
||||
|
||||
// User-initiated close (Esc, window close button). Tell the shell so its
|
||||
// openPanelIds map stays consistent and `toggle` works on the next call.
|
||||
function requestClose() {
|
||||
if (shell && typeof shell.hide === "function") shell.hide("omarchy.dev-gallery")
|
||||
else window.visible = false
|
||||
}
|
||||
|
||||
// ---- host injections ----------------------------------------------------
|
||||
property var barWidgetRegistry: null
|
||||
property var pluginRegistry: null
|
||||
property var shell: null
|
||||
property var manifest: null
|
||||
|
||||
// ---- theme --------------------------------------------------------------
|
||||
readonly property color foreground: Color.foreground
|
||||
readonly property color background: Color.background
|
||||
readonly property color accent: Color.accent
|
||||
readonly property color urgent: Color.urgent
|
||||
readonly property string fontFamily: "monospace"
|
||||
|
||||
// Fake `bar` for components that take a whole bar object (e.g. Slider).
|
||||
readonly property var fakeBar: QtObject {
|
||||
readonly property color foreground: root.foreground
|
||||
readonly property color background: root.background
|
||||
readonly property color urgent: root.urgent
|
||||
readonly property string fontFamily: root.fontFamily
|
||||
readonly property string position: "top"
|
||||
readonly property bool vertical: false
|
||||
readonly property int barSize: 26
|
||||
}
|
||||
|
||||
// ---- cursor demo state --------------------------------------------------
|
||||
property int cursorDemoIndex: 1
|
||||
|
||||
FloatingWindow {
|
||||
id: window
|
||||
title: "Omarchy shell – dev gallery"
|
||||
color: root.background
|
||||
implicitWidth: 720
|
||||
implicitHeight: 760
|
||||
minimumSize: Qt.size(560, 520)
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.closingFromHost && root.shell && typeof root.shell.hide === "function")
|
||||
root.shell.hide("omarchy.dev-gallery")
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
id: focusScope
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Item {
|
||||
id: focusSink
|
||||
width: 1; height: 1
|
||||
focus: true
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) { root.requestClose(); event.accepted = true }
|
||||
if (event.key === Qt.Key_Right || event.text === "l") {
|
||||
root.cursorDemoIndex = Math.min(2, root.cursorDemoIndex + 1)
|
||||
event.accepted = true
|
||||
}
|
||||
if (event.key === Qt.Key_Left || event.text === "h") {
|
||||
root.cursorDemoIndex = Math.max(0, root.cursorDemoIndex - 1)
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 18
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: parent.width - 24
|
||||
spacing: 22
|
||||
|
||||
// ---- Header ------------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "Omarchy shell · dev gallery"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Live previews of every component in plugins/bar/common/. Use this as the visual reference when porting panels or extracting new primitives. Press h/l to walk the cursor demo, Esc to close."
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 11
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Common.PanelSeparator { foreground: root.foreground }
|
||||
|
||||
// ---- PanelSectionHeader ------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PanelSectionHeader"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Small-caps-style intro label for a section."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: shCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: shCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Common.PanelSectionHeader {
|
||||
text: "DNS provider"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
Common.PanelSectionHeader {
|
||||
text: "Wi-Fi networks"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
Common.PanelSectionHeader {
|
||||
text: "Playing"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
fontSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PanelSeparator ----------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PanelSeparator"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "1px horizontal rule. Default 0.12 alpha on foreground; tweak via strength."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
implicitHeight: sepCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: sepCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 12
|
||||
|
||||
Common.PanelSeparator { foreground: root.foreground }
|
||||
Common.PanelSeparator { foreground: root.foreground; strength: 0.25 }
|
||||
Common.PanelSeparator { foreground: root.foreground; strength: 0.45 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- CursorSurface -----------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "CursorSurface"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Single highlight chrome for keyboard+mouse navigable items. Press h/l (anywhere in this window) to move the demo cursor. The middle item is also marked `current` to show how the two states layer."
|
||||
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: csCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: csCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{ "label": "Idle row" },
|
||||
{ "label": "Currently-active row (e.g. connected wifi, default sink)" },
|
||||
{ "label": "Forget / scan / disabled-ish row" }
|
||||
]
|
||||
|
||||
Common.CursorSurface {
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: parent.width
|
||||
implicitHeight: csLabel.implicitHeight + 16
|
||||
hasCursor: root.cursorDemoIndex === index
|
||||
current: index === 1
|
||||
foreground: root.foreground
|
||||
fill: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18)
|
||||
|
||||
Text {
|
||||
id: csLabel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
text: modelData.label
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onContainsMouseChanged: if (containsMouse) root.cursorDemoIndex = parent.index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PillButton --------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PillButton"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Compact rounded button with optional icon, label, tooltip, and `active` highlight. Used inside panels for inline actions (Refresh, DNS pills, Bluetooth header)."
|
||||
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: pillCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: pillCol
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Common.PillButton {
|
||||
text: "DHCP"
|
||||
tooltipText: "Use DNS from DHCP"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
text: "Cloudflare"
|
||||
tooltipText: "Set DNS to Cloudflare"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
active: true
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
tooltipText: "Refresh"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
horizontalPadding: 8
|
||||
verticalPadding: 4
|
||||
}
|
||||
|
||||
Common.PillButton {
|
||||
iconText: ""
|
||||
text: "On"
|
||||
tooltipText: "Turn Bluetooth off"
|
||||
tooltipBackground: root.background
|
||||
tooltipForeground: root.foreground
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
active: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PanelActionButton -------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PanelActionButton"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "22×22 right-edge action button. Two flavors via hoverColor: default (foreground tint, e.g. confirm) and urgent (red tint, e.g. forget/unpair). Hover and click states are intrinsic; the row that owns it stays responsible for the cursor highlight."
|
||||
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: pabCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: pabCol
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
spacing: 18
|
||||
|
||||
Common.PanelActionButton {
|
||||
iconText: ""
|
||||
tooltipText: "Confirm (default flavor)"
|
||||
foreground: root.foreground
|
||||
panelBackground: root.background
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Common.PanelActionButton {
|
||||
iconText: ""
|
||||
tooltipText: "Forget network (urgent flavor)"
|
||||
foreground: root.foreground
|
||||
hoverColor: root.urgent
|
||||
panelBackground: root.background
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Common.PanelActionButton {
|
||||
iconText: ""
|
||||
tooltipText: "Disabled — type a passphrase first"
|
||||
foreground: root.foreground
|
||||
panelBackground: root.background
|
||||
fontFamily: root.fontFamily
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- PanelToolTip ------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "PanelToolTip"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Hover the swatch below to see the styled tooltip. Use this whenever a custom button or row needs a hover hint."
|
||||
color: Qt.darker(root.foreground, 1.5)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 140
|
||||
height: 36
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.35)
|
||||
border.width: 1
|
||||
radius: 4
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "hover me"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tipMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
Common.PanelToolTip {
|
||||
visible: tipMouse.containsMouse
|
||||
text: "Styled tooltip — drop into any panel"
|
||||
panelForeground: root.foreground
|
||||
panelBackground: root.background
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Slider ------------------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Slider"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "Volume / progress slider. Drag, click anywhere on the track, or scroll the wheel."
|
||||
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: sliderRow.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
id: sliderRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 10
|
||||
property real demoVolume: 0.45
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 16
|
||||
width: 22
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Common.Slider {
|
||||
id: demoSlider
|
||||
bar: root.fakeBar
|
||||
width: parent.width - 70
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
value: sliderRow.demoVolume
|
||||
onMoved: function(v) { sliderRow.demoVolume = v }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Math.round((demoSlider.dragging ? demoSlider.liveValue : sliderRow.demoVolume) * 100) + "%"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
width: 38
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Composed example -------------------------------------------
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Composed example"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "A miniature wifi-style row built from CursorSurface + PanelActionButton + PanelToolTip. This is what new panel rows should look like — no inline Rectangle/Text/MouseArea reimplementation."
|
||||
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: composedCol.implicitHeight + 24
|
||||
color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.04)
|
||||
radius: 6
|
||||
border.color: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.10)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
id: composedCol
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 14
|
||||
anchors.rightMargin: 14
|
||||
spacing: 6
|
||||
|
||||
Common.PanelSectionHeader {
|
||||
text: "Wi-Fi networks"
|
||||
foreground: root.foreground
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Common.CursorSurface {
|
||||
width: parent.width
|
||||
implicitHeight: composedRow.implicitHeight + 12
|
||||
current: true
|
||||
foreground: root.foreground
|
||||
fill: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18)
|
||||
|
||||
Item {
|
||||
id: composedRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
implicitHeight: 36
|
||||
|
||||
Text {
|
||||
id: composedIcon
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ""
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
Common.PanelActionButton {
|
||||
id: composedForget
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconText: ""
|
||||
tooltipText: "Forget network"
|
||||
foreground: root.foreground
|
||||
hoverColor: root.urgent
|
||||
panelBackground: root.background
|
||||
fontFamily: root.fontFamily
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 1
|
||||
anchors.left: composedIcon.right
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: composedForget.left
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "HughesWiFi"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
Text {
|
||||
text: "Connected"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 10
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Common.PanelSeparator { foreground: root.foreground }
|
||||
|
||||
Common.CursorSurface {
|
||||
width: parent.width
|
||||
implicitHeight: idleRow.implicitHeight + 12
|
||||
foreground: root.foreground
|
||||
fill: Qt.rgba(root.foreground.r, root.foreground.g, root.foreground.b, 0.18)
|
||||
|
||||
Item {
|
||||
id: idleRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
anchors.rightMargin: 10
|
||||
implicitHeight: 36
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: ""
|
||||
color: Qt.darker(root.foreground, 1.4)
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "HughesATT"
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 1; height: 12 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "omarchy.dev-gallery",
|
||||
"name": "Dev gallery",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Visual reference for omarchy-shell common UI components. Summon with: omarchy-shell-ipc shell summon omarchy.dev-gallery '{}'",
|
||||
"kinds": ["panel"],
|
||||
"activation": "on-demand",
|
||||
"entryPoints": { "panel": "GalleryPanel.qml" }
|
||||
}
|
||||
Reference in New Issue
Block a user