Extract indicators bar widget

This commit is contained in:
David Heinemeier Hansson
2026-05-20 18:32:00 +02:00
parent 1e63e86133
commit cf4729ca2b
16 changed files with 470 additions and 455 deletions
+1 -1
View File
@@ -255,7 +255,7 @@ stop_screenrecording() {
} }
toggle_screenrecording_indicator() { toggle_screenrecording_indicator() {
omarchy-shell -q bar refreshScreenRecording omarchy-shell -q indicators refresh
} }
screenrecording_active() { screenrecording_active() {
+1 -1
View File
@@ -24,7 +24,7 @@ Options:
Examples: Examples:
omarchy-shell shell ping omarchy-shell shell ping
omarchy-shell shell summon omarchy.settings "{}" omarchy-shell shell summon omarchy.settings "{}"
omarchy-shell -q bar refreshIndicators omarchy-shell -q indicators refresh
omarchy-shell shell listPlugins omarchy-shell shell listPlugins
omarchy-shell menu toggle root omarchy-shell menu toggle root
USAGE USAGE
+1 -1
View File
@@ -3,4 +3,4 @@
# omarchy:summary=Toggle idle behavior so the system either idles normally or stays awake # omarchy:summary=Toggle idle behavior so the system either idles normally or stays awake
omarchy-shell idle toggle omarchy-shell idle toggle
omarchy-shell -q bar refreshIndicators omarchy-shell -q indicators refresh
+1 -1
View File
@@ -45,4 +45,4 @@ else
hyprctl hyprsunset temperature $OFF_TEMP hyprctl hyprsunset temperature $OFF_TEMP
fi fi
omarchy-shell -q bar refreshIndicators >/dev/null 2>&1 || true omarchy-shell -q indicators refresh >/dev/null 2>&1 || true
+1 -1
View File
@@ -3,4 +3,4 @@
# omarchy:summary=Toggle notification do-not-disturb mode # omarchy:summary=Toggle notification do-not-disturb mode
state=$(omarchy-shell notifications toggleDnd 2>/dev/null || echo "") state=$(omarchy-shell notifications toggleDnd 2>/dev/null || echo "")
omarchy-shell -q bar refreshIndicators omarchy-shell -q indicators refresh
+3 -1
View File
@@ -11,10 +11,11 @@ WidgetButton {
property string activeTooltipText: "" property string activeTooltipText: ""
property string inactiveTooltipText: activeTooltipText property string inactiveTooltipText: activeTooltipText
property string indicatorBlock: "single" property string indicatorBlock: "single"
property var indicatorHost: null
property var activeOverride: null property var activeOverride: null
readonly property bool effectiveActive: activeOverride === null || activeOverride === undefined ? active : activeOverride === true readonly property bool effectiveActive: activeOverride === null || activeOverride === undefined ? active : activeOverride === true
readonly property bool belongsInBlock: indicatorBlock === "active" ? effectiveActive : (indicatorBlock === "inactive" ? !effectiveActive : true) readonly property bool belongsInBlock: indicatorBlock === "active" ? effectiveActive : (indicatorBlock === "inactive" ? !effectiveActive : true)
readonly property bool inactiveRevealed: !effectiveActive && !!bar && bar.revealInactiveIndicators readonly property bool inactiveRevealed: !effectiveActive && !!indicatorHost && indicatorHost.revealInactiveIndicators
function extractData(raw) { function extractData(raw) {
var text = String(raw || "").trim() var text = String(raw || "").trim()
@@ -49,6 +50,7 @@ WidgetButton {
interactive: belongsInBlock && (effectiveActive || indicatorBlock === "inactive" || inactiveRevealed) interactive: belongsInBlock && (effectiveActive || indicatorBlock === "inactive" || inactiveRevealed)
useActiveColor: false useActiveColor: false
maintainIndicatorReveal: indicatorBlock === "inactive" maintainIndicatorReveal: indicatorBlock === "inactive"
revealHost: indicatorHost
fontSize: Style.font.caption fontSize: Style.font.caption
horizontalMargin: 5 horizontalMargin: 5
verticalPadding: 5 verticalPadding: 5
+5 -4
View File
@@ -24,6 +24,7 @@ Item {
property bool pressable: true property bool pressable: true
property bool useActiveColor: true property bool useActiveColor: true
property bool maintainIndicatorReveal: false property bool maintainIndicatorReveal: false
property var revealHost: bar
property string tooltipText: "" property string tooltipText: ""
property var registeredBar: null property var registeredBar: null
@@ -96,16 +97,16 @@ Item {
onEntered: { onEntered: {
if (root.bar) { if (root.bar) {
root.bar.showTooltip(root, root.tooltipText) root.bar.showTooltip(root, root.tooltipText)
if (root.maintainIndicatorReveal && root.bar.setIndicatorItemHovered)
root.bar.setIndicatorItemHovered(true)
} }
if (root.maintainIndicatorReveal && root.revealHost && root.revealHost.setIndicatorItemHovered)
root.revealHost.setIndicatorItemHovered(true)
} }
onExited: { onExited: {
if (root.bar) { if (root.bar) {
root.bar.hideTooltip(root) root.bar.hideTooltip(root)
if (root.maintainIndicatorReveal && root.bar.setIndicatorItemHovered)
root.bar.setIndicatorItemHovered(false)
} }
if (root.maintainIndicatorReveal && root.revealHost && root.revealHost.setIndicatorItemHovered)
root.revealHost.setIndicatorItemHovered(false)
} }
onClicked: function(mouse) { if (root.pressable) root.triggerPress(mouse.button) } onClicked: function(mouse) { if (root.pressable) root.triggerPress(mouse.button) }
onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) } onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) }
+1 -2
View File
@@ -30,8 +30,7 @@ User-installed plugins live alongside these conceptually but on disk under
The status bar. Mounted at startup, lives forever. Layout lives in the The status bar. Mounted at startup, lives forever. Layout lives in the
top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell
providing [`shell-defaults.json`](../shell-defaults.json) when the user has providing [`shell-defaults.json`](../shell-defaults.json) when the user has
no file). Owns the `bar` IPC target for refresh hooks fired by indicator no file). See [`bar/README.md`](bar/README.md) for the widget catalogue
scripts. See [`bar/README.md`](bar/README.md) for the widget catalogue
and customization schema. and customization schema.
## Bar settings ## Bar settings
-434
View File
@@ -63,25 +63,6 @@ Item {
property int tooltipRequest: 0 property int tooltipRequest: 0
property var activePopout: null property var activePopout: null
property var clickTargets: [] property var clickTargets: []
property bool indicatorAreaHovered: false
property bool indicatorItemHovered: false
readonly property bool revealInactiveIndicators: indicatorAreaHovered || indicatorItemHovered
function setIndicatorAreaHovered(hovered) {
indicatorAreaHovered = hovered
if (hovered) indicatorHideTimer.stop()
else indicatorHideTimer.restart()
}
function setIndicatorItemHovered(hovered) {
if (hovered) {
indicatorItemHovered = true
indicatorHideTimer.stop()
} else {
indicatorHideTimer.restart()
}
}
function registerClickTarget(target) { function registerClickTarget(target) {
if (!target || clickTargets.indexOf(target) !== -1) return if (!target || clickTargets.indexOf(target) !== -1) return
var next = clickTargets.slice() var next = clickTargets.slice()
@@ -128,8 +109,6 @@ Item {
readonly property bool vertical: position === "left" || position === "right" readonly property bool vertical: position === "left" || position === "right"
readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal
signal indicatorsRefreshRequested()
function normalizePosition(value) { function normalizePosition(value) {
var next = String(value || "").trim() var next = String(value || "").trim()
return /^(top|bottom|left|right)$/.test(next) ? next : "top" return /^(top|bottom|left|right)$/.test(next) ? next : "top"
@@ -146,26 +125,6 @@ Item {
} }
} }
function indicatorEntriesFromSettings(settings) {
var source = []
if (settings.items && typeof settings.items.length === "number") source = settings.items
else if (settings.indicators && typeof settings.indicators.length === "number") source = settings.indicators
var result = []
for (var i = 0; i < source.length; i++) {
var item = source[i]
if (typeof item !== "string" && item !== null && typeof item === "object") {
try {
item = JSON.parse(JSON.stringify(item))
} catch (error) {
}
}
var id = entryId(item)
if (id !== "") result.push(item)
}
return result
}
// The tray drawer reveals inward (away from the bar edge). Place it at the // The tray drawer reveals inward (away from the bar edge). Place it at the
// section's inner edge: start of the right section, end of the left/center // section's inner edge: start of the right section, end of the left/center
// sections. The drawer's reserved space then sits next to the bar center, // sections. The drawer's reserved space then sits next to the bar center,
@@ -258,7 +217,6 @@ Item {
function builtinModuleComponent(name) { function builtinModuleComponent(name) {
switch (String(name)) { switch (String(name)) {
case "clock": return clockModuleComponent case "clock": return clockModuleComponent
case "indicators": return indicatorsModuleComponent
case "tray": return trayModuleComponent case "tray": return trayModuleComponent
default: return null default: return null
} }
@@ -364,10 +322,6 @@ Item {
clearTooltip() clearTooltip()
} }
function refreshIndicators() {
indicatorsRefreshRequested()
}
function trayIconSource(icon) { function trayIconSource(icon) {
var value = String(icon || "") var value = String(icon || "")
var marker = "?path=" var marker = "?path="
@@ -428,15 +382,6 @@ Item {
onTriggered: if (!root.targetTooltipHovered(root.tooltipTarget)) root.hideTooltip(root.tooltipTarget) onTriggered: if (!root.targetTooltipHovered(root.tooltipTarget)) root.hideTooltip(root.tooltipTarget)
} }
Timer {
id: indicatorHideTimer
interval: 120
onTriggered: {
if (!root.indicatorAreaHovered)
root.indicatorItemHovered = false
}
}
// Presence of the `bar-off` flag = bar hidden. Watching the parent toggles // Presence of the `bar-off` flag = bar hidden. Watching the parent toggles
// directory because FileView can't observe a file that doesn't exist yet, // directory because FileView can't observe a file that doesn't exist yet,
// and the flag is created/removed by `omarchy-toggle-bar`. // and the flag is created/removed by `omarchy-toggle-bar`.
@@ -453,26 +398,6 @@ Item {
onFileChanged: barHiddenProbe.running = true onFileChanged: barHiddenProbe.running = true
} }
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshIndicators()
}
IpcHandler {
target: "bar"
function refreshScreenRecording(): void {
root.refreshIndicators()
}
function refreshIndicators(): void {
root.refreshIndicators()
}
}
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
@@ -619,7 +544,6 @@ Item {
Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } }
Component { id: clockModuleComponent; ClockModule {} } Component { id: clockModuleComponent; ClockModule {} }
Component { id: indicatorsModuleComponent; IndicatorsModule {} }
Component { id: trayModuleComponent; TrayModule {} } Component { id: trayModuleComponent; TrayModule {} }
function findCenterAnchorEntry() { function findCenterAnchorEntry() {
@@ -989,364 +913,6 @@ Item {
} }
} }
component IndicatorsModule: Item {
id: indicatorsRoot
property var bar: root
property string moduleName: "indicators"
property var settings: ({})
readonly property var indicatorEntries: root.indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: []
property var indicatorActiveStates: ({})
ListModel { id: activeIndicatorModel }
function hasIndicatorId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
if (root.entryId(indicatorEntries[i]) === id) return true
}
return false
}
function entryForId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
var entry = indicatorEntries[i]
if (root.entryId(entry) === id) return entry
}
return { id: id }
}
function activeModelIndex(id) {
for (var i = 0; i < activeIndicatorModel.count; i++) {
if (activeIndicatorModel.get(i).activeId === id) return i
}
return -1
}
function copyActiveStates() {
var states = {}
for (var id in indicatorActiveStates) {
if (indicatorActiveStates[id] === true) states[id] = true
}
return states
}
function orderedActiveIds(states, preferredOrder) {
var ids = []
for (var i = 0; i < preferredOrder.length; i++) {
var id = preferredOrder[i]
if (ids.indexOf(id) === -1 && hasIndicatorId(id) && states[id] === true) ids.push(id)
}
return ids
}
function syncActiveIndicatorModel() {
for (var i = activeIndicatorModel.count - 1; i >= 0; i--) {
if (activeIndicatorIds.indexOf(activeIndicatorModel.get(i).activeId) === -1)
activeIndicatorModel.remove(i)
}
for (var j = 0; j < activeIndicatorIds.length; j++) {
var id = activeIndicatorIds[j]
var index = activeModelIndex(id)
if (index === -1) activeIndicatorModel.insert(j, { activeId: id })
else if (index !== j) activeIndicatorModel.move(index, j, 1)
}
}
function setIndicatorActive(entry, active) {
var id = root.entryId(entry)
if (id === "") return
var states = copyActiveStates()
if (active) states[id] = true
else delete states[id]
indicatorActiveStates = states
var ids = orderedActiveIds(states, activeIndicatorIds)
if (active && ids.indexOf(id) === -1 && hasIndicatorId(id)) ids.push(id)
activeIndicatorIds = ids
syncActiveIndicatorModel()
}
function syncActiveIndicatorOrder() {
activeIndicatorIds = orderedActiveIds(indicatorActiveStates, activeIndicatorIds)
syncActiveIndicatorModel()
}
onIndicatorEntriesChanged: syncActiveIndicatorOrder()
implicitWidth: root.vertical ? verticalIndicators.implicitWidth : horizontalIndicators.implicitWidth
implicitHeight: root.vertical ? verticalIndicators.implicitHeight : horizontalIndicators.implicitHeight
Row {
id: horizontalIndicators
visible: !root.vertical
spacing: 0
ActiveIndicatorBlock {
indicatorsModule: indicatorsRoot
indicatorModel: activeIndicatorModel
horizontal: true
reportActiveState: !root.vertical
}
Item {
id: inactiveHorizontalArea
implicitWidth: inactiveHorizontalBlock.implicitWidth
implicitHeight: inactiveHorizontalBlock.implicitHeight
width: implicitWidth
height: implicitHeight
IndicatorBlock {
id: inactiveHorizontalBlock
anchors.fill: parent
indicatorsModule: indicatorsRoot
indicatorEntries: indicatorsRoot.indicatorEntries
indicatorBlock: "inactive"
horizontal: true
reportActiveState: !root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
}
Column {
id: verticalIndicators
visible: root.vertical
spacing: 0
ActiveIndicatorBlock {
indicatorsModule: indicatorsRoot
indicatorModel: activeIndicatorModel
horizontal: false
reportActiveState: root.vertical
}
Item {
id: inactiveVerticalArea
implicitWidth: inactiveVerticalBlock.implicitWidth
implicitHeight: inactiveVerticalBlock.implicitHeight
width: implicitWidth
height: implicitHeight
IndicatorBlock {
id: inactiveVerticalBlock
anchors.fill: parent
indicatorsModule: indicatorsRoot
indicatorEntries: indicatorsRoot.indicatorEntries
indicatorBlock: "inactive"
horizontal: false
reportActiveState: root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
}
}
component ActiveIndicatorBlock: Item {
id: activeIndicatorBlockRoot
property var indicatorModel: null
property var indicatorsModule: null
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.childrenRect.width : 0
implicitHeight: blockLoader.item ? blockLoader.item.childrenRect.height : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.fill: parent
sourceComponent: activeIndicatorBlockRoot.horizontal ? horizontalActiveIndicatorBlock : verticalActiveIndicatorBlock
}
Component {
id: horizontalActiveIndicatorBlock
Row {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalActiveIndicatorBlock
Column {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorBlock: Item {
id: indicatorBlockRoot
property var indicatorEntries: []
property var indicatorsModule: null
property string indicatorBlock: "active"
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.childrenRect.width : 0
implicitHeight: blockLoader.item ? blockLoader.item.childrenRect.height : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.fill: parent
sourceComponent: indicatorBlockRoot.horizontal ? horizontalIndicatorBlock : verticalIndicatorBlock
}
Component {
id: horizontalIndicatorBlock
Row {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalIndicatorBlock
Column {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorLoader: Item {
id: indicatorSlot
required property var entry
property var indicatorsModule: null
required property string indicatorBlock
property bool reportActiveState: false
property bool activeStateObserved: false
readonly property string indicatorId: root.entryId(entry)
readonly property var indicatorSettings: root.entrySettings(entry)
implicitWidth: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitWidth : 0
implicitHeight: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitHeight : 0
width: implicitWidth
height: implicitHeight
onEntryChanged: {
activeStateObserved = false
injectProps()
syncActiveState()
}
onIndicatorBlockChanged: injectProps()
onIndicatorSettingsChanged: injectProps()
onIndicatorsModuleChanged: syncActiveState()
onReportActiveStateChanged: syncActiveState()
Loader {
id: indicatorSource
anchors.fill: parent
source: indicatorSlot.indicatorId ? Qt.resolvedUrl("indicators/" + indicatorSlot.indicatorId + ".qml") : ""
onLoaded: {
indicatorSlot.injectProps()
indicatorSlot.syncActiveState()
}
onStatusChanged: if (status === Loader.Error) console.warn("Indicator loader error", indicatorSlot.indicatorId, source)
}
Connections {
target: indicatorSource.item
ignoreUnknownSignals: true
function onActiveChanged() { indicatorSlot.syncActiveState() }
}
function injectProps() {
var target = indicatorSource.item
if (!target) return
if ("bar" in target) target.bar = root
if ("moduleName" in target) target.moduleName = indicatorId
if ("settings" in target) target.settings = indicatorSettings
if ("indicatorBlock" in target) target.indicatorBlock = indicatorBlock
if ("activeOverride" in target) target.activeOverride = indicatorBlock === "active" ? true : null
}
function syncActiveState() {
if (!reportActiveState || !indicatorsModule || !indicatorsModule.setIndicatorActive) return
var active = !!indicatorSource.item && indicatorSource.item.active === true
if (indicatorBlock === "active") {
if (active) activeStateObserved = true
else if (!activeStateObserved) return
}
indicatorsModule.setIndicatorActive(entry, active)
}
}
component ClockModule: ModuleButton { component ClockModule: ModuleButton {
text: root.clockText() text: root.clockText()
horizontalMargin: 8.75 horizontalMargin: 8.75
+1
View File
@@ -15,6 +15,7 @@ QtObject {
"powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" }, "powerPanel": { displayName: "Power", description: "Battery, power profile, and system stats", category: "System", allowMultiple: false, sourceDir: "../panels", sourceName: "Power" },
"bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" }, "bluetoothPanel": { displayName: "Bluetooth", description: "Bluetooth device list with connect/disconnect", category: "Network", allowMultiple: false, sourceDir: "../panels", sourceName: "Bluetooth" },
"daytime": { displayName: "Daytime", description: "Day/time label with date tooltip", category: "Time", allowMultiple: false, settingsForm: "daytimeSettings" }, "daytime": { displayName: "Daytime", description: "Day/time label with date tooltip", category: "Time", allowMultiple: false, settingsForm: "daytimeSettings" },
"indicators": { displayName: "Indicators", description: "Manual state indicators", category: "Status", allowMultiple: false },
"notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false }, "notificationCenter": { displayName: "Notification center", description: "Recent notifications + DND", category: "Status", allowMultiple: false },
"systemUpdate": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false }, "systemUpdate": { displayName: "System update", description: "Indicates available system updates", category: "System", allowMultiple: false },
"systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false }, "systemStats": { displayName: "System stats", description: "CPU icon — hover for graphs, click to open btop", category: "System", allowMultiple: false },
+3 -2
View File
@@ -58,6 +58,7 @@ Example `shell.json` (bar subtree only shown):
| `workspaces` | Hyprland workspace switcher | left = focus workspace | | `workspaces` | Hyprland workspace switcher | left = focus workspace |
| `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup | | `media` | MPRIS now-playing — scrolling track + artist, cover-art popup | left = play/pause · middle = next · scroll = prev/next · right = popup |
| `daytime` | Day/time label with date tooltip | hover = date | | `daytime` | Day/time label with date tooltip | hover = date |
| `indicators` | Manual state indicators | left = indicator action |
| `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND | | `notificationCenter` | Bell with badge + popup with recent notifications, DND toggle | left = popup · right = toggle DND |
| `systemUpdate` | Available update indicator | left = update | | `systemUpdate` | Available update indicator | left = update |
| `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal | | `systemStats` | Inline CPU + memory sparklines, popup with detail | left = popup · right = terminal |
@@ -76,9 +77,9 @@ Example `shell.json` (bar subtree only shown):
### Built-in base modules (in `Bar.qml`) ### Built-in base modules (in `Bar.qml`)
`clock`, `indicators`, `tray`. `clock`, `tray`.
The `indicators` module loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above. The `indicators` widget loads individual bar indicators from `indicators/`, ordered by its `items` array in `shell.json`. Rich panels such as `powerPanel`, `networkPanel`, and `audioPanel` live in `../panels/` above.
## Orientation ## Orientation
+2 -2
View File
@@ -30,9 +30,9 @@ BarIndicator {
Component.onCompleted: refresh() Component.onCompleted: refresh()
Connections { Connections {
target: root.bar target: root.indicatorHost
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onIndicatorsRefreshRequested() { root.refresh() } function onRefreshRequested() { root.refresh() }
} }
Process { Process {
@@ -23,9 +23,9 @@ BarIndicator {
Component.onCompleted: refresh() Component.onCompleted: refresh()
Connections { Connections {
target: root.bar target: root.indicatorHost
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onIndicatorsRefreshRequested() { root.refresh() } function onRefreshRequested() { root.refresh() }
} }
Process { Process {
+2 -2
View File
@@ -22,9 +22,9 @@ BarIndicator {
Component.onCompleted: refresh() Component.onCompleted: refresh()
Connections { Connections {
target: root.bar target: root.indicatorHost
ignoreUnknownSignals: true ignoreUnknownSignals: true
function onIndicatorsRefreshRequested() { root.refresh() } function onRefreshRequested() { root.refresh() }
} }
Process { Process {
+446
View File
@@ -0,0 +1,446 @@
import QtQuick
import Quickshell
import qs.Commons
import qs.Ui
BarWidget {
id: root
moduleName: "indicators"
readonly property bool vertical: bar ? bar.vertical : false
readonly property var indicatorEntries: indicatorEntriesFromSettings(settings)
property var activeIndicatorIds: []
property var indicatorActiveStates: ({})
property bool indicatorAreaHovered: false
property bool indicatorItemHovered: false
readonly property bool revealInactiveIndicators: indicatorAreaHovered || indicatorItemHovered
signal refreshRequested()
ListModel { id: activeIndicatorModel }
function entryId(entry) {
if (typeof entry === "string") return entry
if (Util.isPlainObject(entry)) {
var id = entry["id"]
if (id !== undefined && id !== null && String(id) !== "") return String(id)
}
return ""
}
function entrySettings(entry) {
if (!Util.isPlainObject(entry)) return {}
var copy = {}
for (var key in entry) {
if (key === "id") continue
copy[key] = entry[key]
}
return copy
}
function indicatorEntriesFromSettings(settings) {
var source = []
if (settings.items && typeof settings.items.length === "number") source = settings.items
else if (settings.indicators && typeof settings.indicators.length === "number") source = settings.indicators
var result = []
for (var i = 0; i < source.length; i++) {
var item = source[i]
if (typeof item !== "string" && item !== null && typeof item === "object") {
try {
item = JSON.parse(JSON.stringify(item))
} catch (error) {
}
}
var id = entryId(item)
if (id !== "") result.push(item)
}
return result
}
function setIndicatorAreaHovered(hovered) {
indicatorAreaHovered = hovered
if (hovered) indicatorHideTimer.stop()
else indicatorHideTimer.restart()
}
function setIndicatorItemHovered(hovered) {
if (hovered) {
indicatorItemHovered = true
indicatorHideTimer.stop()
} else {
indicatorHideTimer.restart()
}
}
function hasIndicatorId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
if (entryId(indicatorEntries[i]) === id) return true
}
return false
}
function entryForId(id) {
for (var i = 0; i < indicatorEntries.length; i++) {
var entry = indicatorEntries[i]
if (entryId(entry) === id) return entry
}
return { id: id }
}
function activeModelIndex(id) {
for (var i = 0; i < activeIndicatorModel.count; i++) {
if (activeIndicatorModel.get(i).activeId === id) return i
}
return -1
}
function copyActiveStates() {
var states = {}
for (var id in indicatorActiveStates) {
if (indicatorActiveStates[id] === true) states[id] = true
}
return states
}
function orderedActiveIds(states, preferredOrder) {
var ids = []
for (var i = 0; i < preferredOrder.length; i++) {
var id = preferredOrder[i]
if (ids.indexOf(id) === -1 && hasIndicatorId(id) && states[id] === true) ids.push(id)
}
return ids
}
function syncActiveIndicatorModel() {
for (var i = activeIndicatorModel.count - 1; i >= 0; i--) {
if (activeIndicatorIds.indexOf(activeIndicatorModel.get(i).activeId) === -1)
activeIndicatorModel.remove(i)
}
for (var j = 0; j < activeIndicatorIds.length; j++) {
var id = activeIndicatorIds[j]
var index = activeModelIndex(id)
if (index === -1) activeIndicatorModel.insert(j, { activeId: id })
else if (index !== j) activeIndicatorModel.move(index, j, 1)
}
}
function setIndicatorActive(entry, active) {
var id = entryId(entry)
if (id === "") return
var states = copyActiveStates()
if (active) states[id] = true
else delete states[id]
indicatorActiveStates = states
var ids = orderedActiveIds(states, activeIndicatorIds)
if (active && ids.indexOf(id) === -1 && hasIndicatorId(id)) ids.push(id)
activeIndicatorIds = ids
syncActiveIndicatorModel()
}
function syncActiveIndicatorOrder() {
activeIndicatorIds = orderedActiveIds(indicatorActiveStates, activeIndicatorIds)
syncActiveIndicatorModel()
}
onIndicatorEntriesChanged: syncActiveIndicatorOrder()
implicitWidth: root.vertical ? verticalIndicators.implicitWidth : horizontalIndicators.implicitWidth
implicitHeight: root.vertical ? verticalIndicators.implicitHeight : horizontalIndicators.implicitHeight
IpcHandler {
target: "indicators"
function refresh(): void {
root.refreshRequested()
}
}
Timer {
id: indicatorHideTimer
interval: 120
onTriggered: {
if (!root.indicatorAreaHovered)
root.indicatorItemHovered = false
}
}
Timer {
interval: 2000
running: true
repeat: true
triggeredOnStart: true
onTriggered: root.refreshRequested()
}
Row {
id: horizontalIndicators
visible: !root.vertical
spacing: 0
ActiveIndicatorBlock {
indicatorsModule: root
indicatorModel: activeIndicatorModel
horizontal: true
reportActiveState: !root.vertical
}
Item {
id: inactiveHorizontalArea
implicitWidth: inactiveHorizontalBlock.implicitWidth
implicitHeight: inactiveHorizontalBlock.implicitHeight
width: implicitWidth
height: implicitHeight
IndicatorBlock {
id: inactiveHorizontalBlock
anchors.fill: parent
indicatorsModule: root
indicatorEntries: root.indicatorEntries
indicatorBlock: "inactive"
horizontal: true
reportActiveState: !root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
}
Column {
id: verticalIndicators
visible: root.vertical
spacing: 0
ActiveIndicatorBlock {
indicatorsModule: root
indicatorModel: activeIndicatorModel
horizontal: false
reportActiveState: root.vertical
}
Item {
id: inactiveVerticalArea
implicitWidth: inactiveVerticalBlock.implicitWidth
implicitHeight: inactiveVerticalBlock.implicitHeight
width: implicitWidth
height: implicitHeight
IndicatorBlock {
id: inactiveVerticalBlock
anchors.fill: parent
indicatorsModule: root
indicatorEntries: root.indicatorEntries
indicatorBlock: "inactive"
horizontal: false
reportActiveState: root.vertical
}
HoverHandler {
onHoveredChanged: root.setIndicatorAreaHovered(hovered)
}
}
}
component ActiveIndicatorBlock: Item {
id: activeIndicatorBlockRoot
property var indicatorModel: null
property var indicatorsModule: null
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.childrenRect.width : 0
implicitHeight: blockLoader.item ? blockLoader.item.childrenRect.height : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.fill: parent
sourceComponent: activeIndicatorBlockRoot.horizontal ? horizontalActiveIndicatorBlock : verticalActiveIndicatorBlock
}
Component {
id: horizontalActiveIndicatorBlock
Row {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalActiveIndicatorBlock
Column {
spacing: 0
Repeater {
model: activeIndicatorBlockRoot.indicatorModel
IndicatorLoader {
required property string activeId
indicatorsModule: activeIndicatorBlockRoot.indicatorsModule
entry: activeIndicatorBlockRoot.indicatorsModule.entryForId(activeId)
indicatorBlock: "active"
reportActiveState: activeIndicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorBlock: Item {
id: indicatorBlockRoot
property var indicatorEntries: []
property var indicatorsModule: null
property string indicatorBlock: "active"
property bool horizontal: true
property bool reportActiveState: false
implicitWidth: blockLoader.item ? blockLoader.item.childrenRect.width : 0
implicitHeight: blockLoader.item ? blockLoader.item.childrenRect.height : 0
width: implicitWidth
height: implicitHeight
Loader {
id: blockLoader
anchors.fill: parent
sourceComponent: indicatorBlockRoot.horizontal ? horizontalIndicatorBlock : verticalIndicatorBlock
}
Component {
id: horizontalIndicatorBlock
Row {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
Component {
id: verticalIndicatorBlock
Column {
spacing: 0
Repeater {
model: indicatorBlockRoot.indicatorEntries
IndicatorLoader {
required property var modelData
indicatorsModule: indicatorBlockRoot.indicatorsModule
entry: modelData
indicatorBlock: indicatorBlockRoot.indicatorBlock
reportActiveState: indicatorBlockRoot.reportActiveState
}
}
}
}
}
component IndicatorLoader: Item {
id: indicatorSlot
required property var entry
property var indicatorsModule: null
required property string indicatorBlock
property bool reportActiveState: false
property bool activeStateObserved: false
readonly property string indicatorId: root.entryId(entry)
readonly property var indicatorSettings: root.entrySettings(entry)
implicitWidth: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitWidth : 0
implicitHeight: indicatorSource.item && indicatorSource.item.visible ? indicatorSource.item.implicitHeight : 0
width: implicitWidth
height: implicitHeight
onEntryChanged: {
activeStateObserved = false
injectProps()
syncActiveState()
}
onIndicatorBlockChanged: injectProps()
onIndicatorSettingsChanged: injectProps()
onIndicatorsModuleChanged: syncActiveState()
onReportActiveStateChanged: syncActiveState()
Loader {
id: indicatorSource
anchors.fill: parent
source: indicatorSlot.indicatorId ? Qt.resolvedUrl("../indicators/" + indicatorSlot.indicatorId + ".qml") : ""
onLoaded: {
indicatorSlot.injectProps()
indicatorSlot.syncActiveState()
}
onStatusChanged: if (status === Loader.Error) console.warn("Indicator loader error", indicatorSlot.indicatorId, source)
}
Connections {
target: indicatorSource.item
ignoreUnknownSignals: true
function onActiveChanged() { indicatorSlot.syncActiveState() }
}
function injectProps() {
var target = indicatorSource.item
if (!target) return
if ("bar" in target) target.bar = root.bar
if ("moduleName" in target) target.moduleName = indicatorId
if ("settings" in target) target.settings = indicatorSettings
if ("indicatorBlock" in target) target.indicatorBlock = indicatorBlock
if ("indicatorHost" in target) target.indicatorHost = root
if ("activeOverride" in target) target.activeOverride = indicatorBlock === "active" ? true : null
}
function syncActiveState() {
if (!reportActiveState || !indicatorsModule || !indicatorsModule.setIndicatorActive) return
var active = !!indicatorSource.item && indicatorSource.item.active === true
if (indicatorBlock === "active") {
if (active) activeStateObserved = true
else if (!activeStateObserved) return
}
indicatorsModule.setIndicatorActive(entry, active)
}
}
}
-1
View File
@@ -292,7 +292,6 @@ Item {
// ---------------- widget catalog ----------------------------------------- // ---------------- widget catalog -----------------------------------------
readonly property var builtinWidgetMeta: ({ readonly property var builtinWidgetMeta: ({
"clock": { name: "Clock", description: "Date / time text", category: "Time" }, "clock": { name: "Clock", description: "Date / time text", category: "Time" },
"indicators": { name: "Indicators", description: "Manual state indicators", category: "Status" },
"tray": { name: "System tray", description: "Status notifier items", category: "Status" } "tray": { name: "System tray", description: "Status notifier items", category: "Status" }
}) })