diff --git a/.gitignore b/.gitignore
index ef9b0f2..a6d76d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
contents/ui/config_ui.py
-KZones.kwinscript
\ No newline at end of file
+kzones.kwinscript
\ No newline at end of file
diff --git a/README.md b/README.md
index 69fecef..daa14fc 100644
--- a/README.md
+++ b/README.md
@@ -271,6 +271,7 @@ Each **layout** object needs the following keys:
- `name`: The name of the layout, shown when cycling between layouts
- `padding`: The amount of space between the window and the zone in pixels
- `zones`: An array containing all zone objects for this layout
+- `screens`: An optional array of monitor connector names (e.g. `["DP-1", "HDMI-A-1"]`) restricting this layout to those monitors. Omit or leave empty to show the layout on every monitor. To discover connector names, bind the **KZones: Show detected monitors** shortcut in `System Settings / Shortcuts` and press it, or run `kscreen-doctor -o`. When any layout sets `screens`, per-screen tracking of the active layout is enabled automatically.
Each **zone** object can contain the following keys:
@@ -323,6 +324,7 @@ List of all available shortcuts:
| Move active window right | Meta + Right |
| Snap all windows | Meta + Space |
| Snap active window | Meta + Shift + Space |
+| Show detected monitors (OSD) | *(unbound — set in System Settings → Shortcuts)* |
*To change the default bindings, go to `System Settings / Shortcuts` and search for KZones*
diff --git a/src/contents/code/core.mjs b/src/contents/code/core.mjs
index 0cb7c30..2fed5b2 100644
--- a/src/contents/code/core.mjs
+++ b/src/contents/code/core.mjs
@@ -3,12 +3,80 @@ export let Workspace = null;
export let QML = {};
export let config = {};
+// Native-JS reference to parsed layouts. Avoids round-tripping through
+// `config.layouts`, which QML coerces to QVariantList and loses Array identity.
+let _layoutsRaw = [];
+
export function init(kwin, workspace) {
console.log("KZones: Loading APIs...");
KWin = kwin || null;
Workspace = workspace || null;
}
+export function getScreenId(screen) {
+ return screen && screen.name ? String(screen.name) : "";
+}
+
+// QML wraps JS arrays as QVariantList across the property-var boundary,
+// which breaks `Array.isArray`.
+function isArrayLike(x) {
+ return x != null && typeof x !== "string" && typeof x.length === "number";
+}
+
+// Missing/empty `screens` means: apply to every monitor (back-compat).
+export function layoutAppliesToScreen(layout, screenId) {
+ if (!layout) return true;
+ const s = layout.screens;
+ if (!isArrayLike(s) || s.length === 0) return true;
+ for (let i = 0; i < s.length; i++) {
+ if (String(s[i]) === screenId) return true;
+ }
+ return false;
+}
+
+// Returns [{ layout, index }, ...] visible on `screenId`. `index` is the
+// position in the unfiltered layouts array so callers can keep referring to
+// layouts by their original index.
+export function getLayoutsForScreen(screenId) {
+ const out = [];
+ const layouts = _layoutsRaw;
+ if (!isArrayLike(layouts)) return out;
+ for (let i = 0; i < layouts.length; i++) {
+ if (layoutAppliesToScreen(layouts[i], screenId))
+ out.push({ layout: layouts[i], index: i });
+ }
+ return out;
+}
+
+// Enumerate attached monitors. Surfaces connector names users need for the
+// per-layout `screens` field; KWin scripting can't auto-populate the KCM.
+export function getDetectedScreens() {
+ if (!Workspace) return [];
+ let screens = [];
+ try {
+ if (Array.isArray(Workspace.screens)) {
+ screens = Workspace.screens;
+ } else if (Workspace.screens && typeof Workspace.screens.length === "number") {
+ for (let i = 0; i < Workspace.screens.length; i++)
+ screens.push(Workspace.screens[i]);
+ } else if (typeof Workspace.numScreens === "number" && typeof Workspace.screenAt === "function") {
+ for (let i = 0; i < Workspace.numScreens; i++)
+ screens.push(Workspace.screenAt(i));
+ }
+ } catch (e) {
+ console.error("KZones: getDetectedScreens enumeration failed:", e);
+ return [];
+ }
+ return screens.map(s => {
+ const g = (s && s.geometry) || {};
+ return {
+ name: (s && s.name) ? String(s.name) : "",
+ width: g.width || 0,
+ height: g.height || 0
+ };
+ });
+}
+
export function registerQMLComponent(name, component) {
console.log("KZones: Registering QML component:", name);
try {
@@ -49,6 +117,11 @@ export function loadConfig() {
// TODO: Notify user about invalid config and using defaults instead
layouts = defaultLayouts;
}
+ _layoutsRaw = layouts;
+
+ // Any screen-scoped layout forces per-screen tracking; otherwise switching
+ // screens could land on an index hidden by the filter.
+ const anyScopedLayout = layouts.some(l => isArrayLike(l && l.screens) && l.screens.length > 0);
config.enableZoneSelector = KWin.readConfig("enableZoneSelector", true);
config.zoneSelectorTriggerDistance = KWin.readConfig("zoneSelectorTriggerDistance", 1);
@@ -59,7 +132,7 @@ export function loadConfig() {
config.enableEdgeSnapping = KWin.readConfig("enableEdgeSnapping", false);
config.edgeSnappingTriggerDistance = KWin.readConfig("edgeSnappingTriggerDistance", 1);
config.rememberWindowGeometries = KWin.readConfig("rememberWindowGeometries", true);
- config.trackLayoutPerScreen = KWin.readConfig("trackLayoutPerScreen", false);
+ config.trackLayoutPerScreen = KWin.readConfig("trackLayoutPerScreen", false) || anyScopedLayout;
config.trackLayoutPerDesktop = KWin.readConfig("trackLayoutPerDesktop", false);
config.showOsdMessages = KWin.readConfig("showOsdMessages", true);
config.fadeWindowsWhileMoving = KWin.readConfig("fadeWindowsWhileMoving", false);
diff --git a/src/contents/ui/components/Debug.qml b/src/contents/ui/components/Debug.qml
index ccff7d6..b62ee69 100644
--- a/src/contents/ui/components/Debug.qml
+++ b/src/contents/ui/components/Debug.qml
@@ -1,6 +1,6 @@
+import "../components" as Components
import QtQuick
import QtQuick.Layouts
-import "../components" as Components
ColumnLayout {
property var config: new Object()
diff --git a/src/contents/ui/components/Indicator.qml b/src/contents/ui/components/Indicator.qml
index 0c417cf..9d6fcf3 100644
--- a/src/contents/ui/components/Indicator.qml
+++ b/src/contents/ui/components/Indicator.qml
@@ -1,5 +1,5 @@
-import QtQuick
import "../components" as Components
+import QtQuick
Rectangle {
id: indicator
diff --git a/src/contents/ui/components/Selector.qml b/src/contents/ui/components/Selector.qml
index c0cda35..57da70d 100644
--- a/src/contents/ui/components/Selector.qml
+++ b/src/contents/ui/components/Selector.qml
@@ -1,6 +1,6 @@
+import "../components" as Components
import QtQuick
import QtQuick.Layouts
-import "../components" as Components
Item {
id: selector
@@ -8,6 +8,9 @@ Item {
property var config
property int currentLayout
property int highlightedZone
+ // [{ layout, index }, ...] — the layouts to display for the active screen.
+ // `index` is the position in the unfiltered config.layouts.
+ property var availableLayouts: []
property bool expanded: false
property bool near: false
property bool animating: false
@@ -43,14 +46,14 @@ Item {
Repeater {
id: repeater
- model: config.layouts
+ model: availableLayouts
Components.Indicator {
- zones: modelData.zones
- activeZone: (currentLayout == index) ? highlightedZone : -1
+ zones: modelData.layout.zones
+ activeZone: (currentLayout === modelData.index) ? highlightedZone : -1
width: 160 - 30
height: 100 - 30
- hovering: (currentLayout == index)
+ hovering: (currentLayout === modelData.index)
}
}
diff --git a/src/contents/ui/components/Shortcuts.qml b/src/contents/ui/components/Shortcuts.qml
index 69f60c2..66a09ab 100644
--- a/src/contents/ui/components/Shortcuts.qml
+++ b/src/contents/ui/components/Shortcuts.qml
@@ -17,6 +17,7 @@ Item {
signal moveActiveWindowRight()
signal snapActiveWindow()
signal snapAllWindows()
+ signal showDetectedMonitors()
ShortcutHandler {
name: "KZones: Cycle layouts"
@@ -169,4 +170,14 @@ Item {
}
}
+ ShortcutHandler {
+ name: "KZones: Show detected monitors"
+ text: "KZones: Show detected monitors"
+ // No default binding — bind in System Settings → Shortcuts.
+ sequence: ""
+ onActivated: {
+ showDetectedMonitors();
+ }
+ }
+
}
diff --git a/src/contents/ui/components/Zones.qml b/src/contents/ui/components/Zones.qml
index 1fa5837..c5e5db5 100644
--- a/src/contents/ui/components/Zones.qml
+++ b/src/contents/ui/components/Zones.qml
@@ -1,6 +1,6 @@
+import "../components" as Components
import QtQuick
import QtQuick.Layouts
-import "../components" as Components
Item {
id: zones
diff --git a/src/contents/ui/config.ui b/src/contents/ui/config.ui
index 80866a8..2949054 100644
--- a/src/contents/ui/config.ui
+++ b/src/contents/ui/config.ui
@@ -361,6 +361,16 @@
Layouts
+ -
+
+
+ Add an optional "screens" array (e.g. ["DP-1", "HDMI-A-1"]) to a layout to restrict it to specific monitors. Omit or leave empty to show on every monitor. Bind the "Show detected monitors" shortcut in System Settings → Shortcuts to discover connector names.
+
+
+ true
+
+
+
-
diff --git a/src/contents/ui/main.qml b/src/contents/ui/main.qml
index ea9b7d3..00f64ef 100644
--- a/src/contents/ui/main.qml
+++ b/src/contents/ui/main.qml
@@ -1,11 +1,11 @@
-import QtQuick
-import QtQuick.Layouts
-import org.kde.plasma.core as PlasmaCore
-import org.kde.plasma.components as PlasmaComponents
-import org.kde.kwin
import "../code/core.mjs" as Core
import "../code/utils.mjs" as Utils
+import QtQuick
+import QtQuick.Layouts
import "components" as Components
+import org.kde.kwin
+import org.kde.plasma.components as PlasmaComponents
+import org.kde.plasma.core as PlasmaCore
Item {
id: root
@@ -21,15 +21,35 @@ Item {
property var screenLayouts: new Object()
property int highlightedZone: -1
property var activeScreen: null
+ // [{ layout, index }, ...] — layouts visible on activeScreen. `index` is
+ // the position in the unfiltered config.layouts so existing references
+ // (client.layout, repeaterLayout.itemAt(...), etc.) stay valid.
+ property var availableLayouts: []
property bool showZoneOverlay: config.zoneOverlayShowWhen == 0
function refreshClientArea() {
activeScreen = Workspace.activeScreen;
clientArea = Workspace.clientArea(KWin.FullScreenArea, activeScreen, Workspace.currentDesktop);
displaySize = Workspace.virtualScreenSize;
+ refreshAvailableLayouts();
currentLayout = getCurrentLayout();
}
+ function refreshAvailableLayouts() {
+ availableLayouts = Core.getLayoutsForScreen(Core.getScreenId(activeScreen));
+ }
+
+ function firstAvailableIndex() {
+ return availableLayouts.length > 0 ? availableLayouts[0].index : 0;
+ }
+
+ function isLayoutAvailable(idx) {
+ for (let i = 0; i < availableLayouts.length; i++) if (availableLayouts[i].index === idx) {
+ return true;
+ }
+ return false;
+ }
+
function matchZone(client) {
refreshClientArea();
client.zone = -1;
@@ -313,15 +333,21 @@ Item {
function getCurrentLayout() {
if (config.trackLayoutPerScreen || config.trackLayoutPerDesktop) {
const key = getLayoutKey();
- if (!screenLayouts[key])
- screenLayouts[key] = 0;
+ if (screenLayouts[key] === undefined || !isLayoutAvailable(screenLayouts[key]))
+ screenLayouts[key] = firstAvailableIndex();
return screenLayouts[key];
}
+ if (!isLayoutAvailable(currentLayout))
+ return firstAvailableIndex();
+
return currentLayout;
}
function setCurrentLayout(layout) {
+ if (!isLayoutAvailable(layout))
+ return ;
+
if (config.trackLayoutPerScreen || config.trackLayoutPerDesktop)
screenLayouts[getLayoutKey()] = layout;
@@ -554,13 +580,13 @@ Item {
// zone selector
if (config.enableZoneSelector) {
if (!zoneSelector.animating && zoneSelector.expanded) {
- zoneSelector.repeater.model.forEach((layout, layoutIndex) => {
- const layoutItem = zoneSelector.repeater.itemAt(layoutIndex);
- layout.zones.forEach((zone, zoneIndex) => {
+ zoneSelector.repeater.model.forEach((entry, repeaterIndex) => {
+ const layoutItem = zoneSelector.repeater.itemAt(repeaterIndex);
+ entry.layout.zones.forEach((zone, zoneIndex) => {
const zoneItem = layoutItem.children[zoneIndex];
if (Utils.isHovering(zoneItem)) {
hoveringZone = zoneIndex;
- setCurrentLayout(layoutIndex);
+ setCurrentLayout(entry.index);
}
});
});
@@ -645,7 +671,10 @@ Item {
"oldGeometry": Workspace.activeWindow && Workspace.activeWindow.oldGeometry,
"activeScreen": activeScreen && activeScreen.name,
"currentLayout": currentLayout,
- "screenLayouts": screenLayouts
+ "screenLayouts": screenLayouts,
+ "availableLayouts": availableLayouts.map((e) => {
+ return e.index + ":" + e.layout.name;
+ })
})
config: root.config
}
@@ -662,7 +691,7 @@ Item {
currentLayout: root.currentLayout
highlightedZone: root.highlightedZone
layoutIndex: index
- visible: index == root.currentLayout
+ visible: index === root.currentLayout && root.isLayoutAvailable(index)
}
}
@@ -673,6 +702,7 @@ Item {
config: root.config
currentLayout: root.currentLayout
highlightedZone: root.highlightedZone
+ availableLayouts: root.availableLayouts
}
}
@@ -683,12 +713,26 @@ Item {
Components.Shortcuts {
onCycleLayouts: {
- setCurrentLayout((currentLayout + 1) % config.layouts.length);
+ if (availableLayouts.length === 0)
+ return ;
+
+ const pos = availableLayouts.findIndex((e) => {
+ return e.index === currentLayout;
+ });
+ const next = availableLayouts[(pos + 1) % availableLayouts.length].index;
+ setCurrentLayout(next);
highlightedZone = -1;
Utils.osd(osdLayoutName());
}
onCycleLayoutsReversed: {
- setCurrentLayout((currentLayout - 1 + config.layouts.length) % config.layouts.length);
+ if (availableLayouts.length === 0)
+ return ;
+
+ const pos = availableLayouts.findIndex((e) => {
+ return e.index === currentLayout;
+ });
+ const prev = availableLayouts[(pos - 1 + availableLayouts.length) % availableLayouts.length].index;
+ setCurrentLayout(prev);
highlightedZone = -1;
Utils.osd(osdLayoutName());
}
@@ -726,12 +770,13 @@ Item {
moveClientToZone(Workspace.activeWindow, zone);
}
onActivateLayout: {
- if (layout <= config.layouts.length - 1) {
- setCurrentLayout(layout);
+ if (layout >= 0 && layout < availableLayouts.length) {
+ setCurrentLayout(availableLayouts[layout].index);
highlightedZone = -1;
Utils.osd(osdLayoutName());
} else {
- Utils.osd(`Layout ${layout + 1} does not exist`);
+ const screenName = activeScreen && activeScreen.name ? activeScreen.name : "this screen";
+ Utils.osd(`Layout ${layout + 1} does not exist on ${screenName}`);
}
}
onMoveActiveWindowUp: {
@@ -752,6 +797,17 @@ Item {
onSnapAllWindows: {
moveAllClientsToClosestZone();
}
+ onShowDetectedMonitors: {
+ const screens = Core.getDetectedScreens();
+ if (screens.length === 0) {
+ Utils.osd("KZones: no monitors detected");
+ return ;
+ }
+ const summary = screens.map((s) => {
+ return `${s.name} (${s.width}x${s.height})`;
+ }).join(" ");
+ Utils.osd("Monitors: " + summary);
+ }
}
DBusCall {
@@ -778,6 +834,14 @@ Item {
}
+ function onActiveScreenChanged() {
+ refreshClientArea();
+ }
+
+ function onScreensChanged() {
+ refreshClientArea();
+ }
+
function onWindowAdded(client) {
connectSignals(client);
// check if client is in a zone application list
diff --git a/src/metadata.json b/src/metadata.json
index 0426b47..91256cf 100644
--- a/src/metadata.json
+++ b/src/metadata.json
@@ -16,7 +16,7 @@
"ServiceTypes": [
"KWin/Script"
],
- "Version": "0.9.3",
+ "Version": "0.10.0",
"Website": "https://github.com/gerritdevriese/kzones"
},
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted",