consolidate logic

This commit is contained in:
Ryan Hughes
2026-05-19 19:18:56 -04:00
parent ccc796a3e0
commit 1f3a827701
20 changed files with 158 additions and 148 deletions
+17
View File
@@ -0,0 +1,17 @@
import QtQuick
// Base item every bar widget extends. Codifies the three properties the
// bar host injects into each widget slot:
// bar - the host Bar instance (foreground/background/run/etc).
// moduleName - widget's canonical id, used by the host registry to look
// up settings and to disambiguate inline IPC routes.
// settings - per-widget overrides read from shell.json's layout entry.
//
// Widgets are free to add their own properties, signals, and child items.
Item {
id: root
property QtObject bar: null
property string moduleName: ""
property var settings: ({})
}
+11
View File
@@ -45,6 +45,14 @@ PanelWindow {
property bool open: false
property int gap: Style.gapsOut // distance between bar edge and panel
// Item that should take keyboard focus once the panel maps. Typically a
// PanelKeyCatcher inside the panel content. Layer-shell grants focus to
// the surface at map time, but Qt still needs an active-focus target
// inside the surface for Keys.onPressed handlers to fire. Schedule the
// focus through Qt.callLater so it runs after the surface is fully
// mapped and child items have completed layout.
property Item focusTarget: null
default property alias contentItem: contentHolder.children
readonly property var coordinatorKey: owner || root
@@ -187,6 +195,9 @@ PanelWindow {
// Coordinate on `open`, not `visible`. `visible` lags into the fade-out
// animation, which made ownership transfer to a sibling popup race.
onOpenChanged: {
if (open && focusTarget) Qt.callLater(function() {
if (root.open && root.focusTarget) root.focusTarget.forceActiveFocus()
})
if (!bar) return
if (open) bar.requestPopout(coordinatorKey)
else if (bar.activePopout === coordinatorKey) bar.releasePopout(coordinatorKey)
+49
View File
@@ -0,0 +1,49 @@
import QtQuick
import Quickshell
import Quickshell.Io
// Owns the open/close lifecycle for a bar panel widget. Wraps the
// repetitive popupOpen + closePopout + toggle/show/hide IpcHandler triplet
// so each panel just declares one of these and binds its WidgetButton +
// KeyboardPanel to the exposed `open` property.
//
// Usage:
// PanelController { id: ctrl; ipcTarget: "audioPanel" }
//
// WidgetButton { onPressed: ctrl.toggle() }
// KeyboardPanel { open: ctrl.open; owner: ctrl; focusTarget: keyCatcher }
//
// The bar popout coordinator uses `owner` as a registry key, so each
// PanelController instance doubles as that key. KeyboardPanel calls
// `owner.closePopout()` when another panel grabs the slot.
//
// Set `manageIpc: false` when the panel needs to declare its own IpcHandler
// for additional methods on the same target (monitorPanel adds brightness +
// state). Quickshell only honors one IpcHandler per target, so the panel's
// handler must then also delegate toggle/show/hide to this controller.
QtObject {
id: root
// IPC target name. The bar pairs this with the bar widget's filename so a
// Hyprland keybind (`omarchy-shell <target> toggle`) summons the panel.
property string ipcTarget: ""
property bool manageIpc: true
property bool open: false
function toggle() { open = !open }
function show() { if (!open) open = true }
function hide() { open = false }
function closePopout() { open = false }
property IpcHandler _ipc: manageIpc ? ipcComponent.createObject(root) : null
property Component ipcComponent: Component {
IpcHandler {
target: root.ipcTarget
function toggle(): void { root.toggle() }
function show(): void { root.show() }
function hide(): void { root.hide() }
}
}
}
+2
View File
@@ -1,5 +1,6 @@
module qs.Ui
BarWidget 1.0 BarWidget.qml
Button 1.0 Button.qml
ButtonGroup 1.0 ButtonGroup.qml
CursorSurface 1.0 CursorSurface.qml
@@ -7,6 +8,7 @@ Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml
NumberField 1.0 NumberField.qml
PanelActionButton 1.0 PanelActionButton.qml
PanelController 1.0 PanelController.qml
PanelKeyCatcher 1.0 PanelKeyCatcher.qml
PanelSectionHeader 1.0 PanelSectionHeader.qml
PanelSeparator 1.0 PanelSeparator.qml
+3 -4
View File
@@ -2,13 +2,12 @@ import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "activeWindow"
property QtObject bar: null
property string moduleName: "activeWindow"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
+10 -27
View File
@@ -6,16 +6,14 @@ import Quickshell.Services.Pipewire
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "audioPanel"
property QtObject bar: null
property string moduleName: "audioPanel"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
PanelController { id: ctrl; ipcTarget: "audioPanel" }
readonly property bool popupOpen: ctrl.open
function closePopout() { ctrl.hide() }
readonly property var sink: Pipewire.defaultAudioSink
readonly property var source: Pipewire.defaultAudioSource
@@ -219,10 +217,7 @@ Item {
focusSection = "output"
selectedIndex = -1 // first keyboard cursor reveal starts on the output slider
cursorActive = false
Qt.callLater(function() {
resetScroll()
if (keyCatcher) keyCatcher.forceActiveFocus()
})
Qt.callLater(resetScroll)
}
}
@@ -460,19 +455,6 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
onTriggered: if (!sinkAvailabilityProc.running) sinkAvailabilityProc.running = true
}
// 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() }
}
WidgetButton {
id: button
anchors.fill: parent
@@ -481,7 +463,7 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
fontSize: Style.font.body
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleOutputMute()
else root.popupOpen = !root.popupOpen
else ctrl.toggle()
}
onWheelMoved: function(delta) {
@@ -493,9 +475,10 @@ for block in re.split(r"(?m)^Sink #", sys.stdin.read())[1:]:
KeyboardPanel {
id: panel
anchorItem: button
owner: root
owner: ctrl
bar: root.bar
open: root.popupOpen
open: ctrl.open
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(370))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
+9 -21
View File
@@ -6,14 +6,13 @@ import Quickshell.Bluetooth
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "bluetoothPanel"
property QtObject bar: null
property string moduleName: "bluetoothPanel"
property var settings: ({})
property bool popupOpen: false
PanelController { id: ctrl; ipcTarget: "bluetoothPanel" }
readonly property bool popupOpen: ctrl.open
// Address -> true while we are waiting for a click-initiated pair to land
// so we can chain trust + connect at root scope. Doing this in the row's
@@ -21,7 +20,7 @@ Item {
// moment `paired` flips, before the row's handler reliably fires.
property var pendingPairAddresses: ({})
function closePopout() { popupOpen = false }
function closePopout() { ctrl.hide() }
readonly property var adapter: Bluetooth.defaultAdapter
readonly property var devices: Bluetooth.devices ? Bluetooth.devices.values : []
@@ -226,7 +225,6 @@ Item {
if (knownDevices.length > 0) { focusSection = "known"; selectedIndex = 0 }
else { focusSection = "header"; selectedIndex = 1 }
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -335,17 +333,6 @@ 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() }
}
WidgetButton {
id: button
anchors.fill: parent
@@ -354,16 +341,17 @@ Item {
onPressed: function(b) {
if (b === Qt.RightButton && root.adapter) root.adapter.enabled = !root.adapter.enabled
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
else root.popupOpen = !root.popupOpen
else ctrl.toggle()
}
}
KeyboardPanel {
id: panel
anchorItem: button
owner: root
owner: ctrl
bar: root.bar
open: root.popupOpen
open: ctrl.open
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
+2 -4
View File
@@ -4,12 +4,10 @@ import Quickshell
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "calendar"
property QtObject bar: null
property string moduleName: "calendar"
property var settings: ({})
property date now: new Date()
property date viewMonth: new Date()
+2 -4
View File
@@ -3,12 +3,10 @@ import Quickshell
import Quickshell.Io
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "idleInhibitor"
property QtObject bar: null
property string moduleName: "idleInhibitor"
property var settings: ({})
property bool active: false
+2 -4
View File
@@ -5,12 +5,10 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "keyboardLayout"
property QtObject bar: null
property string moduleName: "keyboardLayout"
property var settings: ({})
property string layoutLabel: ""
property string layoutFull: ""
+3 -4
View File
@@ -2,13 +2,12 @@ import QtQuick
import Quickshell
import Quickshell.Io
import qs.Commons
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "lockKeys"
property QtObject bar: null
property string moduleName: "lockKeys"
property var settings: ({})
property bool capsOn: false
property bool numOn: false
+2 -4
View File
@@ -4,12 +4,10 @@ import Quickshell.Services.Mpris
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "media"
property QtObject bar: null
property string moduleName: "media"
property var settings: ({})
function setting(name, fallback) {
var value = settings ? settings[name] : undefined
+2 -4
View File
@@ -3,12 +3,10 @@ import Quickshell
import Quickshell.Services.Pipewire
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "microphone"
property QtObject bar: null
property string moduleName: "microphone"
property var settings: ({})
readonly property var source: Pipewire.defaultAudioSource
readonly property bool muted: source && source.audio ? source.audio.muted : true
+14 -16
View File
@@ -5,14 +5,15 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "monitorPanel"
property QtObject bar: null
property string moduleName: "monitorPanel"
property var settings: ({})
property bool popupOpen: false
// manageIpc: false so this panel can own the single IpcHandler the target
// permits — needed for the brightness + state methods below.
PanelController { id: ctrl; ipcTarget: "monitorPanel"; manageIpc: false }
readonly property bool popupOpen: ctrl.open
property int brightnessPercent: 0
property int pendingBrightnessPercent: 0
property bool brightnessSetQueued: false
@@ -169,7 +170,7 @@ Item {
flick.contentY = bottom + margin - flick.height
}
function closePopout() { popupOpen = false }
function closePopout() { ctrl.hide() }
IpcHandler {
target: "monitorPanel"
@@ -190,12 +191,9 @@ Item {
})
}
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() }
function toggle(): void { ctrl.toggle() }
function show(): void { ctrl.show() }
function hide(): void { ctrl.hide() }
}
function refresh() {
@@ -273,7 +271,6 @@ Item {
selectedIndex = 0
}
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -346,7 +343,7 @@ Item {
bar: root.bar
text: root.displays.length > 1 ? "󰍺" : "󰍹"
fontSize: Style.font.subtitle
onPressed: function(b) { root.popupOpen = !root.popupOpen }
onPressed: function(b) { ctrl.toggle() }
onWheelMoved: function(delta) {
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
}
@@ -355,9 +352,10 @@ Item {
KeyboardPanel {
id: panel
anchorItem: button
owner: root
owner: ctrl
bar: root.bar
open: root.popupOpen
open: ctrl.open
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(320))
contentHeight: panel.fittedContentHeight(panelColumn.implicitHeight, Style.space(560))
+12 -23
View File
@@ -5,17 +5,16 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "networkPanel"
property QtObject bar: null
property string moduleName: "networkPanel"
property var settings: ({})
property bool popupOpen: false
PanelController { id: ctrl; ipcTarget: "networkPanel" }
readonly property bool popupOpen: ctrl.open
// Centralized close so callers can't forget to drop the passphrase prompt.
function closePopout() {
popupOpen = false
ctrl.hide()
passwordSsid = ""
}
@@ -108,12 +107,13 @@ Item {
var idx = dnsProviders.indexOf(dnsProvider)
dnsIndex = idx >= 0 ? idx : 0
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
// When the passphrase prompt closes (Esc / Cancel / success) restore
// focus to the keyCatcher so j/k/Enter resume working without a click.
// The KeyboardPanel's focusTarget covers initial popup-open; this handles
// the inline-editor case where focus was handed off to a child.
onPasswordSsidChanged: {
if (passwordSsid === "" && popupOpen) {
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
@@ -478,18 +478,6 @@ iwctl known-networks ${quotedSsid} forget
Component.onCompleted: refresh()
// Lets a Hyprland keybind summon the panel without needing to click the
// bar icon. Paired with the SUPER+CTRL+W binding in utilities.lua.
IpcHandler {
target: "networkPanel"
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() }
}
// Pulls everything we want about the active route's interface in one shot.
Process {
id: detailsProc
@@ -643,8 +631,8 @@ fi
rightExtraMargin: 2
onPressed: function(b) {
if (root.popupOpen) root.closePopout()
else { root.popupOpen = true; root.refresh() }
if (ctrl.open) root.closePopout()
else { ctrl.show(); root.refresh() }
}
}
@@ -657,9 +645,10 @@ fi
KeyboardPanel {
id: panel
anchorItem: button
owner: root
owner: ctrl
bar: root.bar
open: root.popupOpen
open: ctrl.open
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
@@ -4,12 +4,10 @@ import Quickshell
import qs.Commons
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "notificationCenter"
property QtObject bar: null
property string moduleName: "notificationCenter"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }
+9 -17
View File
@@ -5,14 +5,13 @@ import Quickshell.Services.UPower
import qs.Commons
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "powerPanel"
property QtObject bar: null
property string moduleName: "powerPanel"
property var settings: ({})
property bool popupOpen: false
PanelController { id: ctrl; ipcTarget: "powerPanel" }
readonly property bool popupOpen: ctrl.open
property var batteryInfo: ({})
property var systemInfo: ({})
property var profiles: []
@@ -20,7 +19,7 @@ Item {
property int profileIndex: 0
property bool cursorActive: false
function closePopout() { popupOpen = false }
function closePopout() { ctrl.hide() }
function selectProfileByDelta(delta) {
if (profiles.length === 0) { profileIndex = 0; return }
@@ -114,7 +113,6 @@ Item {
var idx = profiles.indexOf(activeProfile)
profileIndex = idx >= 0 ? idx : 0
cursorActive = false
Qt.callLater(function() { if (keyCatcher) keyCatcher.forceActiveFocus() })
}
}
@@ -123,13 +121,6 @@ Item {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
IpcHandler {
target: "powerPanel"
function toggle(): void { root.popupOpen = !root.popupOpen }
function show(): void { root.popupOpen = true }
function hide(): void { root.closePopout() }
}
Process {
id: batteryProc
command: ["bash", "-lc", `
@@ -173,15 +164,16 @@ printf 'time\t%s\n' "$($OMARCHY_PATH/bin/omarchy-battery-remaining-time 2>/dev/n
rightExtraMargin: 2
active: UPower.displayDevice && UPower.displayDevice.percentage <= 0.2 && UPower.onBattery
tooltipText: ""
onPressed: function(b) { root.popupOpen = !root.popupOpen }
onPressed: function(b) { ctrl.toggle() }
}
KeyboardPanel {
id: panel
anchorItem: button
owner: root
owner: ctrl
bar: root.bar
open: root.popupOpen
open: ctrl.open
focusTarget: keyCatcher
contentWidth: panel.fittedContentWidth(Style.space(340))
contentHeight: panel.fittedContentHeight(column.implicitHeight)
+3 -4
View File
@@ -1,11 +1,10 @@
import QtQuick
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "spacer"
property QtObject bar: null
property string moduleName: "spacer"
property var settings: ({})
readonly property bool vertical: bar ? bar.vertical : false
readonly property int span: settings && settings.size !== undefined ? Number(settings.size) : 12
+2 -4
View File
@@ -4,12 +4,10 @@ import Quickshell.Io
import qs.Ui
import qs.Commons
Item {
BarWidget {
id: root
moduleName: "systemStats"
property QtObject bar: null
property string moduleName: "systemStats"
property var settings: ({})
property real cpuPercent: 0
property real memPercent: 0
+2 -4
View File
@@ -4,12 +4,10 @@ import Quickshell.Io
import qs.Commons
import qs.Ui
Item {
BarWidget {
id: root
moduleName: "weather"
property QtObject bar: null
property string moduleName: "weather"
property var settings: ({})
property bool popupOpen: false
function closePopout() { popupOpen = false }