diff --git a/CHANGELOG.md b/CHANGELOG.md index 099cd8352..b96ce49ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ for finer detail. ## Unreleased +### Added +- QS Bar adds per-monitor workspace isolation (`isolateMonitors`), allowing + multi-monitor setups to show only the workspaces assigned to each screen on its + respective bar. The inactive screen dims its active workspace indicator to 72% + opacity to convey keyboard focus, and the right-click WorkspacePanel popup also + filters by screen. Accessible via `shell.json` (`qsbar.isolateMonitors`), + `ryoku-shell bar set workspaces isolateMonitors on/off`, and QS Bar Settings > + Identity > Workspaces (#767). + ### Removed - The mango compositor is gone: its provider, config tree, packages, installer choice, deploy steps, tests and docs. Ryoku ships Hyprland and diff --git a/ryoku/i18n/catalog/en.json b/ryoku/i18n/catalog/en.json index 926a0e4c0..d97c5d24c 100644 --- a/ryoku/i18n/catalog/en.json +++ b/ryoku/i18n/catalog/en.json @@ -3465,6 +3465,7 @@ "EXTRACTING": "EXTRACTING", "Each add-on defines its own; applied live": "Each add-on defines its own; applied live", "Each app's level under the sound card.": "Each app's level under the sound card.", + "Each bar shows only the workspaces on its own screen.": "Each bar shows only the workspaces on its own screen.", "Each combo your config wants that Ryoku already uses. Keep Ryoku's shortcut, use yours (Ryoku unbinds its own so yours actually wins), or remap yours to a free chord. Untouched rows keep Ryoku's.": "Each combo your config wants that Ryoku already uses. Keep Ryoku's shortcut, use yours (Ryoku unbinds its own so yours actually wins), or remap yours to a free chord. Untouched rows keep Ryoku's.", "Each device keeps its own bindings; unplug it and nothing else changes.": "Each device keeps its own bindings; unplug it and nothing else changes.", "Each display keeps its own placement and audio settings.": "Each display keeps its own placement and audio settings.", @@ -7288,6 +7289,7 @@ "Per surface": "Per surface", "Per-animation control, for when a feel isn't enough. Each row is one desktop animation: turn it off, change its speed, its curve, or its style.": "Per-animation control, for when a feel isn't enough. Each row is one desktop animation: turn it off, change its speed, its curve, or its style.", "Per-app look on top of the global Appearance": "Per-app look on top of the global Appearance", + "Per-monitor": "Per-monitor", "Per-widget colour": "Per-widget colour", "Percentage from the system battery. Unknown battery state fails closed.": "Percentage from the system battery. Unknown battery state fails closed.", "Percentage meaning": "Percentage meaning", diff --git a/ryoku/shell/ipc/bar_test.go b/ryoku/shell/ipc/bar_test.go index 673a87e62..8c98c01cb 100644 --- a/ryoku/shell/ipc/bar_test.go +++ b/ryoku/shell/ipc/bar_test.go @@ -21,8 +21,9 @@ const catalogueFixture = `[ { "key": "launcherLogoIcon", "type": "choice", "label": "Glyph", "options": ["ryoku", "arch", "dragon"] } ] }, { "id": "workspaces", "gid": "G2", "label": "Workspaces", "gloss": "間", "category": "Bar", "settings": [ - { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, - { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] } ] }, + { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, + { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] }, + { "key": "isolateMonitors", "type": "toggle", "label": "Isolate monitors" } ] }, { "id": "status", "gid": "G3", "label": "Status", "visKey": "status", "desc": "Arch update, tray, notifications" }, { "id": "memory", "gid": "G4", "label": "Memory", "visKey": "memory" }, { "id": "cpu", "gid": "G5", "label": "CPU", "visKey": "cpu" }, @@ -447,6 +448,28 @@ func TestBarVerbsThroughStore(t *testing.T) { } } +// TestBarWorkspacesIsolateMonitors verifies that the isolateMonitors toggle +// is accepted by the bar-set verb and round-trips through the settings store. +func TestBarWorkspacesIsolateMonitors(t *testing.T) { + d := barTestDaemon(t) + + // Turn the toggle on. + if got := d.dispatch("bar set workspaces isolateMonitors on"); got != "ok" { + t.Fatalf("bar set isolateMonitors on: %s", got) + } + if qs, _ := d.frame(t)["qsbar"].(map[string]any); qs["isolateMonitors"] != true { + t.Fatalf("qsbar.isolateMonitors = %v, want true", qs["isolateMonitors"]) + } + + // Turn it back off. + if got := d.dispatch("bar set workspaces isolateMonitors off"); got != "ok" { + t.Fatalf("bar set isolateMonitors off: %s", got) + } + if qs, _ := d.frame(t)["qsbar"].(map[string]any); qs["isolateMonitors"] != false { + t.Fatalf("qsbar.isolateMonitors = %v, want false", qs["isolateMonitors"]) + } +} + // TestDockVerbsThroughStore drives the dock verbs and checks the top-level dock // object round-trips, keeping the shipped fields the CLI does not touch. func TestDockVerbsThroughStore(t *testing.T) { diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml index 2980bd309..c7ca75150 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/BarSlot.qml @@ -941,6 +941,7 @@ PanelWindow { id: compWorkspace WorkspaceWidget { root: barSlot.root + screenName: barSlot.screenName readonly property real barContentLeftInset: 0 readonly property real barContentRightInset: 0 } diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml index 3327509a5..12821569c 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/Theme.qml @@ -2342,6 +2342,8 @@ Item { // ── workspace display style (orthogonal to mode; persisted) ── // "rings" is the stable cache token for the user-facing Frame style. property string workspaceStyle: "default" // default, numbers, magic, kanji, rings, aurora +// Workspace monitor isolation (persisted) + property bool isolateMonitors: false // The marker styles this variant offers (continuous V2 adds Kanji, Frame, // Aurora; "rings" is the persisted cache token for the Frame style). readonly property var workspaceStyleOptions: [ @@ -2713,6 +2715,7 @@ Item { onLauncherLogoIconChanged: if (_widgetsLoaded) saveWidgets() onWeatherImperialChanged: if (_widgetsLoaded) saveWidgets() onWorkspaceStyleChanged: if (_widgetsLoaded) saveWidgets() + onIsolateMonitorsChanged: if (_widgetsLoaded) persistWidgetsToConfig() onBarPositionChanged: if (_widgetsLoaded) saveWidgets() onBarScaleChanged: if (_widgetsLoaded) saveWidgets() onBarShellStyleChanged: if (_widgetsLoaded) saveWidgets() @@ -3010,6 +3013,7 @@ Item { q.barPosition = barPosition q.workspaceMode = workspaceMode q.workspaceStyle = workspaceStyle + q.isolateMonitors = isolateMonitors q.pickerStyle = pickerStyle q.launcherLogoMode = launcherLogoMode q.aiTools = serializeAiTools() @@ -3079,6 +3083,7 @@ Item { if (q.barPosition === "top" || q.barPosition === "bottom") barPosition = q.barPosition if (q.workspaceMode !== undefined) workspaceMode = q.workspaceMode if (q.workspaceStyle !== undefined) workspaceStyle = q.workspaceStyle + if (q.isolateMonitors !== undefined) isolateMonitors = q.isolateMonitors === true if (q.pickerStyle !== undefined) pickerStyle = q.pickerStyle if (q.launcherLogoMode !== undefined) launcherLogoMode = q.launcherLogoMode if (q.aiTools !== undefined) parseAiTools(q.aiTools) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml index dc005ac8e..d742f9571 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/controlcenter/routes/IdentityRoute.qml @@ -304,10 +304,37 @@ Item { } - // ── PREVIEW ── + // ── workspaces: ISOLATE ── Entrance { width: page.colW index: 4 + SettingCard { + width: page.colW + title: I18n.tr("ISOLATE") + kana: "\u5206" + + SettingRow { + anchors.left: parent.left + anchors.right: parent.right + label: I18n.tr("Per-monitor") + desc: I18n.tr("Each bar shows only the workspaces on its own screen.") + source: "shell.json" + controlWidth: Math.max(120, 62 * 2) + Seg { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + options: [I18n.tr("Off"), I18n.tr("On")] + current: page.root && page.root.isolateMonitors ? I18n.tr("On") : I18n.tr("Off") + onChose: (key) => { if (page.root) page.root.isolateMonitors = (key === I18n.tr("On")) } + } + } + } + } + + // ── PREVIEW ── + Entrance { + width: page.colW + index: 5 SettingCard { width: page.colW title: I18n.tr("PREVIEW") diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json index 8b2c0da93..9a0e5882a 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/core/widgets.json @@ -2,7 +2,11 @@ { "id": "launcher", "gid": "G1", "label": "Launcher", "gloss": "起動", "category": "Bar", "desc": "The mark that opens the launcher and this panel. Choose it in Identity." }, { "id": "workspaces", "gid": "G2", "label": "Workspaces", "gloss": "間", "category": "Bar", - "desc": "The workspace row. Its count and marker are set in Identity." }, + "desc": "The workspace row. Its count and marker are set in Identity.", + "settings": [ + { "key": "workspaceMode", "type": "choice", "label": "Shown", "options": ["active", "5", "10"] }, + { "key": "workspaceStyle", "type": "choice", "label": "Marker", "options": ["default", "numbers", "magic", "kanji", "rings", "aurora", "pacman"] }, + { "key": "isolateMonitors", "type": "toggle", "label": "Isolate monitors" } ] }, { "id": "status", "gid": "G3", "label": "Status", "visKey": "status", "desc": "Arch update, tray, notifications" }, { "id": "memory", "gid": "G4", "label": "Memory", "visKey": "memory" }, { "id": "cpu", "gid": "G5", "label": "CPU", "visKey": "cpu" }, diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js new file mode 100644 index 000000000..84d0048cc --- /dev/null +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.js @@ -0,0 +1,70 @@ +.pragma library + +function activeWorkspace(workspaces, screenName, isolateMonitors, focusedWorkspace) { + if (!isolateMonitors || !screenName) return focusedWorkspace + for (var i = 0; i < workspaces.length; i++) { + var workspace = workspaces[i] + if (!workspace.special && workspace.active && workspace.output === screenName) + return workspace + } + return null +} + +function needsWorkspaceData(mode, dynamicModel, isolateMonitors) { + return dynamicModel || mode === "active" || isolateMonitors +} + +function workspaceList(workspaces, mode, dynamicModel, screenName, isolateMonitors, active, extraWorkspace) { + if (dynamicModel) { + var live = [] + for (var i = 0; i < workspaces.length; i++) { + var workspace = workspaces[i] + if (workspace.special) continue + if (isolateMonitors && screenName && workspace.output && workspace.output !== screenName) + continue + live.push(workspace.name) + } + return live + } + + if (mode === "active") { + var ids = {} + for (var j = 0; j < workspaces.length; j++) { + var current = workspaces[j] + if (current.special) continue + if (isolateMonitors && screenName && current.output && current.output !== screenName) + continue + ids[current.name] = true + } + if (active && !active.special + && (!isolateMonitors || !screenName || !active.output || active.output === screenName)) + ids[active.name] = true + return Object.keys(ids).sort(function(a, b) { return Number(a) - Number(b) }) + } + + var count = mode === "5" ? 5 : 10 + var fixed = [] + for (var k = 1; k <= count; k++) { + var name = String(k) + var existing = workspaces.find(function(workspace) { return workspace.name === name }) + if (isolateMonitors && screenName && existing && existing.output && existing.output !== screenName) + continue + fixed.push(name) + } + if (extraWorkspace) fixed.push(extraWorkspace) + return fixed +} + +function extraWorkspace(focused, mode) { + var count = mode === "5" ? 5 : 10 + var number = focused ? Number(focused.name) : 0 + return number > count ? focused.name : "" +} + +function hasMonitorFocus(isolateMonitors, screenName, focusedOutput) { + return !isolateMonitors || !screenName || !focusedOutput || focusedOutput === screenName +} + +function markerOpacity(hasFocus) { + return hasFocus ? 1.0 : 0.72 +} diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs new file mode 100644 index 000000000..55215cc42 --- /dev/null +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceIsolation.test.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import vm from "node:vm"; + +const source = await readFile(new URL("./WorkspaceIsolation.js", import.meta.url), "utf8"); +const module = { exports: {} }; +vm.runInNewContext(source.replace(/^\.pragma library\s*/, "") + + "\nmodule.exports = { activeWorkspace, needsWorkspaceData, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity };", + { module, Number, Object }); +const { activeWorkspace, needsWorkspaceData, workspaceList, extraWorkspace, hasMonitorFocus, markerOpacity } = module.exports; + +const workspaces = [ + { name: "1", output: "eDP-1", active: true, special: false }, + { name: "2", output: "DP-1", active: false, special: false }, + { name: "12", output: "DP-1", active: true, special: false }, + { name: "special:1", output: "eDP-1", active: false, special: true } +]; + +assert.equal(activeWorkspace(workspaces, "eDP-1", true, workspaces[2]), workspaces[0], + "each output selects its own active workspace"); +assert.equal(activeWorkspace(workspaces, "DP-1", true, workspaces[0]), workspaces[2], + "the other output selects its own active workspace"); +assert.equal(activeWorkspace(workspaces, "eDP-1", false, workspaces[2]), workspaces[2], + "disabling isolation keeps the globally focused workspace"); + +assert.equal(needsWorkspaceData("5", false, false), false, + "fixed mode without isolation does not track live workspace data"); +assert.equal(needsWorkspaceData("5", false, true), true, + "fixed mode needs live workspace data to filter monitor assignments"); +assert.equal(needsWorkspaceData("active", false, false), true, + "active mode needs live workspace data"); +assert.equal(needsWorkspaceData("5", true, false), true, + "dynamic mode needs live workspace data"); + +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "eDP-1", true, null, "")), + ["1", "3", "4", "5"], "fixed mode filters assigned workspaces and keeps uncreated slots"); +assert.deepEqual(Array.from(workspaceList(workspaces, "5", false, "DP-1", true, null, "12")), + ["2", "3", "4", "5", "12"], "fixed mode includes the local active workspace beyond its range"); +assert.equal(extraWorkspace({ name: "1" }, "5"), ""); +assert.equal(extraWorkspace({ name: "3" }, "5"), "", + "in-range focus changes keep the extra workspace binding unchanged"); +assert.equal(extraWorkspace({ name: "12" }, "5"), "12", + "fixed mode adds a workspace beyond its numbered range"); + +let focusedOutput = "DP-1"; +assert.equal(markerOpacity(hasMonitorFocus(true, "eDP-1", focusedOutput)), 0.72, + "an active marker dims when another output has focus"); +focusedOutput = "eDP-1"; +assert.equal(markerOpacity(hasMonitorFocus(true, "eDP-1", focusedOutput)), 1.0, + "the active marker brightens when focus moves to its output"); diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml index 0f1d380ee..a78b404af 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/modules/WorkspaceWidget.qml @@ -2,10 +2,12 @@ import Ryoku.Ui.Singletons import QtQuick import QtQuick.Shapes import shell.services +import "WorkspaceIsolation.js" as WorkspaceIsolation Item { id: wsWidget required property var root + property string screenName: "" // Workspace cells intentionally stay dense without a local surface. A widget // border needs its own breathing room, though: make that padding part of the @@ -16,47 +18,40 @@ Item { implicitHeight: 28 readonly property color contentColor: root.widgetContentColor("G2", root.seal) - // The focused workspace name only when it is a real workspace beyond the - // persist range, else "". Renotifies on value change only, so switching - // between in-range workspaces keeps the model stable and the delegate - // Behaviors animating. + // Per-monitor active workspace: when isolateMonitors is active, this monitor's + // active workspace takes precedence over the globally focused one. + readonly property var monitorActiveWs: WorkspaceIsolation.activeWorkspace( + Wm.workspaces, wsWidget.screenName, root.isolateMonitors, Wm.focusedWorkspace) + + // Indicates whether this monitor holds the keyboard/window input focus. + readonly property bool hasMonitorFocus: WorkspaceIsolation.hasMonitorFocus( + root.isolateMonitors, wsWidget.screenName, Wm.focusedOutput) + readonly property bool dynamicModel: Wm.workspaceModel === "dynamic" + readonly property var workspaceData: WorkspaceIsolation.needsWorkspaceData( + root.workspaceMode, wsWidget.dynamicModel, root.isolateMonitors) ? Wm.workspaces : [] + readonly property var listActiveWorkspace: root.workspaceMode === "active" + ? (root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace) : null readonly property string extraWs: { if (root.workspaceMode === "active" || wsWidget.dynamicModel) return "" - var n = root.workspaceMode === "5" ? 5 : 10 - var f = Wm.focusedWorkspace - var num = f ? Number(f.name) : 0 - return (num > n) ? f.name : "" + var focused = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return WorkspaceIsolation.extraWorkspace(focused, root.workspaceMode) } // A dynamic model follows the live workspace list with no invented slots; a // fixed model shows a stable numbered row plus the focused-beyond-range one. - readonly property var workspaceList: { - if (wsWidget.dynamicModel) { - var live = [] - var lw = Wm.workspaces - for (var k = 0; k < lw.length; k++) if (!lw[k].special) live.push(lw[k].name) - return live - } - if (root.workspaceMode === "active") { - var ids = {} - var ws = Wm.workspaces - for (var i = 0; i < ws.length; i++) if (!ws[i].special) ids[ws[i].name] = true - if (Wm.focusedWorkspace && !Wm.focusedWorkspace.special) ids[Wm.focusedWorkspace.name] = true - return Object.keys(ids).sort(function(a, b) { return Number(a) - Number(b) }) - } - var n = root.workspaceMode === "5" ? 5 : 10 - var list = []; for (var j = 1; j <= n; j++) list.push(String(j)) - if (extraWs !== "") list.push(extraWs) - return list - } + readonly property var workspaceList: WorkspaceIsolation.workspaceList( + wsWidget.workspaceData, root.workspaceMode, wsWidget.dynamicModel, wsWidget.screenName, + root.isolateMonitors, wsWidget.listActiveWorkspace, wsWidget.extraWs) // ── PACMAN style state: pacman rests on the focused cell, pellet glyphs // mark occupied cells, dimmed dots the empty ones. On a focus change a // runner pacman travels from the old cell to the new one, chomping as it // goes; the destination pellet is eaten (fades/shrinks) as it arrives. ── - readonly property string focusedWorkspaceKey: Wm.focusedWorkspace - && !Wm.focusedWorkspace.special ? Wm.focusedWorkspace.name : "" + readonly property string focusedWorkspaceKey: { + var fw = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return fw && !fw.special ? fw.name : "" + } property string pacmanLastFocusedWorkspaceKey: "" property string pacmanTargetWorkspaceKey: "" @@ -199,8 +194,10 @@ Item { // default-only `dot`, invisible in numbers/magic) Behavior on scale { NumberAnimation { duration: 120 } } - readonly property bool isFocused: Wm.focusedWorkspace !== null - && Wm.focusedWorkspace.name === wsKey + readonly property bool isFocused: { + var act = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace + return act !== null && act.name === wsKey + } readonly property bool isOccupied: { var w = Wm.workspaceByName(wsKey) @@ -222,7 +219,13 @@ Item { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } } - // ── DEFAULT style: glow + dot ── + Item { + id: wsCellVisual + anchors.fill: parent + opacity: wsCell.isFocused ? WorkspaceIsolation.markerOpacity(wsWidget.hasMonitorFocus) : 1.0 + Behavior on opacity { NumberAnimation { duration: 150 } } + + // ── DEFAULT style: glow + dot ── // glow - alle states, nur opacity variiert Rectangle { visible: root.workspaceStyle === "default" @@ -405,6 +408,7 @@ Item { emptyColor: wsWidget.contentColor hoverColor: wsWidget.contentColor } + } MouseArea { id: wsMa @@ -429,9 +433,11 @@ Item { anchors.fill: parent z: 0 visible: root.workspaceStyle === "rings" && targetIndex >= 0 + opacity: WorkspaceIsolation.markerOpacity(wsWidget.hasMonitorFocus) + Behavior on opacity { NumberAnimation { duration: 150 } } readonly property int targetIndex: { - var focused = Wm.focusedWorkspace + var focused = root.isolateMonitors ? wsWidget.monitorActiveWs : Wm.focusedWorkspace if (!focused || focused.special) return -1 return wsWidget.workspaceList.indexOf(focused.name) } @@ -500,6 +506,8 @@ Item { Item { id: pacmanRunner visible: wsWidget.pacmanTraveling && root.workspaceStyle === "pacman" + opacity: !wsWidget.hasMonitorFocus ? 0.72 : 1.0 + Behavior on opacity { NumberAnimation { duration: 150 } } z: 4 x: wsWidget.pacmanTravelX - width / 2 y: Math.round((parent.height - height) / 2) diff --git a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml index ab296ab9f..6052bdf49 100644 --- a/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml +++ b/ryoku/shell/quickshell/shell/modules/bar/barstyles/qsbar/panels/WorkspacePanel.qml @@ -34,11 +34,22 @@ PanelWindow { Rectangle { id: card property int navIndex: -1 + readonly property var filteredWorkspaces: { + var res = [] + var vs = Wm.workspaces + for (var i = 0; i < vs.length; i++) { + if (vs[i].special) continue + if (root.isolateMonitors && root.activePopupScreenName && vs[i].output && vs[i].output !== root.activePopupScreenName) + continue + res.push(vs[i]) + } + res.sort(function(x, y) { return Number(x.name) - Number(y.name) }) + return res + } readonly property var wsIds: { + var vs = card.filteredWorkspaces var a = [] - var vs = Wm.workspaces - for (var i = 0; i < vs.length; i++) if (!vs[i].special) a.push(vs[i].name) - a.sort(function(x, y) { return Number(x) - Number(y) }) + for (var i = 0; i < vs.length; i++) a.push(vs[i].name) return a } width: 240 @@ -102,12 +113,20 @@ PanelWindow { width: parent.width spacing: 4 Repeater { - model: Wm.workspaces + model: card.filteredWorkspaces delegate: Rectangle { required property var modelData - visible: !modelData.special - readonly property bool isActive: Wm.focusedWorkspace && Wm.focusedWorkspace.name === modelData.name + readonly property bool isActive: { + if (root.isolateMonitors && root.activePopupScreenName) { + var ws = Wm.workspaces + for (var i = 0; i < ws.length; i++) { + if (!ws[i].special && ws[i].active && ws[i].output === root.activePopupScreenName) + return ws[i].name === modelData.name + } + } + return Wm.focusedWorkspace && Wm.focusedWorkspace.name === modelData.name + } readonly property bool navOn: card.navIndex >= 0 && card.navIndex < card.wsIds.length && card.wsIds[card.navIndex] === modelData.name width: col.width height: 30; radius: root.panelButtonRadius