Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
contents/ui/config_ui.py
KZones.kwinscript
kzones.kwinscript
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -323,6 +324,7 @@ List of all available shortcuts:
| Move active window right | <kbd>Meta</kbd> + <kbd>Right</kbd> |
| Snap all windows | <kbd>Meta</kbd> + <kbd>Space</kbd> |
| Snap active window | <kbd>Meta</kbd> + <kbd>Shift</kbd> + <kbd>Space</kbd> |
| Show detected monitors (OSD) | *(unbound — set in System Settings → Shortcuts)* |

*To change the default bindings, go to `System Settings / Shortcuts` and search for KZones*

Expand Down
75 changes: 74 additions & 1 deletion src/contents/code/core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/contents/ui/components/Debug.qml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "../components" as Components
import QtQuick
import QtQuick.Layouts
import "../components" as Components

ColumnLayout {
property var config: new Object()
Expand Down
2 changes: 1 addition & 1 deletion src/contents/ui/components/Indicator.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import QtQuick
import "../components" as Components
import QtQuick

Rectangle {
id: indicator
Expand Down
13 changes: 8 additions & 5 deletions src/contents/ui/components/Selector.qml
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import "../components" as Components
import QtQuick
import QtQuick.Layouts
import "../components" as Components

Item {
id: selector

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
Expand Down Expand Up @@ -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)
}

}
Expand Down
11 changes: 11 additions & 0 deletions src/contents/ui/components/Shortcuts.qml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Item {
signal moveActiveWindowRight()
signal snapActiveWindow()
signal snapAllWindows()
signal showDetectedMonitors()

ShortcutHandler {
name: "KZones: Cycle layouts"
Expand Down Expand Up @@ -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();
}
}

}
2 changes: 1 addition & 1 deletion src/contents/ui/components/Zones.qml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "../components" as Components
import QtQuick
import QtQuick.Layouts
import "../components" as Components

Item {
id: zones
Expand Down
10 changes: 10 additions & 0 deletions src/contents/ui/config.ui
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,16 @@
<string>Layouts</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_6">
<item>
<widget class="QLabel" name="label_layoutsScreensExplanation">
<property name="text">
<string>Add an optional &quot;screens&quot; array (e.g. [&quot;DP-1&quot;, &quot;HDMI-A-1&quot;]) to a layout to restrict it to specific monitors. Omit or leave empty to show on every monitor. Bind the &quot;Show detected monitors&quot; shortcut in System Settings → Shortcuts to discover connector names.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="kcfg_layoutsJson">
<property name="plainText">
Expand Down
Loading