From cf4729ca2bcc62e8e9b56a34fd0a72d034361068 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 20 May 2026 18:32:00 +0200 Subject: [PATCH] Extract indicators bar widget --- bin/omarchy-capture-screenrecording | 2 +- bin/omarchy-shell | 2 +- bin/omarchy-toggle-idle | 2 +- bin/omarchy-toggle-nightlight | 2 +- bin/omarchy-toggle-notification-silencing | 2 +- shell/Ui/BarIndicator.qml | 4 +- shell/Ui/WidgetButton.qml | 9 +- shell/plugins/README.md | 3 +- shell/plugins/bar/Bar.qml | 434 ----------------- shell/plugins/bar/FirstPartyWidgets.qml | 1 + shell/plugins/bar/README.md | 5 +- shell/plugins/bar/indicators/nightlight.qml | 4 +- .../bar/indicators/screenrecording.qml | 4 +- shell/plugins/bar/indicators/stayAwake.qml | 4 +- shell/plugins/bar/widgets/indicators.qml | 446 ++++++++++++++++++ shell/plugins/settings/SettingsPanel.qml | 1 - 16 files changed, 470 insertions(+), 455 deletions(-) create mode 100644 shell/plugins/bar/widgets/indicators.qml diff --git a/bin/omarchy-capture-screenrecording b/bin/omarchy-capture-screenrecording index becd5fdb..fb67d880 100755 --- a/bin/omarchy-capture-screenrecording +++ b/bin/omarchy-capture-screenrecording @@ -255,7 +255,7 @@ stop_screenrecording() { } toggle_screenrecording_indicator() { - omarchy-shell -q bar refreshScreenRecording + omarchy-shell -q indicators refresh } screenrecording_active() { diff --git a/bin/omarchy-shell b/bin/omarchy-shell index b73e6188..51c80435 100755 --- a/bin/omarchy-shell +++ b/bin/omarchy-shell @@ -24,7 +24,7 @@ Options: Examples: omarchy-shell shell ping omarchy-shell shell summon omarchy.settings "{}" - omarchy-shell -q bar refreshIndicators + omarchy-shell -q indicators refresh omarchy-shell shell listPlugins omarchy-shell menu toggle root USAGE diff --git a/bin/omarchy-toggle-idle b/bin/omarchy-toggle-idle index 5f6c9beb..df1bb37e 100755 --- a/bin/omarchy-toggle-idle +++ b/bin/omarchy-toggle-idle @@ -3,4 +3,4 @@ # omarchy:summary=Toggle idle behavior so the system either idles normally or stays awake omarchy-shell idle toggle -omarchy-shell -q bar refreshIndicators +omarchy-shell -q indicators refresh diff --git a/bin/omarchy-toggle-nightlight b/bin/omarchy-toggle-nightlight index 2b3b5f0e..30c86173 100755 --- a/bin/omarchy-toggle-nightlight +++ b/bin/omarchy-toggle-nightlight @@ -45,4 +45,4 @@ else hyprctl hyprsunset temperature $OFF_TEMP fi -omarchy-shell -q bar refreshIndicators >/dev/null 2>&1 || true +omarchy-shell -q indicators refresh >/dev/null 2>&1 || true diff --git a/bin/omarchy-toggle-notification-silencing b/bin/omarchy-toggle-notification-silencing index adf608c0..bff86131 100755 --- a/bin/omarchy-toggle-notification-silencing +++ b/bin/omarchy-toggle-notification-silencing @@ -3,4 +3,4 @@ # omarchy:summary=Toggle notification do-not-disturb mode state=$(omarchy-shell notifications toggleDnd 2>/dev/null || echo "") -omarchy-shell -q bar refreshIndicators +omarchy-shell -q indicators refresh diff --git a/shell/Ui/BarIndicator.qml b/shell/Ui/BarIndicator.qml index 01cd175f..57299280 100644 --- a/shell/Ui/BarIndicator.qml +++ b/shell/Ui/BarIndicator.qml @@ -11,10 +11,11 @@ WidgetButton { property string activeTooltipText: "" property string inactiveTooltipText: activeTooltipText property string indicatorBlock: "single" + property var indicatorHost: null property var activeOverride: null 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 inactiveRevealed: !effectiveActive && !!bar && bar.revealInactiveIndicators + readonly property bool inactiveRevealed: !effectiveActive && !!indicatorHost && indicatorHost.revealInactiveIndicators function extractData(raw) { var text = String(raw || "").trim() @@ -49,6 +50,7 @@ WidgetButton { interactive: belongsInBlock && (effectiveActive || indicatorBlock === "inactive" || inactiveRevealed) useActiveColor: false maintainIndicatorReveal: indicatorBlock === "inactive" + revealHost: indicatorHost fontSize: Style.font.caption horizontalMargin: 5 verticalPadding: 5 diff --git a/shell/Ui/WidgetButton.qml b/shell/Ui/WidgetButton.qml index ad24ff56..0dddce9c 100644 --- a/shell/Ui/WidgetButton.qml +++ b/shell/Ui/WidgetButton.qml @@ -24,6 +24,7 @@ Item { property bool pressable: true property bool useActiveColor: true property bool maintainIndicatorReveal: false + property var revealHost: bar property string tooltipText: "" property var registeredBar: null @@ -96,16 +97,16 @@ Item { onEntered: { if (root.bar) { 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: { if (root.bar) { 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) } onWheel: function(wheel) { root.wheelMoved(wheel.angleDelta.y) } diff --git a/shell/plugins/README.md b/shell/plugins/README.md index e6ea0085..09b3c262 100644 --- a/shell/plugins/README.md +++ b/shell/plugins/README.md @@ -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 top-level `bar:` subtree of `~/.config/omarchy/shell.json` (with the shell providing [`shell-defaults.json`](../shell-defaults.json) when the user has -no file). Owns the `bar` IPC target for refresh hooks fired by indicator -scripts. See [`bar/README.md`](bar/README.md) for the widget catalogue +no file). See [`bar/README.md`](bar/README.md) for the widget catalogue and customization schema. ## Bar settings diff --git a/shell/plugins/bar/Bar.qml b/shell/plugins/bar/Bar.qml index cc87a348..88f3664a 100644 --- a/shell/plugins/bar/Bar.qml +++ b/shell/plugins/bar/Bar.qml @@ -63,25 +63,6 @@ Item { property int tooltipRequest: 0 property var activePopout: null 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) { if (!target || clickTargets.indexOf(target) !== -1) return var next = clickTargets.slice() @@ -128,8 +109,6 @@ Item { readonly property bool vertical: position === "left" || position === "right" readonly property int barSize: vertical ? Style.bar.sizeVertical : Style.bar.sizeHorizontal - signal indicatorsRefreshRequested() - function normalizePosition(value) { var next = String(value || "").trim() 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 // 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, @@ -258,7 +217,6 @@ Item { function builtinModuleComponent(name) { switch (String(name)) { case "clock": return clockModuleComponent - case "indicators": return indicatorsModuleComponent case "tray": return trayModuleComponent default: return null } @@ -364,10 +322,6 @@ Item { clearTooltip() } - function refreshIndicators() { - indicatorsRefreshRequested() - } - function trayIconSource(icon) { var value = String(icon || "") var marker = "?path=" @@ -428,15 +382,6 @@ Item { 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 // directory because FileView can't observe a file that doesn't exist yet, // and the flag is created/removed by `omarchy-toggle-bar`. @@ -453,26 +398,6 @@ Item { 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 { model: Quickshell.screens @@ -619,7 +544,6 @@ Item { Component { id: emptyModuleComponent; Item { implicitWidth: 0; implicitHeight: 0; visible: false } } Component { id: clockModuleComponent; ClockModule {} } - Component { id: indicatorsModuleComponent; IndicatorsModule {} } Component { id: trayModuleComponent; TrayModule {} } 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 { text: root.clockText() horizontalMargin: 8.75 diff --git a/shell/plugins/bar/FirstPartyWidgets.qml b/shell/plugins/bar/FirstPartyWidgets.qml index 21e2ca05..7291864e 100644 --- a/shell/plugins/bar/FirstPartyWidgets.qml +++ b/shell/plugins/bar/FirstPartyWidgets.qml @@ -15,6 +15,7 @@ QtObject { "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" }, "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 }, "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 }, diff --git a/shell/plugins/bar/README.md b/shell/plugins/bar/README.md index 5a65118e..fce962fb 100644 --- a/shell/plugins/bar/README.md +++ b/shell/plugins/bar/README.md @@ -58,6 +58,7 @@ Example `shell.json` (bar subtree only shown): | `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 | | `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 | | `systemUpdate` | Available update indicator | left = update | | `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`) -`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 diff --git a/shell/plugins/bar/indicators/nightlight.qml b/shell/plugins/bar/indicators/nightlight.qml index 85a5ffdc..e8f3b43c 100644 --- a/shell/plugins/bar/indicators/nightlight.qml +++ b/shell/plugins/bar/indicators/nightlight.qml @@ -30,9 +30,9 @@ BarIndicator { Component.onCompleted: refresh() Connections { - target: root.bar + target: root.indicatorHost ignoreUnknownSignals: true - function onIndicatorsRefreshRequested() { root.refresh() } + function onRefreshRequested() { root.refresh() } } Process { diff --git a/shell/plugins/bar/indicators/screenrecording.qml b/shell/plugins/bar/indicators/screenrecording.qml index 0036072f..3d91b779 100644 --- a/shell/plugins/bar/indicators/screenrecording.qml +++ b/shell/plugins/bar/indicators/screenrecording.qml @@ -23,9 +23,9 @@ BarIndicator { Component.onCompleted: refresh() Connections { - target: root.bar + target: root.indicatorHost ignoreUnknownSignals: true - function onIndicatorsRefreshRequested() { root.refresh() } + function onRefreshRequested() { root.refresh() } } Process { diff --git a/shell/plugins/bar/indicators/stayAwake.qml b/shell/plugins/bar/indicators/stayAwake.qml index 66e7beab..357f363b 100644 --- a/shell/plugins/bar/indicators/stayAwake.qml +++ b/shell/plugins/bar/indicators/stayAwake.qml @@ -22,9 +22,9 @@ BarIndicator { Component.onCompleted: refresh() Connections { - target: root.bar + target: root.indicatorHost ignoreUnknownSignals: true - function onIndicatorsRefreshRequested() { root.refresh() } + function onRefreshRequested() { root.refresh() } } Process { diff --git a/shell/plugins/bar/widgets/indicators.qml b/shell/plugins/bar/widgets/indicators.qml new file mode 100644 index 00000000..ce410b8b --- /dev/null +++ b/shell/plugins/bar/widgets/indicators.qml @@ -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) + } + } +} diff --git a/shell/plugins/settings/SettingsPanel.qml b/shell/plugins/settings/SettingsPanel.qml index 67158823..69c32ac3 100644 --- a/shell/plugins/settings/SettingsPanel.qml +++ b/shell/plugins/settings/SettingsPanel.qml @@ -292,7 +292,6 @@ Item { // ---------------- widget catalog ----------------------------------------- readonly property var builtinWidgetMeta: ({ "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" } })