Compare commits
274 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21eacd7ba4 | ||
|
|
5004417285 | ||
|
|
a110aee7ce | ||
|
|
817ea64171 | ||
|
|
af930a9b2f | ||
|
|
489a1447e7 | ||
|
|
b984f025ec | ||
|
|
4e1204f1bd | ||
|
|
bbcf51783d | ||
|
|
019da3766e | ||
|
|
1535c994b8 | ||
|
|
296d0deca9 | ||
|
|
17e7d5b46e | ||
|
|
840a50d14d | ||
|
|
4f99c4dd45 | ||
|
|
030eddaf34 | ||
|
|
7246a7660e | ||
|
|
687256d1dd | ||
|
|
12bb7506cc | ||
|
|
1808ee0025 | ||
|
|
3021f61933 | ||
|
|
e908138478 | ||
|
|
99ad115370 | ||
|
|
c5a4238f5f | ||
|
|
0670d9c265 | ||
|
|
845874b0d0 | ||
|
|
a422a077f6 | ||
|
|
2fe1be99cb | ||
|
|
1a449c238d | ||
|
|
9bda7d1a09 | ||
|
|
2ce72bcee8 | ||
|
|
ff3f6c5d6b | ||
|
|
3ab230b498 | ||
|
|
ba9f362a1c | ||
|
|
ad6c3f1cae | ||
|
|
ba4dd2a9c1 | ||
|
|
bb61853009 | ||
|
|
0cfd9b9e36 | ||
|
|
43c4f7ef9a | ||
|
|
9cb3f33ecb | ||
|
|
31b9e61ae3 | ||
|
|
668e6696ab | ||
|
|
e63959cfbf | ||
|
|
ef2650beb8 | ||
|
|
750c47c040 | ||
|
|
88ca0d02e1 | ||
|
|
aba786b754 | ||
|
|
47aa625c99 | ||
|
|
03c7cc6503 | ||
|
|
9e9ff2b74f | ||
|
|
5674624e6f | ||
|
|
44dd88ef7c | ||
|
|
f800d6ecf0 | ||
|
|
3477e17bb3 | ||
|
|
755c781646 | ||
|
|
926345ba31 | ||
|
|
a2295ede43 | ||
|
|
ca80a7ca28 | ||
|
|
64474b1677 | ||
|
|
eca63cbc16 | ||
|
|
3a8baf4cd7 | ||
|
|
dc14171ae7 | ||
|
|
6dcf8979c2 | ||
|
|
fe5661c07f | ||
|
|
90b783b34b | ||
|
|
fb40bd9592 | ||
|
|
768d95450d | ||
|
|
e95a0e44c9 | ||
|
|
c1b8d05919 | ||
|
|
e98ce18105 | ||
|
|
25a9efc8e4 | ||
|
|
db48644944 | ||
|
|
950e0de076 | ||
|
|
05ffe0895e | ||
|
|
61db5ca69f | ||
|
|
f7b5dd0b9c | ||
|
|
f83f60c98f | ||
|
|
3e8734eefb | ||
|
|
bed0ea7ed8 | ||
|
|
92c99f0b87 | ||
|
|
b2024bc8aa | ||
|
|
58f358313b | ||
|
|
352a7061f6 | ||
|
|
7314c0ee24 | ||
|
|
c65361853c | ||
|
|
fa53e765b3 | ||
|
|
4d35681ee2 | ||
|
|
1824bcdf85 | ||
|
|
ce1b402bf2 | ||
|
|
2df6d5d8e6 | ||
|
|
e7d33030ba | ||
|
|
2bd000f0a6 | ||
|
|
6313d8f18e | ||
|
|
464ec3bcb1 | ||
|
|
fae793cb09 | ||
|
|
d7346a6fab | ||
|
|
0e5efd2be7 | ||
|
|
a1a315790e | ||
|
|
9d62499bf0 | ||
|
|
97cf61d1dd | ||
|
|
5d83c6dd2c | ||
|
|
8915e8a9da | ||
|
|
22e4c47189 | ||
|
|
552d2b851f | ||
|
|
c4ce795359 | ||
|
|
1ac1fc3c2b | ||
|
|
d0e041d16a | ||
|
|
8fc3fc976d | ||
|
|
e0eeace9dc | ||
|
|
84e2a06b35 | ||
|
|
3373e02658 | ||
|
|
43f425c868 | ||
|
|
5c5127f7ce | ||
|
|
8664f05998 | ||
|
|
e4f6a32d42 | ||
|
|
2882bb8d5d | ||
|
|
5ef71c92ce | ||
|
|
de59979a6e | ||
|
|
71d7d60837 | ||
|
|
7f44d23dd0 | ||
|
|
aceffae5f9 | ||
|
|
09e5eaca88 | ||
|
|
734dd8a4cc | ||
|
|
aeabb396f9 | ||
|
|
05b9ebc325 | ||
|
|
4728afb5ea | ||
|
|
653cb20e43 | ||
|
|
0cb2b68bf6 | ||
|
|
b8b8900754 | ||
|
|
14c006b5b1 | ||
|
|
9fe6be9b91 | ||
|
|
97d1592318 | ||
|
|
cc8cc04b05 | ||
|
|
cc74d3610a | ||
|
|
088725402e | ||
|
|
25b7507b30 | ||
|
|
9b32caafdc | ||
|
|
668d579d63 | ||
|
|
f4e9822f29 | ||
|
|
bdb0a4aeb0 | ||
|
|
dde1a12fce | ||
|
|
c57c8391fb | ||
|
|
ec64b47ceb | ||
|
|
b055345e48 | ||
|
|
eb43f45287 | ||
|
|
e61d7538b2 | ||
|
|
a3c0976f55 | ||
|
|
b372489eb5 | ||
|
|
fa974a68aa | ||
|
|
20517aed7f | ||
|
|
b49082d51d | ||
|
|
08135a4ad4 | ||
|
|
681ae38d85 | ||
|
|
9912a8d917 | ||
|
|
1bcf768588 | ||
|
|
c84fddc618 | ||
|
|
ad62dafdc7 | ||
|
|
425c5c9e5b | ||
|
|
4b0f259c6d | ||
|
|
a5ecc94479 | ||
|
|
a3f479e2e6 | ||
|
|
daef95731b | ||
|
|
3b3bb679de | ||
|
|
566c8fe53d | ||
|
|
07ce7d4e60 | ||
|
|
0dfc29b1eb | ||
|
|
4d784c5d01 | ||
|
|
a4d27a2885 | ||
|
|
f703f0655a | ||
|
|
963949b039 | ||
|
|
22ee707207 | ||
|
|
8d6e4f9bc7 | ||
|
|
075f6c7e3d | ||
|
|
5404b61d20 | ||
|
|
76b0016055 | ||
|
|
64cdb90f4a | ||
|
|
80ecc7e6c9 | ||
|
|
0e59f382b4 | ||
|
|
6001dd5b02 | ||
|
|
201dd4463e | ||
|
|
10718bc2c7 | ||
|
|
b15bb85037 | ||
|
|
4904d075ae | ||
|
|
7871bbbe6d | ||
|
|
d91ea7b412 | ||
|
|
37e9b85279 | ||
|
|
0bdb4af0e6 | ||
|
|
4f06f17ba7 | ||
|
|
382cbe101b | ||
|
|
e580acf979 | ||
|
|
453c4ece2c | ||
|
|
15b77d0207 | ||
|
|
d23c13c344 | ||
|
|
c4307e187f | ||
|
|
463da59197 | ||
|
|
c5ec40e5ea | ||
|
|
2f4268fc94 | ||
|
|
f1911b1247 | ||
|
|
5b71f1c48f | ||
|
|
c7e7b91f3f | ||
|
|
4b3a403559 | ||
|
|
9477b7e337 | ||
|
|
c87ef982ae | ||
|
|
de3e78424a | ||
|
|
3039033ea9 | ||
|
|
de0f89062a | ||
|
|
0831c1be8b | ||
|
|
d6bfe2fd03 | ||
|
|
048bf2a51a | ||
|
|
a949dca458 | ||
|
|
57c4643098 | ||
|
|
e92563b424 | ||
|
|
671326bdd7 | ||
|
|
5e9db7d2cd | ||
|
|
b447eacdfd | ||
|
|
94f6e6f33b | ||
|
|
85b0221220 | ||
|
|
1894b055f7 | ||
|
|
05f7550a3b | ||
|
|
a04f629de0 | ||
|
|
4bda4d0d7c | ||
|
|
8bf076948a | ||
|
|
04bd85a287 | ||
|
|
b8cf677084 | ||
|
|
e67e6f4d62 | ||
|
|
bdf62b65e4 | ||
|
|
3238d70772 | ||
|
|
8382696d01 | ||
|
|
18470b391f | ||
|
|
ed8ec7c794 | ||
|
|
fce442c25d | ||
|
|
81ef0e0442 | ||
|
|
0fbb0fe90e | ||
|
|
b12b70a294 | ||
|
|
0a3ba5c963 | ||
|
|
fa17b1fcc2 | ||
|
|
68d2c5bbd8 | ||
|
|
5c8da41647 | ||
|
|
9e808f99c9 | ||
|
|
29fa65613e | ||
|
|
9c1592b626 | ||
|
|
dec4281bb9 | ||
|
|
41facafac7 | ||
|
|
0635e20622 | ||
|
|
454a14724d | ||
|
|
0fff1ce837 | ||
|
|
ee8bac5a42 | ||
|
|
dba5e07a86 | ||
|
|
e2a5625d41 | ||
|
|
6e9edad39d | ||
|
|
0266cde2f1 | ||
|
|
f0e662de37 | ||
|
|
e5c9b52370 | ||
|
|
83ac2506cf | ||
|
|
d8eec7a881 | ||
|
|
cb66a26394 | ||
|
|
414bfc8518 | ||
|
|
81a82cbfde | ||
|
|
9318799a82 | ||
|
|
2f93e3aa8f | ||
|
|
e1263dd544 | ||
|
|
b614fd4481 | ||
|
|
13d6f39bf2 | ||
|
|
3b103841c2 | ||
|
|
9ec9e8e62d | ||
|
|
208ef7d9fb | ||
|
|
b7610be193 | ||
|
|
ed9d4320ae | ||
|
|
e3a6b1ad91 | ||
|
|
31a56b8f24 | ||
|
|
a65a62f396 | ||
|
|
88b976b252 | ||
|
|
4bdc031d7b | ||
|
|
0cf395d2e1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
/package/contents/code/main.js
|
||||
/package/contents/config/main.xml
|
||||
/karousel.tar.gz
|
||||
/karousel*.tar.gz
|
||||
/.idea
|
||||
|
||||
18
Makefile
18
Makefile
@@ -1,8 +1,11 @@
|
||||
.PHONY: *
|
||||
|
||||
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
|
||||
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
|
||||
|
||||
config:
|
||||
mkdir -p ./package/contents/config
|
||||
tsc ./src/config/definition.ts ./configgen/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
|
||||
tsc ${TSC_SCRIPT_FLAGS} ./src/config/definition.ts ./generators/config/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
|
||||
|
||||
build:
|
||||
tsc --outFile ./package/contents/code/main.js
|
||||
@@ -13,8 +16,17 @@ install: build config
|
||||
uninstall:
|
||||
kpackagetool5 --type=KWin/Script -r ./package
|
||||
|
||||
package:
|
||||
tar -czf ./karousel.tar.gz ./package
|
||||
package: build config
|
||||
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package --transform s/package/karousel/
|
||||
|
||||
logs:
|
||||
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
|
||||
|
||||
docs-key-bindings-bbcode:
|
||||
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsBbcode.ts --outFile /dev/stdout | node -
|
||||
|
||||
docs-key-bindings-table:
|
||||
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsTable.ts --outFile /dev/stdout | node -
|
||||
|
||||
docs-key-bindings-fmt:
|
||||
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsFmt.ts --outFile /dev/stdout | node -
|
||||
|
||||
48
README.md
48
README.md
@@ -13,10 +13,56 @@ unprompted reflow of window content.
|
||||
Windows are automatically centered when possible. And when running out of width, windows can be
|
||||
scrolled through horizontally.
|
||||
|
||||
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and
|
||||
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
|
||||
[Niri](https://github.com/YaLTeR/niri), and
|
||||
[Cardboard](https://gitlab.com/cardboardwm/cardboard).
|
||||
|
||||
## Dependencies
|
||||
Karousel requires the following QML modules:
|
||||
- QtQuick 2.15
|
||||
- org.kde.kwin 3.0
|
||||
- org.kde.notification 1.0
|
||||
|
||||
## Limitations
|
||||
- Doesn't support multiple screens
|
||||
- Doesn't support windows on all desktops
|
||||
- Doesn't support windows on multiple activities
|
||||
|
||||
## Key bindings
|
||||
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
|
||||
Here's the default ones:
|
||||
| Shortcut | Action |
|
||||
| --- | --- |
|
||||
| Meta+Space | Toggle floating |
|
||||
| Meta+A | Move focus left |
|
||||
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||
| Meta+Home | Move focus to start |
|
||||
| Meta+End | Move focus to end |
|
||||
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
|
||||
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
|
||||
| Meta+Shift+W | Move window up |
|
||||
| Meta+Shift+S | Move window down |
|
||||
| Meta+Shift+Home | Move window to start |
|
||||
| Meta+Shift+End | Move window to end |
|
||||
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
|
||||
| Meta+Ctrl+Shift+A | Move column left |
|
||||
| Meta+Ctrl+Shift+D | Move column right |
|
||||
| Meta+Ctrl+Shift+Home | Move column to start |
|
||||
| Meta+Ctrl+Shift+End | Move column to end |
|
||||
| Meta+Ctrl++ | Increase column width |
|
||||
| Meta+Ctrl+- | Decrease column width |
|
||||
| Meta+Ctrl+X | Equalize widths of visible columns |
|
||||
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
|
||||
| Meta+Alt+A | Scroll one column to the left |
|
||||
| Meta+Alt+D | Scroll one column to the right |
|
||||
| Meta+Alt+PgUp | Scroll left |
|
||||
| Meta+Alt+PgDown | Scroll right |
|
||||
| Meta+Alt+Home | Scroll to start |
|
||||
| Meta+Alt+End | Scroll to end |
|
||||
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
|
||||
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |
|
||||
| Meta+Ctrl+Shift+F[N] | Move column to desktop N |
|
||||
| Meta+Ctrl+Shift+Alt+F[N] | Move this and all following columns to desktop N |
|
||||
|
||||
@@ -5,9 +5,20 @@ console.log(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
for (const entry of configDef) {
|
||||
console.log(` <entry name="${entry.name}" type="${entry.type}">
|
||||
<default>${entry.default}</default>
|
||||
<default>${escapeXml(entry.default)}</default>
|
||||
</entry>`);
|
||||
}
|
||||
|
||||
console.log(` </group>
|
||||
</kcfg>`);
|
||||
|
||||
function escapeXml(input: any) {
|
||||
if (typeof input === "string") {
|
||||
return input
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
67
generators/docs/keyBindings.ts
Normal file
67
generators/docs/keyBindings.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
type KeyBinding = {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultKeySequence: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
type NumKeyBinding = {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultModifiers: string;
|
||||
fKeys: boolean;
|
||||
action: string;
|
||||
}
|
||||
|
||||
function formatComment(comment: string | undefined) {
|
||||
return comment === undefined ? "" : ` (${comment})`;
|
||||
}
|
||||
|
||||
function printCols(...columns: (string[] | string)[]) {
|
||||
const nCols = columns.length;
|
||||
if (nCols === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nRows = Math.min(...columns.filter(
|
||||
(column: string[] | string) => column instanceof Array
|
||||
).map(
|
||||
(column: string[] | string) => column.length
|
||||
));
|
||||
if (nRows === Infinity) {
|
||||
// we only have single string columns
|
||||
nRows = 1;
|
||||
}
|
||||
|
||||
const colWidths = columns.map(
|
||||
(column: string[] | string) => {
|
||||
if (column instanceof Array) {
|
||||
return Math.max(...column.map(
|
||||
(cell: string) => cell.length
|
||||
))
|
||||
} else {
|
||||
return column.length;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getCell(col: number, row: number) {
|
||||
const column = columns[col];
|
||||
const cell = column instanceof Array ? column[row] : column;
|
||||
if (col < nCols-1) {
|
||||
return cell.padEnd(colWidths[col]);
|
||||
} else {
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
for (let row = 0; row < nRows; row++) {
|
||||
let line = "";
|
||||
for (let col = 0; col < nCols; col++) {
|
||||
line += getCell(col, row);
|
||||
}
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
12
generators/docs/keyBindingsBbcode.ts
Normal file
12
generators/docs/keyBindingsBbcode.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
console.log(`[list]`);
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
console.log(` [*] ${binding.defaultKeySequence} — ${binding.description}${formatComment(binding.comment)}`);
|
||||
}
|
||||
|
||||
for (const binding of numKeyBindings) {
|
||||
const numPrefix = binding.fKeys ? "F" : "";
|
||||
console.log(` [*] ${binding.defaultModifiers}+${numPrefix}[N] — ${binding.description}N${formatComment(binding.comment)}`);
|
||||
}
|
||||
|
||||
console.log(`[/list]`);
|
||||
14
generators/docs/keyBindingsFmt.ts
Normal file
14
generators/docs/keyBindingsFmt.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const colLeft = [
|
||||
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
|
||||
...numKeyBindings.map((binding: NumKeyBinding) => {
|
||||
const numPrefix = binding.fKeys ? "F" : "";
|
||||
return `${binding.defaultModifiers}+${numPrefix}[N]`;
|
||||
}),
|
||||
];
|
||||
|
||||
const colRight = [
|
||||
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
|
||||
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
|
||||
];
|
||||
|
||||
printCols(colLeft, " ", colRight);
|
||||
18
generators/docs/keyBindingsTable.ts
Normal file
18
generators/docs/keyBindingsTable.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
const colLeft = [
|
||||
"Shortcut",
|
||||
"---",
|
||||
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
|
||||
...numKeyBindings.map((binding: NumKeyBinding) => {
|
||||
const numPrefix = binding.fKeys ? "F" : "";
|
||||
return `${binding.defaultModifiers}+${numPrefix}[N]`;
|
||||
}),
|
||||
];
|
||||
|
||||
const colRight = [
|
||||
"Action",
|
||||
"---",
|
||||
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
|
||||
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
|
||||
];
|
||||
|
||||
printCols("| ", colLeft, " | ", colRight, " |");
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick 2.15
|
||||
import org.kde.kwin 3.0
|
||||
import org.kde.notification 1.0
|
||||
import "./main.js" as Karousel
|
||||
|
||||
Item {
|
||||
@@ -9,11 +10,19 @@ Item {
|
||||
|
||||
Component.onCompleted: {
|
||||
qmlBase.karouselInstance = Karousel.init();
|
||||
print("script started");
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
qmlBase.karouselInstance.destroy();
|
||||
print("script stopped");
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: notificationInvalidWindowRules
|
||||
componentName: "plasma_workspace"
|
||||
eventId: "notification"
|
||||
title: "Karousel"
|
||||
text: "Your Window Rules JSON is malformed, please review your Karousel configuration"
|
||||
flags: Notification.Persistent
|
||||
urgency: Notification.HighUrgency
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,19 +11,146 @@
|
||||
<layout class="QVBoxLayout" name="layout_main">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabContainer">
|
||||
<widget class="QWidget" name="tab_general">
|
||||
<widget class="QWidget" name="tab_behavior">
|
||||
<attribute name="title">
|
||||
<string>General</string>
|
||||
<string>Behavior</string>
|
||||
</attribute>
|
||||
<layout class="QGridLayout" name="layout_tab_general" columnstretch="0,1">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox">
|
||||
<property name="flat">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_untileOnDrag">
|
||||
<property name="text">
|
||||
<string>Un-tile windows by dragging them</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
|
||||
<property name="text">
|
||||
<string>Stack columns by default</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
|
||||
<property name="text">
|
||||
<string>Resize neighbor column on edge resize</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_reMaximize">
|
||||
<property name="text">
|
||||
<string>Re-maximize tiled windows</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Restore maximized and full-screen states of tiled windows on focus</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_skipSwitcher">
|
||||
<property name="text">
|
||||
<string>Tiled windows skip switcher</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<widget class="QGroupBox">
|
||||
<property name="title">
|
||||
<string>Scrolling mode</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_scrollingLazy">
|
||||
<property name="text">
|
||||
<string>Only scroll as necessary</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_scrollingCentered">
|
||||
<property name="text">
|
||||
<string>Center focused column</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_scrollingGrouped">
|
||||
<property name="text">
|
||||
<string>Center visible columns</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<widget class="QGroupBox">
|
||||
<property name="title">
|
||||
<string>Layering mode</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
|
||||
<property name="text">
|
||||
<string>Keep tiled windows below</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
|
||||
<property name="text">
|
||||
<string>Keep floating windows above</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<spacer name="spacer_footer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
||||
<widget class="QWidget" name="tab_parameters">
|
||||
<attribute name="title">
|
||||
<string>Parameters</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_gapsOuterTop">
|
||||
<property name="text">
|
||||
<string>Top margin:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
@@ -45,9 +172,6 @@
|
||||
<property name="text">
|
||||
<string>Bottom margin:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
@@ -69,9 +193,6 @@
|
||||
<property name="text">
|
||||
<string>Left margin:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
@@ -93,9 +214,6 @@
|
||||
<property name="text">
|
||||
<string>Right margin:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
@@ -117,9 +235,6 @@
|
||||
<property name="text">
|
||||
<string>Horizontal gaps between windows:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
@@ -141,9 +256,6 @@
|
||||
<property name="text">
|
||||
<string>Vertical gaps between windows:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
@@ -161,40 +273,13 @@
|
||||
</item>
|
||||
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_overscroll">
|
||||
<property name="text">
|
||||
<string>Overscroll amount:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_overscroll">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_manualScrollStep">
|
||||
<property name="text">
|
||||
<string>Manual scroll step size:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_manualScrollStep">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
@@ -208,28 +293,55 @@
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item row="8" column="1">
|
||||
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_manualResizeStep">
|
||||
<property name="text">
|
||||
<string>Stack columns by default</string>
|
||||
<string>Manual resize step size:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_manualResizeStep">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>999</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item row="9" column="0" colspan="2">
|
||||
<spacer name="bottomSpacer_tab_general">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_offScreenOpacity">
|
||||
<property name="text">
|
||||
<string>Obscured window opacity:</string>
|
||||
</property>
|
||||
</spacer>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
|
||||
<property name="suffix">
|
||||
<string> %</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
||||
<widget class="QWidget" name="tab_windowRules">
|
||||
<attribute name="title">
|
||||
<string>Window rules</string>
|
||||
<string>Window Rules</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="layout_tab_windowRules">
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="kcfg_windowRules">
|
||||
<property name="tabChangesFocus">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"KPlugin": {
|
||||
"Name": "Karousel",
|
||||
"Description": "Manual columnar tiling extension for KWin",
|
||||
"Description": "Scrollable tiling extension for KWin",
|
||||
"Icon": "preferences-system-windows",
|
||||
"Authors": [{
|
||||
"Email": "peter.fajdiga@gmail.com",
|
||||
@@ -9,7 +9,7 @@
|
||||
}],
|
||||
"Id": "karousel",
|
||||
"ServiceTypes": ["KWin/Script"],
|
||||
"Version": "0.1",
|
||||
"Version": "0.7.2",
|
||||
"License": "GPLv3",
|
||||
"Website": "https://github.com/peterfajdiga/karousel",
|
||||
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
|
||||
|
||||
336
src/Actions.ts
Normal file
336
src/Actions.ts
Normal file
@@ -0,0 +1,336 @@
|
||||
namespace Actions {
|
||||
export function init(world: World, config: Config) {
|
||||
return {
|
||||
focusLeft: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
prevColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusRight: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
nextColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusUp: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const prevWindow = column.getPrevWindow(window);
|
||||
if (prevWindow === null) {
|
||||
return;
|
||||
}
|
||||
prevWindow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusDown: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const nextWindow = column.getNextWindow(window);
|
||||
if (nextWindow === null) {
|
||||
return;
|
||||
}
|
||||
nextWindow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusStart: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
firstColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusEnd: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
lastColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveLeft: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(prevColumn);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, grid.getPrevColumn(column));
|
||||
window.moveToColumn(newColumn);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveRight: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(nextColumn);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, column);
|
||||
window.moveToColumn(newColumn);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveUp: () => {
|
||||
// TODO (optimization): only arrange moved windows
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
column.moveWindowUp(window);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveDown: () => {
|
||||
// TODO (optimization): only arrange moved windows
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
column.moveWindowDown(window);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const newColumn = new Column(grid, null);
|
||||
window.moveToColumn(newColumn);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const newColumn = new Column(grid, grid.getLastColumn());
|
||||
window.moveToColumn(newColumn);
|
||||
});
|
||||
},
|
||||
|
||||
windowToggleFloating: () => {
|
||||
const kwinClient = workspace.activeClient;
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.toggleFloatingClient(kwinClient);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveLeft: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
grid.moveColumnLeft(column);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveRight: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
grid.moveColumnRight(column);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
column.moveAfter(null);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
column.moveAfter(grid.getLastColumn());
|
||||
});
|
||||
},
|
||||
|
||||
columnToggleStacked: () => {
|
||||
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
|
||||
column.toggleStacked();
|
||||
});
|
||||
},
|
||||
|
||||
columnWidthIncrease: () => {
|
||||
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
|
||||
config.columnResizer.increaseWidth(column, config.manualResizeStep);
|
||||
});
|
||||
},
|
||||
|
||||
columnWidthDecrease: () => {
|
||||
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
|
||||
config.columnResizer.decreaseWidth(column, config.manualResizeStep);
|
||||
});
|
||||
},
|
||||
|
||||
columnsWidthEqualize: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
|
||||
});
|
||||
},
|
||||
|
||||
gridScrollLeft: () => {
|
||||
gridScroll(world, -config.manualScrollStep);
|
||||
},
|
||||
|
||||
gridScrollRight: () => {
|
||||
gridScroll(world, config.manualScrollStep);
|
||||
},
|
||||
|
||||
gridScrollStart: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.desktop.scrollToColumn(firstColumn);
|
||||
});
|
||||
},
|
||||
|
||||
gridScrollEnd: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.desktop.scrollToColumn(lastColumn);
|
||||
});
|
||||
},
|
||||
|
||||
gridScrollFocused: () => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
grid.desktop.scrollCenterRange(column);
|
||||
})
|
||||
},
|
||||
|
||||
gridScrollLeftColumn: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.desktop.scrollToColumn(prevColumn);
|
||||
});
|
||||
},
|
||||
|
||||
gridScrollRightColumn: () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.desktop.scrollToColumn(nextColumn);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function initNum(world: World) {
|
||||
return {
|
||||
focusColumn: (columnIndex: number) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
targetColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(targetColumn);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null || targetColumn === column) {
|
||||
return;
|
||||
}
|
||||
if (targetColumn.isAfter(column)) {
|
||||
column.moveAfter(targetColumn);
|
||||
} else {
|
||||
column.moveAfter(grid.getPrevColumn(targetColumn));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveToDesktop: (desktopIndex: number) => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
|
||||
const desktopNumber = desktopIndex + 1;
|
||||
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
column.moveToGrid(newGrid, newGrid.getLastColumn());
|
||||
});
|
||||
},
|
||||
|
||||
tailMoveToDesktop: (desktopIndex: number) => {
|
||||
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
|
||||
const desktopNumber = desktopIndex + 1;
|
||||
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
oldGrid.evacuateTail(newGrid, column);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gridScroll(world: World, amount: number) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const grid = desktopManager.getCurrentDesktop().grid;
|
||||
grid.desktop.adjustScroll(amount, false);
|
||||
});
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
manualScrollStep: number,
|
||||
manualResizeStep: number,
|
||||
columnResizer: ColumnResizer,
|
||||
};
|
||||
|
||||
export type ColumnResizer = {
|
||||
increaseWidth(column: Column, step: number): void,
|
||||
decreaseWidth(column: Column, step: number): void,
|
||||
}
|
||||
}
|
||||
338
src/actions.ts
338
src/actions.ts
@@ -1,338 +0,0 @@
|
||||
function initActions(world: World) {
|
||||
return {
|
||||
focusLeft: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
prevColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusRight: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
nextColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusUp: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const prevWindow = column.getPrevWindow(window);
|
||||
if (prevWindow === null) {
|
||||
return;
|
||||
}
|
||||
prevWindow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusDown: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const nextWindow = column.getNextWindow(window);
|
||||
if (nextWindow === null) {
|
||||
return;
|
||||
}
|
||||
nextWindow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusStart: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
firstColumn.focus();
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
focusEnd: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
lastColumn.focus();
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
windowMoveLeft: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(prevColumn);
|
||||
grid.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, grid.getPrevColumn(column));
|
||||
window.moveToColumn(newColumn);
|
||||
}
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveRight: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(nextColumn);
|
||||
grid.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, column);
|
||||
window.moveToColumn(newColumn);
|
||||
}
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveUp: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
column.moveWindowUp(window);
|
||||
grid.arrange(); // TODO (optimization): only arrange moved windows
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveDown: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
column.moveWindowDown(window);
|
||||
grid.arrange(); // TODO (optimization): only arrange moved windows
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const newColumn = new Column(grid, null);
|
||||
window.moveToColumn(newColumn);
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const newColumn = new Column(grid, grid.getLastColumn());
|
||||
window.moveToColumn(newColumn);
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
windowExpand: () => {
|
||||
world.doIfTiledFocused(false, (window, column, grid) => {
|
||||
column.toggleStacked();
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
windowToggleFloating: () => {
|
||||
const kwinClient = workspace.activeClient;
|
||||
world.toggleFloatingClient(kwinClient);
|
||||
},
|
||||
|
||||
columnMoveLeft: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
grid.moveColumnLeft(column);
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveRight: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
grid.moveColumnRight(column);
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
column.moveAfter(null);
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
column.moveAfter(grid.getLastColumn());
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnExpand: () => {
|
||||
world.doIfTiledFocused(false, (window, column, grid) => {
|
||||
column.expand();
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
gridScrollLeft: () => {
|
||||
gridScroll(world, -world.config.manualScrollStep);
|
||||
},
|
||||
|
||||
gridScrollRight: () => {
|
||||
gridScroll(world, world.config.manualScrollStep);
|
||||
},
|
||||
|
||||
gridScrollStart: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.scrollToColumn(firstColumn);
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
gridScrollEnd: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.scrollToColumn(lastColumn);
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
gridScrollFocused: () => {
|
||||
const focusedWindow = world.getFocusedWindow();
|
||||
if (focusedWindow === null) {
|
||||
return;
|
||||
}
|
||||
const column = focusedWindow.column;
|
||||
const grid = column.grid;
|
||||
grid.scrollToColumn(column);
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
gridScrollLeftColumn: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const column = grid.getLeftmostVisibleColumn(true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.scrollToColumn(prevColumn);
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
gridScrollRightColumn: () => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const column = grid.getRightmostVisibleColumn(true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.scrollToColumn(nextColumn);
|
||||
grid.arrange();
|
||||
},
|
||||
|
||||
focusColumn: (columnIndex: number) => {
|
||||
const grid = world.getCurrentGrid();
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return null;
|
||||
}
|
||||
targetColumn.focus();
|
||||
},
|
||||
|
||||
windowMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return null;
|
||||
}
|
||||
window.moveToColumn(targetColumn);
|
||||
grid.autoAdjustScroll();
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (window, column, grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null || targetColumn === column) {
|
||||
return null;
|
||||
}
|
||||
if (targetColumn.isAfter(column)) {
|
||||
column.moveAfter(targetColumn);
|
||||
} else {
|
||||
column.moveAfter(grid.getPrevColumn(targetColumn));
|
||||
}
|
||||
grid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveToDesktop: (desktopIndex: number) => {
|
||||
world.doIfTiledFocused(true, (window, column, oldGrid) => {
|
||||
const desktopNumber = desktopIndex + 1;
|
||||
const newGrid = world.getGridInCurrentActivity(desktopNumber);
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
column.moveToGrid(newGrid, newGrid.getLastColumn());
|
||||
oldGrid.arrange();
|
||||
newGrid.arrange();
|
||||
});
|
||||
},
|
||||
|
||||
tailMoveToDesktop: (desktopIndex: number) => {
|
||||
world.doIfTiledFocused(true, (window, column, oldGrid) => {
|
||||
const desktopNumber = desktopIndex + 1;
|
||||
const newGrid = world.getGridInCurrentActivity(desktopNumber);
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
oldGrid.evacuateTail(newGrid, column);
|
||||
oldGrid.arrange();
|
||||
newGrid.arrange();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function gridScroll(world: World, amount: number) {
|
||||
const scrollAmount = amount;
|
||||
const grid = world.getCurrentGrid();
|
||||
grid.adjustScroll(scrollAmount, false);
|
||||
grid.arrange();
|
||||
}
|
||||
|
||||
function canTileEver(kwinClient: AbstractClient) {
|
||||
return kwinClient.resizeable;
|
||||
}
|
||||
|
||||
function canTileNow(kwinClient: AbstractClient) {
|
||||
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
|
||||
}
|
||||
|
||||
function makeTileable(kwinClient: AbstractClient) {
|
||||
if (kwinClient.minimized) {
|
||||
kwinClient.minimized = false;
|
||||
}
|
||||
if (kwinClient.desktop <= 0) {
|
||||
kwinClient.desktop = workspace.currentDesktop;
|
||||
}
|
||||
if (kwinClient.activities.length !== 1) {
|
||||
kwinClient.activities = [workspace.currentActivity];
|
||||
}
|
||||
}
|
||||
96
src/behavior/columnResizer/ContextualResizer.ts
Normal file
96
src/behavior/columnResizer/ContextualResizer.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
class ContextualResizer {
|
||||
public increaseWidth(column: Column, step: number) {
|
||||
const grid = column.grid;
|
||||
const desktop = grid.desktop;
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
|
||||
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
|
||||
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
||||
console.assert(false); // should at least see self
|
||||
return;
|
||||
}
|
||||
|
||||
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
|
||||
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
|
||||
|
||||
const newWidth = ContextualResizer.findNextStep(
|
||||
[
|
||||
visibleRange.getWidth(),
|
||||
column.getWidth() + step,
|
||||
column.getWidth() + leftSpace + rightSpace,
|
||||
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
|
||||
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
|
||||
],
|
||||
width => width - column.getWidth(),
|
||||
)
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
column.setWidth(newWidth, true);
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
|
||||
public decreaseWidth(column: Column, step: number) {
|
||||
const grid = column.grid;
|
||||
const desktop = grid.desktop;
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
|
||||
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
|
||||
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
||||
console.assert(false); // should at least see self
|
||||
return;
|
||||
}
|
||||
|
||||
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
|
||||
if (leftOffScreenColumn === column) {
|
||||
leftOffScreenColumn = null;
|
||||
}
|
||||
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
|
||||
if (rightOffScreenColumn === column) {
|
||||
rightOffScreenColumn = null;
|
||||
}
|
||||
|
||||
const visibleColumnsWidth = rightVisibleColumn.getRight() - leftVisibleColumn.getLeft();
|
||||
const unusedWidth = visibleRange.getWidth() - visibleColumnsWidth;
|
||||
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
|
||||
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
|
||||
|
||||
const newWidth = ContextualResizer.findNextStep(
|
||||
[
|
||||
visibleRange.getWidth(),
|
||||
column.getWidth() - step,
|
||||
column.getWidth() - leftOffScreen,
|
||||
column.getWidth() - rightOffScreen,
|
||||
],
|
||||
width => column.getWidth() - width,
|
||||
)
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
column.setWidth(newWidth, true);
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
|
||||
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
|
||||
let bestScore = Infinity;
|
||||
let bestStep = undefined;
|
||||
for (const step of steps) {
|
||||
const score = evaluate(step);
|
||||
if (score > 0 && score < bestScore) {
|
||||
bestScore = score;
|
||||
bestStep = step;
|
||||
}
|
||||
}
|
||||
return bestStep;
|
||||
}
|
||||
}
|
||||
9
src/behavior/columnResizer/RawResizer.ts
Normal file
9
src/behavior/columnResizer/RawResizer.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
class RawResizer {
|
||||
public increaseWidth(column: Column, step: number) {
|
||||
column.adjustWidth(step, true);
|
||||
}
|
||||
|
||||
public decreaseWidth(column: Column, step: number) {
|
||||
column.adjustWidth(-step, true);
|
||||
}
|
||||
}
|
||||
13
src/behavior/scrollClamper/CenterClamper.ts
Normal file
13
src/behavior/scrollClamper/CenterClamper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
class CenterClamper {
|
||||
public clampScrollX(desktop: Desktop, x: number) {
|
||||
const firstColumn = desktop.grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return 0;
|
||||
}
|
||||
const lastColumn = desktop.grid.getLastColumn()!;
|
||||
|
||||
let minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
|
||||
let maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
|
||||
return clamp(x, minScroll, maxScroll);
|
||||
}
|
||||
}
|
||||
10
src/behavior/scrollClamper/EdgeClamper.ts
Normal file
10
src/behavior/scrollClamper/EdgeClamper.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
class EdgeClamper {
|
||||
public clampScrollX(desktop: Desktop, x: number) {
|
||||
let minScroll = 0;
|
||||
let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
|
||||
if (maxScroll < 0) {
|
||||
return Math.round(maxScroll / 2);
|
||||
}
|
||||
return clamp(x, minScroll, maxScroll);
|
||||
}
|
||||
}
|
||||
5
src/behavior/scroller/CenteredScroller.ts
Normal file
5
src/behavior/scroller/CenteredScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class CenteredScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollCenterRange(column);
|
||||
}
|
||||
}
|
||||
5
src/behavior/scroller/GroupedScroller.ts
Normal file
5
src/behavior/scroller/GroupedScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class GroupedScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
}
|
||||
5
src/behavior/scroller/LazyScroller.ts
Normal file
5
src/behavior/scroller/LazyScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class LazyScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollIntoView(column);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,18 @@ type Config = {
|
||||
gapsOuterRight: number,
|
||||
gapsInnerHorizontal: number,
|
||||
gapsInnerVertical: number,
|
||||
overscroll: number,
|
||||
manualScrollStep: number,
|
||||
manualResizeStep: number,
|
||||
offScreenOpacity: number,
|
||||
untileOnDrag: boolean,
|
||||
stackColumnsByDefault: boolean,
|
||||
resizeNeighborColumn: boolean,
|
||||
reMaximize: boolean,
|
||||
skipSwitcher: boolean,
|
||||
scrollingLazy: boolean,
|
||||
scrollingCentered: boolean,
|
||||
scrollingGrouped: boolean,
|
||||
tiledKeepBelow: boolean,
|
||||
floatingKeepAbove: boolean,
|
||||
windowRules: string,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,22 +7,45 @@ const defaultWindowRules = `[
|
||||
"class": "kcalc",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "org.kde.kcalc",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "kfind",
|
||||
"tile": true
|
||||
},
|
||||
{
|
||||
"class": "org.kde.kfind",
|
||||
"tile": true
|
||||
},
|
||||
{
|
||||
"class": "kruler",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "zoom",
|
||||
"caption": "Zoom Cloud Meetings",
|
||||
"class": "org.kde.kruler",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "krunner",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "org.kde.krunner",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "yakuake",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "org.kde.yakuake",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "zoom",
|
||||
"caption": "zoom",
|
||||
"caption": "Zoom Cloud Meetings|zoom|zoom <2>",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
@@ -49,53 +72,103 @@ const defaultWindowRules = `[
|
||||
|
||||
const configDef = [
|
||||
{
|
||||
"name": "gapsOuterTop",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsOuterTop",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterBottom",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsOuterBottom",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterLeft",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsOuterLeft",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterRight",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsOuterRight",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "gapsInnerHorizontal",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsInnerHorizontal",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "gapsInnerVertical",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "gapsInnerVertical",
|
||||
type: "UInt",
|
||||
default: 18,
|
||||
},
|
||||
{
|
||||
"name": "overscroll",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
name: "manualScrollStep",
|
||||
type: "UInt",
|
||||
default: 200,
|
||||
},
|
||||
{
|
||||
"name": "manualScrollStep",
|
||||
"type": "UInt",
|
||||
"default": 200
|
||||
name: "manualResizeStep",
|
||||
type: "UInt",
|
||||
default: 600,
|
||||
},
|
||||
{
|
||||
"name": "stackColumnsByDefault",
|
||||
"type": "Bool",
|
||||
"default": false
|
||||
name: "offScreenOpacity",
|
||||
type: "UInt",
|
||||
default: 100,
|
||||
},
|
||||
{
|
||||
"name": "windowRules",
|
||||
"type": "String",
|
||||
"default": defaultWindowRules
|
||||
name: "untileOnDrag",
|
||||
type: "Bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "stackColumnsByDefault",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "resizeNeighborColumn",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "reMaximize",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "skipSwitcher",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "scrollingLazy",
|
||||
type: "Bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "scrollingCentered",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "scrollingGrouped",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "tiledKeepBelow",
|
||||
type: "Bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "floatingKeepAbove",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "windowRules",
|
||||
type: "String",
|
||||
default: defaultWindowRules,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function loadConfig() {
|
||||
function loadConfig(): Config {
|
||||
const config: any = {};
|
||||
for (const entry of configDef) {
|
||||
config[entry.name] = KWin.readConfig(entry.name, entry.default);
|
||||
|
||||
13
src/extern.d.ts
vendored
13
src/extern.d.ts
vendored
@@ -1,13 +0,0 @@
|
||||
const qmlBase;
|
||||
const console;
|
||||
const KWin;
|
||||
const Qt;
|
||||
const workspace;
|
||||
const options;
|
||||
|
||||
type AbstractClient = any;
|
||||
type TopLevel = any;
|
||||
type X11Client = any;
|
||||
type QRect = any;
|
||||
type QSignal = any;
|
||||
type QQmlTimer = any;
|
||||
6
src/extern/global.d.ts
vendored
Normal file
6
src/extern/global.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare const qmlBase: QmlObject;
|
||||
declare const notificationInvalidWindowRules: Notification;
|
||||
|
||||
type Notification = {
|
||||
sendEvent(): void;
|
||||
};
|
||||
79
src/extern/kwin.d.ts
vendored
Normal file
79
src/extern/kwin.d.ts
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
declare const KWin: {
|
||||
readConfig(key: string, defaultValue: any): any;
|
||||
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
|
||||
};
|
||||
|
||||
declare const workspace: {
|
||||
readonly desktops: number;
|
||||
readonly currentDesktop: number;
|
||||
readonly currentActivity: string;
|
||||
|
||||
activeClient: KwinClient;
|
||||
|
||||
readonly currentDesktopChanged: QSignal<[]>
|
||||
readonly clientAdded: QSignal<[KwinClient]>;
|
||||
readonly clientRemoved: QSignal<[KwinClient]>;
|
||||
readonly clientMinimized: QSignal<[KwinClient]>;
|
||||
readonly clientUnminimized: QSignal<[KwinClient]>;
|
||||
readonly clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
|
||||
readonly clientActivated: QSignal<[KwinClient]>;
|
||||
readonly numberDesktopsChanged: QSignal<[]>;
|
||||
readonly currentActivityChanged: QSignal<[]>;
|
||||
readonly virtualScreenSizeChanged: QSignal<[]>;
|
||||
|
||||
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
|
||||
clientList(): KwinClient[];
|
||||
};
|
||||
|
||||
const enum ClientAreaOption {
|
||||
PlacementArea,
|
||||
MovementArea,
|
||||
MaximizeArea,
|
||||
MaximizeFullArea,
|
||||
FullScreenArea,
|
||||
WorkArea,
|
||||
FullArea,
|
||||
ScreenArea,
|
||||
}
|
||||
|
||||
type Tile = unknown;
|
||||
|
||||
interface KwinClient {
|
||||
readonly shadeable: boolean;
|
||||
readonly caption: string;
|
||||
readonly minSize: QmlSize;
|
||||
readonly transient: boolean;
|
||||
readonly transientFor: KwinClient;
|
||||
readonly move: boolean;
|
||||
readonly resize: boolean;
|
||||
readonly resizeable: boolean;
|
||||
readonly screen: number;
|
||||
readonly resourceClass: QByteArray;
|
||||
readonly dock: boolean;
|
||||
readonly normalWindow: boolean;
|
||||
readonly managed: boolean;
|
||||
|
||||
opacity: number;
|
||||
fullScreen: boolean;
|
||||
activities: string[]; // empty array means all activities
|
||||
skipSwitcher: boolean;
|
||||
keepAbove: boolean;
|
||||
keepBelow: boolean;
|
||||
shade: boolean;
|
||||
minimized: boolean;
|
||||
frameGeometry: QmlRect;
|
||||
desktop: number; // -1 means all desktops
|
||||
tile: Tile;
|
||||
|
||||
readonly fullScreenChanged: QSignal<[]>;
|
||||
readonly desktopChanged: QSignal<[]>;
|
||||
readonly activitiesChanged: QSignal<[]>;
|
||||
readonly captionChanged: QSignal<[]>;
|
||||
readonly tileChanged: QSignal<[]>;
|
||||
readonly moveResizedChanged: QSignal<[]>;
|
||||
readonly moveResizeCursorChanged: QSignal<[]>;
|
||||
readonly clientStartUserMovedResized: QSignal<[]>;
|
||||
readonly frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QmlRect]>;
|
||||
|
||||
setMaximize(vertically: boolean, horizontally: boolean): void;
|
||||
}
|
||||
42
src/extern/qt.d.ts
vendored
Normal file
42
src/extern/qt.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
declare const console: {
|
||||
log(...args: any[]);
|
||||
trace();
|
||||
assert(boolean);
|
||||
};
|
||||
|
||||
declare const Qt: {
|
||||
rect(x: number, y: number, width: number, height: number): QmlRect;
|
||||
createQmlObject(qml: string, parent: QmlObject);
|
||||
};
|
||||
|
||||
type QmlObject = unknown;
|
||||
|
||||
type QByteArray = string;
|
||||
|
||||
type QmlRect = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
top: number;
|
||||
bottom: number; // top + height
|
||||
left: number;
|
||||
right: number; // left + width
|
||||
};
|
||||
|
||||
type QmlSize = {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type QSignal<T extends unknown[]> = {
|
||||
connect(handler: (...args: [...T]) => void): void;
|
||||
disconnect(handler: (...args: [...T]) => void): void;
|
||||
};
|
||||
|
||||
type QmlTimer = {
|
||||
interval: number;
|
||||
readonly triggered: QSignal<[]>;
|
||||
restart(): void;
|
||||
destroy(): void;
|
||||
};
|
||||
218
src/keyBindings/definition.ts
Normal file
218
src/keyBindings/definition.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
const keyBindings: KeyBinding[] = [
|
||||
{
|
||||
name: "window-toggle-floating",
|
||||
description: "Toggle floating",
|
||||
defaultKeySequence: "Meta+Space",
|
||||
action: "windowToggleFloating",
|
||||
},
|
||||
{
|
||||
name: "focus-left",
|
||||
description: "Move focus left",
|
||||
defaultKeySequence: "Meta+A",
|
||||
action: "focusLeft",
|
||||
},
|
||||
{
|
||||
name: "focus-right",
|
||||
description: "Move focus right",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+D",
|
||||
action: "focusRight",
|
||||
},
|
||||
{
|
||||
name: "focus-up",
|
||||
description: "Move focus up",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+W",
|
||||
action: "focusUp",
|
||||
},
|
||||
{
|
||||
name: "focus-down",
|
||||
description: "Move focus down",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+S",
|
||||
action: "focusDown",
|
||||
},
|
||||
{
|
||||
name: "focus-start",
|
||||
description: "Move focus to start",
|
||||
defaultKeySequence: "Meta+Home",
|
||||
action: "focusStart",
|
||||
},
|
||||
{
|
||||
name: "focus-end",
|
||||
description: "Move focus to end",
|
||||
defaultKeySequence: "Meta+End",
|
||||
action: "focusEnd",
|
||||
},
|
||||
{
|
||||
name: "window-move-left",
|
||||
description: "Move window left",
|
||||
comment: "Moves window out of and into columns",
|
||||
defaultKeySequence: "Meta+Shift+A",
|
||||
action: "windowMoveLeft",
|
||||
},
|
||||
{
|
||||
name: "window-move-right",
|
||||
description: "Move window right",
|
||||
comment: "Moves window out of and into columns",
|
||||
defaultKeySequence: "Meta+Shift+D",
|
||||
action: "windowMoveRight",
|
||||
},
|
||||
{
|
||||
name: "window-move-up",
|
||||
description: "Move window up",
|
||||
defaultKeySequence: "Meta+Shift+W",
|
||||
action: "windowMoveUp",
|
||||
},
|
||||
{
|
||||
name: "window-move-down",
|
||||
description: "Move window down",
|
||||
defaultKeySequence: "Meta+Shift+S",
|
||||
action: "windowMoveDown",
|
||||
},
|
||||
{
|
||||
name: "window-move-start",
|
||||
description: "Move window to start",
|
||||
defaultKeySequence: "Meta+Shift+Home",
|
||||
action: "windowMoveStart",
|
||||
},
|
||||
{
|
||||
name: "window-move-end",
|
||||
description: "Move window to end",
|
||||
defaultKeySequence: "Meta+Shift+End",
|
||||
action: "windowMoveEnd",
|
||||
},
|
||||
{
|
||||
name: "column-toggle-stacked",
|
||||
description: "Toggle stacked layout for focused column",
|
||||
comment: "One window in the column visible, others shaded; not supported on Wayland",
|
||||
defaultKeySequence: "Meta+X",
|
||||
action: "columnToggleStacked",
|
||||
},
|
||||
{
|
||||
name: "column-move-left",
|
||||
description: "Move column left",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+A",
|
||||
action: "columnMoveLeft",
|
||||
},
|
||||
{
|
||||
name: "column-move-right",
|
||||
description: "Move column right",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+D",
|
||||
action: "columnMoveRight",
|
||||
},
|
||||
{
|
||||
name: "column-move-start",
|
||||
description: "Move column to start",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+Home",
|
||||
action: "columnMoveStart",
|
||||
},
|
||||
{
|
||||
name: "column-move-end",
|
||||
description: "Move column to end",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+End",
|
||||
action: "columnMoveEnd",
|
||||
},
|
||||
{
|
||||
name: "column-width-increase",
|
||||
description: "Increase column width",
|
||||
defaultKeySequence: "Meta+Ctrl++",
|
||||
action: "columnWidthIncrease",
|
||||
},
|
||||
{
|
||||
name: "column-width-decrease",
|
||||
description: "Decrease column width",
|
||||
defaultKeySequence: "Meta+Ctrl+-",
|
||||
action: "columnWidthDecrease",
|
||||
},
|
||||
{
|
||||
name: "columns-width-equalize",
|
||||
description: "Equalize widths of visible columns",
|
||||
defaultKeySequence: "Meta+Ctrl+X",
|
||||
action: "columnsWidthEqualize",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-focused",
|
||||
description: "Center focused window",
|
||||
comment: "Scrolls so that the focused window is centered in the screen",
|
||||
defaultKeySequence: "Meta+Alt+Return",
|
||||
action: "gridScrollFocused",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-left-column",
|
||||
description: "Scroll one column to the left",
|
||||
defaultKeySequence: "Meta+Alt+A",
|
||||
action: "gridScrollLeftColumn",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-right-column",
|
||||
description: "Scroll one column to the right",
|
||||
defaultKeySequence: "Meta+Alt+D",
|
||||
action: "gridScrollRightColumn",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-left",
|
||||
description: "Scroll left",
|
||||
defaultKeySequence: "Meta+Alt+PgUp",
|
||||
action: "gridScrollLeft",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-right",
|
||||
description: "Scroll right",
|
||||
defaultKeySequence: "Meta+Alt+PgDown",
|
||||
action: "gridScrollRight",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-start",
|
||||
description: "Scroll to start",
|
||||
defaultKeySequence: "Meta+Alt+Home",
|
||||
action: "gridScrollStart",
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-end",
|
||||
description: "Scroll to end",
|
||||
defaultKeySequence: "Meta+Alt+End",
|
||||
action: "gridScrollEnd",
|
||||
},
|
||||
];
|
||||
|
||||
const numKeyBindings: NumKeyBinding[] = [
|
||||
{
|
||||
name: "focus-",
|
||||
description: "Move focus to column ",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultModifiers: "Meta",
|
||||
fKeys: false,
|
||||
action: "focusColumn",
|
||||
},
|
||||
{
|
||||
name: "window-move-to-column-",
|
||||
description: "Move window to column ",
|
||||
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
|
||||
defaultModifiers: "Meta+Shift",
|
||||
fKeys: false,
|
||||
action: "windowMoveToColumn",
|
||||
},
|
||||
{
|
||||
name: "column-move-to-column-",
|
||||
description: "Move column to position ",
|
||||
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
|
||||
defaultModifiers: "Meta+Ctrl+Shift",
|
||||
fKeys: false,
|
||||
action: "columnMoveToColumn",
|
||||
},
|
||||
{
|
||||
name: "column-move-to-desktop-",
|
||||
description: "Move column to desktop ",
|
||||
defaultModifiers: "Meta+Ctrl+Shift",
|
||||
fKeys: true,
|
||||
action: "columnMoveToDesktop",
|
||||
},
|
||||
{
|
||||
name: "tail-move-to-desktop-",
|
||||
description: "Move this and all following columns to desktop ",
|
||||
defaultModifiers: "Meta+Ctrl+Shift+Alt",
|
||||
fKeys: true,
|
||||
action: "tailMoveToDesktop",
|
||||
},
|
||||
];
|
||||
70
src/keyBindings/loader.ts
Normal file
70
src/keyBindings/loader.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
type KeyBinding = {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultKeySequence: string;
|
||||
action: keyof ReturnType<typeof Actions.init>;
|
||||
};
|
||||
|
||||
type NumKeyBinding = {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultModifiers: string;
|
||||
fKeys: boolean;
|
||||
action: keyof ReturnType<typeof Actions.initNum>;
|
||||
};
|
||||
|
||||
function catchWrap(f: () => void) {
|
||||
return () => {
|
||||
try {
|
||||
f();
|
||||
} catch (error: any) {
|
||||
log(error);
|
||||
log(error.stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function registerKeyBinding(name: string, description: string, keySequence: string, callback: () => void) {
|
||||
KWin.registerShortcut(
|
||||
"karousel-" + name,
|
||||
"Karousel: " + description,
|
||||
keySequence,
|
||||
catchWrap(callback),
|
||||
);
|
||||
}
|
||||
|
||||
function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) {
|
||||
const numPrefix = fKeys ? "F" : "";
|
||||
const n = fKeys ? 12 : 9;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const numKey = String(i + 1);
|
||||
const keySequence = i < n ?
|
||||
modifiers + "+" + numPrefix + numKey :
|
||||
"";
|
||||
registerKeyBinding(
|
||||
name + numKey,
|
||||
description + numKey,
|
||||
keySequence,
|
||||
() => callback(i),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function registerKeyBindings(world: World, config: Config) {
|
||||
const actions = Actions.init(world, {
|
||||
manualScrollStep: config.manualScrollStep,
|
||||
manualResizeStep: config.manualResizeStep,
|
||||
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
|
||||
});
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
|
||||
}
|
||||
|
||||
const numActions = Actions.initNum(world);
|
||||
for (const binding of numKeyBindings) {
|
||||
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
class Column {
|
||||
public grid: Grid;
|
||||
public gridX: number;
|
||||
public width: number; // TODO: increase column width to contain transients
|
||||
private width: number; // TODO: increase column width to contain transients
|
||||
private readonly windows: LinkedList<Window>;
|
||||
private stacked: boolean;
|
||||
private focusTaker: Window|null;
|
||||
private widthBeforeExpand: number;
|
||||
private static readonly minWidth = 10;
|
||||
|
||||
constructor(grid: Grid, prevColumn: Column|null) {
|
||||
this.gridX = 0;
|
||||
this.width = 0;
|
||||
this.windows = new LinkedList();
|
||||
this.stacked = grid.world.config.stackColumnsByDefault;
|
||||
this.stacked = grid.config.stackColumnsByDefault;
|
||||
this.focusTaker = null;
|
||||
this.widthBeforeExpand = 0;
|
||||
this.grid = grid;
|
||||
this.grid.onColumnAdded(this, prevColumn);
|
||||
}
|
||||
|
||||
moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
|
||||
public moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
|
||||
if (targetGrid === this.grid) {
|
||||
this.grid.onColumnMoved(this, prevColumn);
|
||||
} else {
|
||||
@@ -26,88 +25,114 @@ class Column {
|
||||
this.grid = targetGrid;
|
||||
targetGrid.onColumnAdded(this, prevColumn);
|
||||
for (const window of this.windows.iterator()) {
|
||||
window.client.kwinClient.desktop = targetGrid.desktop;
|
||||
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
moveAfter(prevColumn: Column|null) {
|
||||
public moveAfter(prevColumn: Column|null) {
|
||||
if (prevColumn === this) {
|
||||
return;
|
||||
}
|
||||
this.grid.onColumnMoved(this, prevColumn);
|
||||
}
|
||||
|
||||
isAfter(other: Column) {
|
||||
public isAfter(other: Column) {
|
||||
return this.gridX > other.gridX;
|
||||
}
|
||||
|
||||
isBefore(other: Column) {
|
||||
public isBefore(other: Column) {
|
||||
return this.gridX < other.gridX;
|
||||
}
|
||||
|
||||
moveWindowUp(window: Window) {
|
||||
public moveWindowUp(window: Window) {
|
||||
this.windows.moveBack(window);
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
moveWindowDown(window: Window) {
|
||||
public moveWindowDown(window: Window) {
|
||||
this.windows.moveForward(window);
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
getWindowCount() {
|
||||
public getWindowCount() {
|
||||
return this.windows.length();
|
||||
}
|
||||
|
||||
isEmpty() {
|
||||
public isEmpty() {
|
||||
return this.getWindowCount() === 0;
|
||||
}
|
||||
|
||||
getPrevWindow(window: Window) {
|
||||
public getPrevWindow(window: Window) {
|
||||
return this.windows.getPrev(window);
|
||||
}
|
||||
|
||||
getNextWindow(window: Window) {
|
||||
public getNextWindow(window: Window) {
|
||||
return this.windows.getNext(window);
|
||||
}
|
||||
|
||||
getWidth() {
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
getMaxWidth() {
|
||||
return this.grid.tilingArea.width;
|
||||
public getMinWidth() {
|
||||
let maxMinWidth = Column.minWidth;
|
||||
for (const window of this.windows.iterator()) {
|
||||
const minWidth = window.client.kwinClient.minSize.width;
|
||||
if (minWidth > maxMinWidth) {
|
||||
maxMinWidth = minWidth;
|
||||
}
|
||||
}
|
||||
return maxMinWidth;
|
||||
}
|
||||
|
||||
setWidth(width: number, setPreferred: boolean) {
|
||||
width = Math.min(width, this.getMaxWidth());
|
||||
const oldWidth = this.width;
|
||||
public getMaxWidth() {
|
||||
return this.grid.desktop.tilingArea.width;
|
||||
}
|
||||
|
||||
public setWidth(width: number, setPreferred: boolean) {
|
||||
width = clamp(width, this.getMinWidth(), this.getMaxWidth());
|
||||
if (width === this.width) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = width;
|
||||
if (setPreferred) {
|
||||
for (const window of this.windows.iterator()) {
|
||||
window.client.preferredWidth = width;
|
||||
}
|
||||
}
|
||||
if (width !== oldWidth) {
|
||||
this.grid.onColumnWidthChanged(this, oldWidth, width);
|
||||
}
|
||||
this.grid.onColumnWidthChanged(this);
|
||||
}
|
||||
|
||||
adjustWidth(widthDelta: number, setPreferred: boolean) {
|
||||
public adjustWidth(widthDelta: number, setPreferred: boolean) {
|
||||
this.setWidth(this.width + widthDelta, setPreferred);
|
||||
}
|
||||
|
||||
expand() {
|
||||
const maxWidth = this.getMaxWidth();
|
||||
const isAlreadyExpanded = this.width === maxWidth && this.widthBeforeExpand > 0;
|
||||
if (isAlreadyExpanded) {
|
||||
this.setWidth(this.widthBeforeExpand, false);
|
||||
} else {
|
||||
this.widthBeforeExpand = this.width;
|
||||
this.setWidth(maxWidth, false);
|
||||
public updateWidth() {
|
||||
let minErr = Infinity;
|
||||
let closestPreferredWidth = this.width;
|
||||
for (const window of this.windows.iterator()) {
|
||||
const err = Math.abs(window.client.preferredWidth - this.width);
|
||||
if (err < minErr) {
|
||||
minErr = err;
|
||||
closestPreferredWidth = window.client.preferredWidth;
|
||||
}
|
||||
}
|
||||
this.setWidth(closestPreferredWidth, false);
|
||||
}
|
||||
|
||||
adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
|
||||
// returns x position of left edge in grid space
|
||||
public getLeft() {
|
||||
return this.gridX;
|
||||
}
|
||||
|
||||
// returns x position of right edge in grid space
|
||||
public getRight() {
|
||||
return this.gridX + this.width;
|
||||
}
|
||||
|
||||
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
|
||||
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
|
||||
if (otherWindow === null) {
|
||||
return;
|
||||
@@ -115,18 +140,20 @@ class Column {
|
||||
|
||||
window.height += heightDelta;
|
||||
otherWindow.height -= heightDelta;
|
||||
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
resizeWindows() {
|
||||
public resizeWindows() {
|
||||
const nWindows = this.windows.length();
|
||||
if (nWindows === 0) {
|
||||
return;
|
||||
}
|
||||
if (nWindows === 1) {
|
||||
this.stacked = this.grid.world.config.stackColumnsByDefault;
|
||||
this.stacked = this.grid.config.stackColumnsByDefault;
|
||||
}
|
||||
|
||||
let remainingPixels = this.grid.tilingArea.height - (nWindows-1) * this.grid.world.config.gapsInnerVertical;
|
||||
let remainingPixels = this.grid.desktop.tilingArea.height - (nWindows-1) * this.grid.config.gapsInnerVertical;
|
||||
let remainingWindows = nWindows;
|
||||
for (const window of this.windows.iterator()) {
|
||||
const windowHeight = Math.round(remainingPixels / remainingWindows);
|
||||
@@ -135,16 +162,18 @@ class Column {
|
||||
remainingWindows--;
|
||||
}
|
||||
// TODO: respect min height
|
||||
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
getFocusTaker() {
|
||||
public getFocusTaker() {
|
||||
if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) {
|
||||
return null;
|
||||
}
|
||||
return this.focusTaker;
|
||||
}
|
||||
|
||||
focus() {
|
||||
public focus() {
|
||||
const window = this.getFocusTaker() ?? this.windows.getFirst();
|
||||
if (window === null) {
|
||||
return;
|
||||
@@ -152,20 +181,27 @@ class Column {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
arrange(x: number) {
|
||||
if (this.stacked && this.windows.length() >= 2) {
|
||||
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
|
||||
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
|
||||
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
|
||||
for (const window of this.windows.iterator()) {
|
||||
window.client.kwinClient.opacity = opacity;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
|
||||
this.arrangeStacked(x);
|
||||
return;
|
||||
}
|
||||
let y = this.grid.tilingArea.y;
|
||||
let y = this.grid.desktop.tilingArea.y;
|
||||
for (const window of this.windows.iterator()) {
|
||||
window.client.setShade(false);
|
||||
window.arrange(x, y, this.width, window.height);
|
||||
y += window.height + this.grid.world.config.gapsInnerVertical;
|
||||
y += window.height + this.grid.config.gapsInnerVertical;
|
||||
}
|
||||
}
|
||||
|
||||
arrangeStacked(x: number) {
|
||||
public arrangeStacked(x: number) {
|
||||
const expandedWindow = this.getFocusTaker();
|
||||
let collapsedHeight;
|
||||
for (const window of this.windows.iterator()) {
|
||||
@@ -178,28 +214,48 @@ class Column {
|
||||
}
|
||||
|
||||
const nCollapsed = this.getWindowCount() - 1;
|
||||
const expandedHeight = this.grid.tilingArea.height - nCollapsed * (collapsedHeight + this.grid.world.config.gapsInnerVertical);
|
||||
let y = this.grid.tilingArea.y;
|
||||
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
|
||||
let y = this.grid.desktop.tilingArea.y;
|
||||
for (const window of this.windows.iterator()) {
|
||||
if (window === expandedWindow) {
|
||||
window.arrange(x, y, this.width, expandedHeight);
|
||||
y += expandedHeight;
|
||||
} else {
|
||||
window.arrange(x, y, this.width, window.height);
|
||||
y += collapsedHeight;
|
||||
y += collapsedHeight!;
|
||||
}
|
||||
y += this.grid.world.config.gapsInnerVertical;
|
||||
y += this.grid.config.gapsInnerVertical;
|
||||
}
|
||||
}
|
||||
|
||||
toggleStacked() {
|
||||
public toggleStacked() {
|
||||
if (this.windows.length() < 2) {
|
||||
return;
|
||||
}
|
||||
this.stacked = !this.stacked;
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onWindowAdded(window: Window) {
|
||||
private canStack() {
|
||||
for (const window of this.windows.iterator()) {
|
||||
if (!window.client.kwinClient.shadeable) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
if (fullyVisible) {
|
||||
return this.getLeft() >= visibleRange.getLeft() &&
|
||||
this.getRight() <= visibleRange.getRight();
|
||||
} else {
|
||||
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
|
||||
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
|
||||
}
|
||||
}
|
||||
|
||||
public onWindowAdded(window: Window) {
|
||||
this.windows.insertEnd(window);
|
||||
if (this.width === 0) {
|
||||
this.setWidth(window.client.preferredWidth, false);
|
||||
@@ -211,9 +267,11 @@ class Column {
|
||||
if (window.isFocused()) {
|
||||
this.onWindowFocused(window);
|
||||
}
|
||||
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onWindowRemoved(window: Window, passFocus: boolean) {
|
||||
public onWindowRemoved(window: Window, passFocus: boolean) {
|
||||
const lastWindow = this.windows.length() === 1;
|
||||
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
|
||||
|
||||
@@ -232,21 +290,23 @@ class Column {
|
||||
windowToFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onWindowFocused(window: Window) {
|
||||
public onWindowFocused(window: Window) {
|
||||
this.grid.onColumnFocused(this);
|
||||
this.focusTaker = window;
|
||||
}
|
||||
|
||||
restoreToTiled() {
|
||||
public restoreToTiled() {
|
||||
const lastFocusedWindow = this.getFocusTaker();
|
||||
if (lastFocusedWindow !== null) {
|
||||
lastFocusedWindow.restoreToTiled();
|
||||
}
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
private destroy(passFocus: boolean) {
|
||||
this.grid.onColumnRemoved(this, passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
311
src/layout/Desktop.ts
Normal file
311
src/layout/Desktop.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
class Desktop {
|
||||
public readonly grid: Grid;
|
||||
public readonly desktopNumber: number;
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly config: Desktop.Config;
|
||||
private scrollX: number;
|
||||
private dirty: boolean;
|
||||
private dirtyScroll: boolean;
|
||||
private dirtyPins: boolean;
|
||||
public clientArea: QmlRect;
|
||||
public tilingArea: QmlRect;
|
||||
|
||||
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
|
||||
this.pinManager = pinManager;
|
||||
this.config = config;
|
||||
this.scrollX = 0;
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
this.dirtyPins = true;
|
||||
this.desktopNumber = desktopNumber;
|
||||
this.grid = new Grid(this, layoutConfig);
|
||||
this.clientArea = Desktop.getClientArea(desktopNumber);
|
||||
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
|
||||
}
|
||||
|
||||
private updateArea() {
|
||||
const newClientArea = Desktop.getClientArea(this.desktopNumber);
|
||||
if (newClientArea === this.clientArea && !this.dirtyPins) {
|
||||
return;
|
||||
}
|
||||
this.clientArea = newClientArea;
|
||||
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
this.dirtyPins = false;
|
||||
this.grid.onScreenSizeChanged();
|
||||
this.autoAdjustScroll();
|
||||
}
|
||||
|
||||
private static getClientArea(desktopNumber: number) {
|
||||
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
|
||||
}
|
||||
|
||||
private static getTilingArea(clientArea: QmlRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
|
||||
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
|
||||
const top = availableSpace.top + config.marginTop;
|
||||
const bottom = availableSpace.bottom - config.marginBottom;
|
||||
const left = availableSpace.left + config.marginLeft;
|
||||
const right = availableSpace.right - config.marginRight;
|
||||
return Qt.rect(
|
||||
left,
|
||||
top,
|
||||
right - left,
|
||||
bottom - top,
|
||||
)
|
||||
}
|
||||
|
||||
public scrollIntoView(range: Desktop.Range) {
|
||||
const left = range.getLeft();
|
||||
const right = range.getRight();
|
||||
const initialVisibleRange = this.getCurrentVisibleRange();
|
||||
|
||||
let targetScrollX: number;
|
||||
if (left < initialVisibleRange.getLeft()) {
|
||||
targetScrollX = left;
|
||||
} else if (right > initialVisibleRange.getRight()) {
|
||||
targetScrollX = right - this.tilingArea.width;
|
||||
} else {
|
||||
targetScrollX = initialVisibleRange.getLeft();
|
||||
}
|
||||
|
||||
this.setScroll(targetScrollX, false);
|
||||
}
|
||||
|
||||
public scrollCenterRange(range: Desktop.Range) {
|
||||
const windowCenter = range.getLeft() + range.getWidth() / 2;
|
||||
const screenCenter = this.scrollX + this.tilingArea.width / 2;
|
||||
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
|
||||
}
|
||||
|
||||
public scrollCenterVisible(focusedColumn: Column) {
|
||||
const columnRange = new Desktop.ColumnRange(focusedColumn);
|
||||
const visibleRange = this.getCurrentVisibleRange();
|
||||
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal);
|
||||
this.scrollCenterRange(columnRange);
|
||||
}
|
||||
|
||||
public autoAdjustScroll() {
|
||||
const focusedColumn = this.grid.getLastFocusedColumn();
|
||||
if (focusedColumn === null || focusedColumn.grid !== this.grid) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollToColumn(focusedColumn);
|
||||
}
|
||||
|
||||
public scrollToColumn(column: Column) {
|
||||
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
|
||||
this.config.scroller.scrollToColumn(this, column);
|
||||
}
|
||||
}
|
||||
|
||||
private getVisibleRange(scrollX: number) {
|
||||
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
|
||||
}
|
||||
|
||||
public getCurrentVisibleRange() {
|
||||
return this.getVisibleRange(this.scrollX);
|
||||
}
|
||||
|
||||
private clampScrollX(x: number) {
|
||||
return this.config.clamper.clampScrollX(this, x);
|
||||
}
|
||||
|
||||
public setScroll(x: number, force: boolean) {
|
||||
const oldScrollX = this.scrollX;
|
||||
this.scrollX = force ? x : this.clampScrollX(x);
|
||||
if (this.scrollX !== oldScrollX) {
|
||||
this.onLayoutChanged();
|
||||
}
|
||||
this.dirtyScroll = false;
|
||||
}
|
||||
|
||||
public adjustScroll(dx: number, force: boolean) {
|
||||
this.setScroll(this.scrollX + dx, force);
|
||||
}
|
||||
|
||||
public equalizeVisibleColumnsWidths() {
|
||||
const visibleRange = this.getCurrentVisibleRange();
|
||||
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
|
||||
|
||||
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
|
||||
let remainingColumns = visibleColumns.length;
|
||||
|
||||
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
|
||||
for (const minWidth of minWidths) {
|
||||
if (minWidth > remainingWidth / remainingColumns) {
|
||||
remainingWidth -= minWidth;
|
||||
remainingColumns--;
|
||||
}
|
||||
}
|
||||
|
||||
const avgWidth = remainingWidth / remainingColumns;
|
||||
for (const column of visibleColumns) {
|
||||
const minWidth = column.getMinWidth();
|
||||
if (minWidth > avgWidth) {
|
||||
column.setWidth(minWidth, true);
|
||||
} else {
|
||||
const columnWidth = Math.round(remainingWidth / remainingColumns);
|
||||
column.setWidth(columnWidth, true);
|
||||
remainingWidth -= column.getWidth();
|
||||
remainingColumns--;
|
||||
}
|
||||
}
|
||||
|
||||
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
|
||||
visibleColumns[0],
|
||||
visibleColumns[visibleColumns.length - 1],
|
||||
));
|
||||
}
|
||||
|
||||
public arrange() {
|
||||
// TODO (optimization): only arrange visible windows
|
||||
this.updateArea();
|
||||
if (!this.dirty) {
|
||||
return;
|
||||
}
|
||||
this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
public onLayoutChanged() {
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
}
|
||||
|
||||
public onPinsChanged() {
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
this.dirtyPins = true;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.grid.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Desktop {
|
||||
export type Config = {
|
||||
marginTop: number,
|
||||
marginBottom: number,
|
||||
marginLeft: number,
|
||||
marginRight: number,
|
||||
scroller: Desktop.Scroller,
|
||||
clamper: Desktop.Clamper,
|
||||
};
|
||||
|
||||
export type Range = {
|
||||
getLeft(): number;
|
||||
getRight(): number;
|
||||
getWidth(): number;
|
||||
}
|
||||
|
||||
export class RangeImpl {
|
||||
private readonly x: number;
|
||||
private readonly width: number;
|
||||
|
||||
constructor(x: number, width: number) {
|
||||
this.x = x;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
public getLeft() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public getRight() {
|
||||
return this.x + this.width;
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public static fromRanges(leftRange: Range, rightRange: Range) {
|
||||
const left = leftRange.getLeft();
|
||||
const right = rightRange.getRight();
|
||||
return new RangeImpl(left, right - left);
|
||||
}
|
||||
}
|
||||
|
||||
export class ColumnRange {
|
||||
private left: Column;
|
||||
private right: Column;
|
||||
private width: number;
|
||||
|
||||
constructor(initialColumn: Column) {
|
||||
this.left = initialColumn;
|
||||
this.right = initialColumn;
|
||||
this.width = initialColumn.getWidth();
|
||||
}
|
||||
|
||||
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
|
||||
const grid = this.left.grid;
|
||||
|
||||
const columnRange = this;
|
||||
function canFit(column: Column) {
|
||||
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth();
|
||||
}
|
||||
function isUsable(column: Column|null) {
|
||||
return column !== null && canFit(column);
|
||||
}
|
||||
|
||||
let leftColumn = grid.getPrevColumn(this.left);
|
||||
let rightColumn = grid.getNextColumn(this.right);
|
||||
function checkColumns() {
|
||||
if (!isUsable(leftColumn)) {
|
||||
leftColumn = null;
|
||||
}
|
||||
if (!isUsable(rightColumn)) {
|
||||
rightColumn = null;
|
||||
}
|
||||
}
|
||||
checkColumns();
|
||||
|
||||
const visibleCenter = visibleRange.getLeft() + visibleRange.getWidth() / 2;
|
||||
while (leftColumn !== null || rightColumn !== null) {
|
||||
const leftToCenter = leftColumn === null ? Infinity : Math.abs(leftColumn.getLeft() - visibleCenter);
|
||||
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
|
||||
if (leftToCenter < rightToCenter) {
|
||||
this.addLeft(leftColumn!, gap);
|
||||
leftColumn = grid.getPrevColumn(leftColumn!);
|
||||
} else {
|
||||
this.addRight(rightColumn!, gap);
|
||||
rightColumn = grid.getNextColumn(rightColumn!);
|
||||
}
|
||||
checkColumns();
|
||||
}
|
||||
}
|
||||
|
||||
public addLeft(column: Column, gap: number) {
|
||||
this.left = column;
|
||||
this.width += column.getWidth() + gap;
|
||||
}
|
||||
|
||||
public addRight(column: Column, gap: number) {
|
||||
this.right = column;
|
||||
this.width += column.getWidth() + gap;
|
||||
}
|
||||
|
||||
public getLeft() {
|
||||
return this.left.getLeft();
|
||||
}
|
||||
|
||||
public getRight() {
|
||||
return this.right.getRight();
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
}
|
||||
|
||||
export type Scroller = {
|
||||
scrollToColumn(desktop: Desktop, column: Column): void;
|
||||
}
|
||||
|
||||
export type Clamper = {
|
||||
clampScrollX(desktop: Desktop, x: number): number;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +1,37 @@
|
||||
import Range = Desktop.Range;
|
||||
|
||||
class Grid {
|
||||
public readonly world: World;
|
||||
public readonly desktop: Desktop;
|
||||
public readonly config: LayoutConfig;
|
||||
private readonly columns: LinkedList<Column>;
|
||||
private lastFocusedColumn: Column|null;
|
||||
private scrollX: number;
|
||||
private width: number;
|
||||
private userResize: boolean; // is any part of the grid being resized by the user
|
||||
public clientArea: QRect;
|
||||
public tilingArea: QRect;
|
||||
public readonly desktop: number;
|
||||
private readonly userResizeFinishedDelayer: Delayer;
|
||||
|
||||
constructor(world: World, desktop: number) {
|
||||
this.world = world;
|
||||
constructor(desktop: Desktop, config: LayoutConfig) {
|
||||
this.desktop = desktop;
|
||||
this.config = config;
|
||||
this.columns = new LinkedList();
|
||||
this.lastFocusedColumn = null;
|
||||
this.scrollX = 0;
|
||||
this.width = 0;
|
||||
this.userResize = false;
|
||||
this.desktop = desktop;
|
||||
this.updateArea();
|
||||
this.userResizeFinishedDelayer = new Delayer(50, () => {
|
||||
// this delay prevents windows' contents from freezing after resizing
|
||||
this.autoAdjustScroll();
|
||||
this.arrange();
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
this.desktop.arrange();
|
||||
});
|
||||
}
|
||||
|
||||
updateArea() {
|
||||
const newClientArea = workspace.clientArea(workspace.PlacementArea, 0, this.desktop);
|
||||
if (newClientArea === this.clientArea) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clientArea = newClientArea;
|
||||
this.tilingArea = Qt.rect(
|
||||
newClientArea.x + this.world.config.gapsOuterLeft,
|
||||
newClientArea.y + this.world.config.gapsOuterTop,
|
||||
newClientArea.width - this.world.config.gapsOuterLeft - this.world.config.gapsOuterRight,
|
||||
newClientArea.height - this.world.config.gapsOuterTop - this.world.config.gapsOuterBottom,
|
||||
)
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.resizeWindows();
|
||||
}
|
||||
|
||||
this.autoAdjustScroll();
|
||||
}
|
||||
|
||||
moveColumnLeft(column: Column) {
|
||||
public moveColumnLeft(column: Column) {
|
||||
this.columns.moveBack(column);
|
||||
this.columnsSetX(column);
|
||||
this.autoAdjustScroll();
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
moveColumnRight(column: Column) {
|
||||
public moveColumnRight(column: Column) {
|
||||
const nextColumn = this.columns.getNext(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
@@ -60,208 +39,203 @@ class Grid {
|
||||
this.moveColumnLeft(nextColumn);
|
||||
}
|
||||
|
||||
getPrevColumn(column: Column) {
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public isUserResizing() {
|
||||
return this.userResize;
|
||||
}
|
||||
|
||||
public getPrevColumn(column: Column) {
|
||||
return this.columns.getPrev(column);
|
||||
}
|
||||
|
||||
getNextColumn(column: Column) {
|
||||
public getNextColumn(column: Column) {
|
||||
return this.columns.getNext(column);
|
||||
}
|
||||
|
||||
getFirstColumn() {
|
||||
public getFirstColumn() {
|
||||
return this.columns.getFirst();
|
||||
}
|
||||
|
||||
getLastColumn() {
|
||||
public getLastColumn() {
|
||||
return this.columns.getLast();
|
||||
}
|
||||
|
||||
getColumnAtIndex(i: number) {
|
||||
public getColumnAtIndex(i: number) {
|
||||
return this.columns.getItemAtIndex(i);
|
||||
}
|
||||
|
||||
getLastFocusedColumn() {
|
||||
public getLastFocusedColumn() {
|
||||
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
|
||||
return null;
|
||||
}
|
||||
return this.lastFocusedColumn;
|
||||
}
|
||||
|
||||
getLeftmostVisibleColumn(fullyVisible: boolean) {
|
||||
public getLastFocusedWindow() {
|
||||
const lastFocusedColumn = this.getLastFocusedColumn();
|
||||
if (lastFocusedColumn === null) {
|
||||
return null;
|
||||
}
|
||||
return lastFocusedColumn.getFocusTaker();
|
||||
}
|
||||
|
||||
private columnsSetX(firstMovedColumn: Column|null) {
|
||||
const lastUnmovedColumn = firstMovedColumn === null ? this.columns.getLast() : this.columns.getPrev(firstMovedColumn);
|
||||
let x = lastUnmovedColumn === null ? 0 : lastUnmovedColumn.getRight() + this.config.gapsInnerHorizontal;
|
||||
if (firstMovedColumn !== null) {
|
||||
for (const column of this.columns.iteratorFrom(firstMovedColumn)) {
|
||||
column.gridX = x;
|
||||
x += column.getWidth() + this.config.gapsInnerHorizontal;
|
||||
}
|
||||
}
|
||||
this.width = x - this.config.gapsInnerHorizontal;
|
||||
}
|
||||
|
||||
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
const left = column.gridX - this.scrollX; // in screen space
|
||||
const right = left + column.width; // in screen space
|
||||
const x = fullyVisible ? left : right;
|
||||
if (x >= 0) {
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getRightmostVisibleColumn(fullyVisible: boolean) {
|
||||
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
let last = null;
|
||||
for (const column of this.columns.iterator()) {
|
||||
const left = column.gridX - this.scrollX; // in screen space
|
||||
const right = left + column.width; // in screen space
|
||||
const x = fullyVisible ? right : left;
|
||||
if (x <= this.tilingArea.width) {
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
last = column;
|
||||
} else {
|
||||
} else if (last !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
scrollToColumn(column: Column) {
|
||||
const left = column.gridX - this.scrollX; // in screen space
|
||||
const right = left + column.width; // in screen space
|
||||
const remainingSpace = this.tilingArea.width - column.width;
|
||||
const overScrollX = Math.min(this.world.config.overscroll, Math.round(remainingSpace / 2));
|
||||
if (left < 0) {
|
||||
this.adjustScroll(left - overScrollX, false);
|
||||
} else if (right > this.tilingArea.width) {
|
||||
this.adjustScroll(right - this.tilingArea.width + overScrollX, false);
|
||||
} else {
|
||||
this.removeOverscroll();
|
||||
}
|
||||
}
|
||||
|
||||
autoAdjustScroll() {
|
||||
const focusedWindow = this.world.getFocusedWindow();
|
||||
if (focusedWindow === null) {
|
||||
this.removeOverscroll();
|
||||
return;
|
||||
}
|
||||
|
||||
const column = focusedWindow.column;
|
||||
if (column.grid !== this) {
|
||||
return;
|
||||
}
|
||||
this.scrollToColumn(column);
|
||||
}
|
||||
|
||||
setScroll(x: number, force: boolean) {
|
||||
if (!force) {
|
||||
let minScroll = 0;
|
||||
let maxScroll = this.width - this.tilingArea.width;
|
||||
if (maxScroll < 0) {
|
||||
const centerScroll = Math.round(maxScroll / 2);
|
||||
minScroll = centerScroll;
|
||||
maxScroll = centerScroll;
|
||||
}
|
||||
x = clamp(x, minScroll, maxScroll);
|
||||
}
|
||||
this.scrollX = x;
|
||||
}
|
||||
|
||||
adjustScroll(dx: number, force: boolean) {
|
||||
this.setScroll(this.scrollX + dx, force);
|
||||
}
|
||||
|
||||
removeOverscroll() {
|
||||
this.setScroll(this.scrollX, false);
|
||||
}
|
||||
|
||||
columnsSetX(firstMovedColumn: Column|null) {
|
||||
const lastUnmovedColumn = firstMovedColumn === null ? this.columns.getLast() : this.columns.getPrev(firstMovedColumn);
|
||||
let x = lastUnmovedColumn === null ? 0 : lastUnmovedColumn.gridX + lastUnmovedColumn.width + this.world.config.gapsInnerHorizontal;
|
||||
if (firstMovedColumn !== null) {
|
||||
for (const column of this.columns.iteratorFrom(firstMovedColumn)) {
|
||||
column.gridX = x;
|
||||
x += column.width + this.world.config.gapsInnerHorizontal;
|
||||
}
|
||||
}
|
||||
this.width = x - this.world.config.gapsInnerHorizontal;
|
||||
}
|
||||
|
||||
arrange() {
|
||||
// TODO (optimization): only arrange visible windows
|
||||
this.updateArea();
|
||||
let x = this.tilingArea.x - this.scrollX;
|
||||
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.arrange(x);
|
||||
x += column.getWidth() + this.world.config.gapsInnerHorizontal;
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
yield column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onColumnAdded(column: Column, prevColumn: Column|null) {
|
||||
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
let width = 0;
|
||||
let nVisible = 0;
|
||||
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
|
||||
width += column.getWidth();
|
||||
nVisible++;
|
||||
}
|
||||
|
||||
if (nVisible > 0) {
|
||||
width += (nVisible-1) * this.config.gapsInnerHorizontal;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
public arrange(x: number, visibleRange: Range) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.arrange(x, visibleRange, this.userResize);
|
||||
x += column.getWidth() + this.config.gapsInnerHorizontal;
|
||||
}
|
||||
|
||||
const focusedWindow = this.getLastFocusedWindow();
|
||||
if (focusedWindow !== null) {
|
||||
focusedWindow.client.ensureTransientsVisible(this.desktop.clientArea);
|
||||
}
|
||||
}
|
||||
|
||||
public onColumnAdded(column: Column, prevColumn: Column|null) {
|
||||
if (prevColumn === null) {
|
||||
this.columns.insertStart(column);
|
||||
} else {
|
||||
this.columns.insertAfter(column, prevColumn);
|
||||
}
|
||||
this.columnsSetX(column);
|
||||
this.autoAdjustScroll();
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
onColumnRemoved(column: Column, passFocus: boolean) {
|
||||
public onColumnRemoved(column: Column, passFocus: boolean) {
|
||||
const isLastColumn = this.columns.length() === 1;
|
||||
const nextColumn = this.getNextColumn(column);
|
||||
const columnToFocus = isLastColumn ? null : this.getPrevColumn(column) ?? nextColumn;
|
||||
if (column === this.lastFocusedColumn) {
|
||||
this.lastFocusedColumn = null;
|
||||
this.lastFocusedColumn = columnToFocus;
|
||||
}
|
||||
|
||||
const lastColumn = this.columns.length() === 1;
|
||||
const columnToFocus = lastColumn || !passFocus ? null : this.getPrevColumn(column) ?? this.getNextColumn(column);
|
||||
const nextColumn = this.columns.getNext(column);
|
||||
|
||||
this.columns.remove(column);
|
||||
|
||||
this.columnsSetX(nextColumn);
|
||||
if (columnToFocus !== null) {
|
||||
|
||||
this.desktop.onLayoutChanged();
|
||||
if (passFocus && columnToFocus !== null) {
|
||||
columnToFocus.focus();
|
||||
} else {
|
||||
this.removeOverscroll();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
}
|
||||
|
||||
onColumnMoved(column: Column, prevColumn: Column|null) {
|
||||
public onColumnMoved(column: Column, prevColumn: Column|null) {
|
||||
const movedLeft = prevColumn === null ? true : column.isAfter(prevColumn);
|
||||
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
|
||||
this.columns.move(column, prevColumn);
|
||||
this.columnsSetX(firstMovedColumn);
|
||||
this.autoAdjustScroll();
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
onColumnWidthChanged(column: Column, oldWidth: number, width: number) {
|
||||
public onColumnWidthChanged(column: Column) {
|
||||
const nextColumn = this.columns.getNext(column);
|
||||
this.columnsSetX(nextColumn);
|
||||
this.desktop.onLayoutChanged();
|
||||
if (!this.userResize) {
|
||||
this.autoAdjustScroll();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
}
|
||||
|
||||
onColumnFocused(column: Column) {
|
||||
public onColumnFocused(column: Column) {
|
||||
const lastFocusedColumn = this.getLastFocusedColumn();
|
||||
if (lastFocusedColumn !== null) {
|
||||
lastFocusedColumn.restoreToTiled();
|
||||
}
|
||||
this.lastFocusedColumn = column;
|
||||
this.scrollToColumn(column);
|
||||
this.desktop.scrollToColumn(column);
|
||||
}
|
||||
|
||||
onUserResizeStarted() {
|
||||
public onScreenSizeChanged() {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.updateWidth();
|
||||
column.resizeWindows();
|
||||
}
|
||||
}
|
||||
|
||||
public onUserResizeStarted() {
|
||||
this.userResize = true;
|
||||
}
|
||||
|
||||
onUserResizeFinished() {
|
||||
public onUserResizeFinished() {
|
||||
this.userResize = false;
|
||||
this.userResizeFinishedDelayer.run();
|
||||
}
|
||||
|
||||
evacuateTail(targetGrid: Grid, startColumn: Column) {
|
||||
public evacuateTail(targetGrid: Grid, startColumn: Column) {
|
||||
for (const column of this.columns.iteratorFrom(startColumn)) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
evacuate(targetGrid: Grid) {
|
||||
public evacuate(targetGrid: Grid) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
public destroy() {
|
||||
this.userResizeFinishedDelayer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
11
src/layout/LayoutConfig.ts
Normal file
11
src/layout/LayoutConfig.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
type LayoutConfig = {
|
||||
gapsInnerHorizontal: number,
|
||||
gapsInnerVertical: number,
|
||||
offScreenOpacity: number,
|
||||
stackColumnsByDefault: boolean,
|
||||
resizeNeighborColumn: boolean,
|
||||
reMaximize: boolean,
|
||||
skipSwitcher: boolean,
|
||||
tiledKeepBelow: boolean,
|
||||
maximizedKeepAbove: boolean,
|
||||
};
|
||||
@@ -2,7 +2,7 @@ class Window {
|
||||
public column: Column;
|
||||
public readonly client: ClientWrapper;
|
||||
public height: number;
|
||||
public readonly focusedState: WindowState;
|
||||
public readonly focusedState: Window.State;
|
||||
private skipArrange: boolean;
|
||||
|
||||
constructor(client: ClientWrapper, column: Column) {
|
||||
@@ -18,7 +18,7 @@ class Window {
|
||||
column.onWindowAdded(this);
|
||||
}
|
||||
|
||||
moveToColumn(targetColumn: Column) {
|
||||
public moveToColumn(targetColumn: Column) {
|
||||
if (targetColumn === this.column) {
|
||||
return;
|
||||
}
|
||||
@@ -27,21 +27,31 @@ class Window {
|
||||
targetColumn.onWindowAdded(this);
|
||||
}
|
||||
|
||||
arrange(x: number, y: number, width: number, height: number) {
|
||||
public arrange(x: number, y: number, width: number, height: number) {
|
||||
if (this.skipArrange) {
|
||||
// window is being manually resized, prevent fighting with the user
|
||||
// window is maximized, fullscreen, or being manually resized, prevent fighting with the user
|
||||
return;
|
||||
}
|
||||
this.client.place(x, y, width, height);
|
||||
if (this.isFocused()) {
|
||||
|
||||
let maximized = false;
|
||||
if (this.column.grid.config.reMaximize && this.isFocused()) {
|
||||
// do this here rather than in `onFocused` to ensure it happens after placement
|
||||
// (otherwise placement may not happen at all)
|
||||
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
|
||||
this.client.setFullScreen(this.focusedState.fullScreen);
|
||||
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
|
||||
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
|
||||
maximized = true;
|
||||
}
|
||||
if (this.focusedState.fullScreen) {
|
||||
this.client.setFullScreen(true);
|
||||
maximized = true;
|
||||
}
|
||||
}
|
||||
if (!maximized) {
|
||||
this.client.place(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
public focus() {
|
||||
if (this.client.isShaded()) {
|
||||
// workaround for KWin deactivating clients when unshading immediately after activation
|
||||
this.client.setShade(false);
|
||||
@@ -49,67 +59,93 @@ class Window {
|
||||
this.client.focus();
|
||||
}
|
||||
|
||||
isFocused() {
|
||||
public isFocused() {
|
||||
return this.client.isFocused();
|
||||
}
|
||||
|
||||
onFocused() {
|
||||
public onFocused() {
|
||||
this.column.onWindowFocused(this);
|
||||
}
|
||||
|
||||
restoreToTiled() {
|
||||
public restoreToTiled() {
|
||||
if (this.isFocused()) {
|
||||
return;
|
||||
}
|
||||
this.client.setMaximize(false, false);
|
||||
this.client.setFullScreen(false);
|
||||
this.client.setMaximize(false, false);
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onMaximizedChanged(horizontally: boolean, vertically: boolean) {
|
||||
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
|
||||
const maximized = horizontally || vertically;
|
||||
this.skipArrange = maximized;
|
||||
this.client.kwinClient.keepBelow = !maximized;
|
||||
if (this.column.grid.config.tiledKeepBelow) {
|
||||
this.client.kwinClient.keepBelow = !maximized;
|
||||
}
|
||||
if (this.column.grid.config.maximizedKeepAbove) {
|
||||
this.client.kwinClient.keepAbove = maximized;
|
||||
}
|
||||
if (this.isFocused()) {
|
||||
this.focusedState.maximizedHorizontally = horizontally;
|
||||
this.focusedState.maximizedVertically = vertically;
|
||||
}
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onFullScreenChanged(fullScreen: boolean) {
|
||||
public onFullScreenChanged(fullScreen: boolean) {
|
||||
this.skipArrange = fullScreen;
|
||||
if (this.isFocused()) {
|
||||
if (this.column.grid.config.tiledKeepBelow) {
|
||||
this.client.kwinClient.keepBelow = !fullScreen;
|
||||
}
|
||||
if (this.column.grid.config.maximizedKeepAbove) {
|
||||
this.client.kwinClient.keepAbove = fullScreen;
|
||||
}
|
||||
if (this.isFocused()) {
|
||||
this.focusedState.fullScreen = fullScreen;
|
||||
}
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
onUserResize(oldGeometry: QRect) {
|
||||
public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
|
||||
const newGeometry = this.client.kwinClient.frameGeometry;
|
||||
const widthDelta = newGeometry.width - oldGeometry.width;
|
||||
const heightDelta = newGeometry.height - oldGeometry.height;
|
||||
if (widthDelta !== 0) {
|
||||
this.column.adjustWidth(widthDelta, true);
|
||||
if (newGeometry.x !== oldGeometry.x) {
|
||||
this.column.grid.adjustScroll(widthDelta, true);
|
||||
let leftEdgeDelta = newGeometry.left - oldGeometry.left;
|
||||
const resizingLeftSide = leftEdgeDelta !== 0;
|
||||
if (resizeNeighborColumn && this.column.grid.config.resizeNeighborColumn) {
|
||||
const neighborColumn = resizingLeftSide ? this.column.grid.getPrevColumn(this.column) : this.column.grid.getNextColumn(this.column);
|
||||
if (neighborColumn !== null) {
|
||||
const oldNeighborWidth = neighborColumn.getWidth();
|
||||
neighborColumn.adjustWidth(-widthDelta, true);
|
||||
if (resizingLeftSide) {
|
||||
leftEdgeDelta -= neighborColumn.getWidth() - oldNeighborWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.column.grid.desktop.adjustScroll(-leftEdgeDelta, true);
|
||||
}
|
||||
if (heightDelta !== 0) {
|
||||
this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y);
|
||||
}
|
||||
}
|
||||
|
||||
onProgrammaticResize(oldGeometry: QRect) {
|
||||
public onFrameGeometryChanged() {
|
||||
const newGeometry = this.client.kwinClient.frameGeometry;
|
||||
this.column.setWidth(newGeometry.width, true);
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: boolean) {
|
||||
this.column.onWindowRemoved(this, passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
type WindowState = {
|
||||
fullScreen: boolean,
|
||||
maximizedHorizontally: boolean,
|
||||
maximizedVertically: boolean,
|
||||
namespace Window {
|
||||
export type State = {
|
||||
fullScreen: boolean,
|
||||
maximizedHorizontally: boolean,
|
||||
maximizedVertically: boolean,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
function init() {
|
||||
const config = loadConfig();
|
||||
const world = new World(config);
|
||||
registerShortcuts(world);
|
||||
registerKeyBindings(world, config);
|
||||
return world;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ class ClientMatcher {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
matches(kwinClient: AbstractClient) {
|
||||
const rule = this.rules.get(String(kwinClient.resourceClass));
|
||||
public matches(kwinClient: KwinClient) {
|
||||
const rule = this.rules.get(kwinClient.resourceClass);
|
||||
if (rule === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ type WindowRule = {
|
||||
class: string,
|
||||
caption: string,
|
||||
tile: boolean,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,21 +3,25 @@ class WindowRuleEnforcer {
|
||||
private readonly preferTiling: ClientMatcher;
|
||||
private readonly followCaption: Set<string>;
|
||||
|
||||
constructor(world: World, windowRules: WindowRule[]) {
|
||||
const [mapFloat, mapTile] = createWindowRuleMaps(windowRules);
|
||||
constructor(windowRules: WindowRule[]) {
|
||||
const [mapFloat, mapTile] = WindowRuleEnforcer.createWindowRuleMaps(windowRules);
|
||||
this.preferFloating = new ClientMatcher(mapFloat);
|
||||
this.preferTiling = new ClientMatcher(mapTile);
|
||||
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]);
|
||||
}
|
||||
|
||||
shouldTile(kwinClient: AbstractClient) {
|
||||
return canTileNow(kwinClient) && (
|
||||
this.preferTiling.matches(kwinClient) ||
|
||||
kwinClient.normalWindow && kwinClient.managed && !this.preferFloating.matches(kwinClient)
|
||||
public shouldTile(kwinClient: KwinClient) {
|
||||
return Clients.canTileNow(kwinClient) && (
|
||||
this.preferTiling.matches(kwinClient) || (
|
||||
kwinClient.normalWindow &&
|
||||
!kwinClient.transient &&
|
||||
kwinClient.managed &&
|
||||
!this.preferFloating.matches(kwinClient)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
initClientSignalManager(world: World, kwinClient: AbstractClient) {
|
||||
public initClientSignalManager(world: World, kwinClient: KwinClient) {
|
||||
if (!this.followCaption.has(kwinClient.resourceClass)) {
|
||||
return null;
|
||||
}
|
||||
@@ -26,58 +30,61 @@ class WindowRuleEnforcer {
|
||||
const manager = new SignalManager();
|
||||
manager.connect(kwinClient.captionChanged, () => {
|
||||
const shouldTile = enforcer.shouldTile(kwinClient);
|
||||
if (shouldTile) {
|
||||
world.tileClient(kwinClient);
|
||||
} else {
|
||||
world.untileClient(kwinClient);
|
||||
}
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(kwinClient);
|
||||
if (shouldTile && desktop !== undefined) {
|
||||
clientManager.tileClient(kwinClient, desktop.grid);
|
||||
} else {
|
||||
clientManager.untileClient(kwinClient);
|
||||
}
|
||||
});
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
||||
function createWindowRuleMaps(windowRules: WindowRule[]) {
|
||||
const mapFloat = new Map<string, string[]>();
|
||||
const mapTile = new Map<string, string[]>();
|
||||
for (const windowRule of windowRules) {
|
||||
const map = windowRule.tile ? mapTile : mapFloat;
|
||||
let captions = map.get(windowRule.class);
|
||||
if (captions === undefined) {
|
||||
captions = [];
|
||||
map.set(windowRule.class, captions);
|
||||
private static createWindowRuleMaps(windowRules: WindowRule[]) {
|
||||
const mapFloat = new Map<string, string[]>();
|
||||
const mapTile = new Map<string, string[]>();
|
||||
for (const windowRule of windowRules) {
|
||||
const map = windowRule.tile ? mapTile : mapFloat;
|
||||
let captions = map.get(windowRule.class);
|
||||
if (captions === undefined) {
|
||||
captions = [];
|
||||
map.set(windowRule.class, captions);
|
||||
}
|
||||
if (windowRule.caption !== undefined) {
|
||||
captions.push(windowRule.caption);
|
||||
}
|
||||
}
|
||||
if (windowRule.caption !== undefined) {
|
||||
captions.push(windowRule.caption);
|
||||
|
||||
return [
|
||||
WindowRuleEnforcer.createWindowRuleRegexMap(mapFloat),
|
||||
WindowRuleEnforcer.createWindowRuleRegexMap(mapTile),
|
||||
];
|
||||
}
|
||||
|
||||
private static createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
|
||||
const regexMap = new Map<string, RegExp>;
|
||||
for (const [k, v] of windowRuleMap) {
|
||||
regexMap.set(k, WindowRuleEnforcer.joinRegexes(v));
|
||||
}
|
||||
return regexMap;
|
||||
}
|
||||
|
||||
return [
|
||||
createWindowRuleRegexMap(mapFloat),
|
||||
createWindowRuleRegexMap(mapTile),
|
||||
];
|
||||
}
|
||||
private static joinRegexes(regexes: string[]) {
|
||||
if (regexes.length === 0) {
|
||||
return new RegExp("");
|
||||
}
|
||||
|
||||
function createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
|
||||
const regexMap = new Map<string, RegExp>;
|
||||
for (const [k, v] of windowRuleMap) {
|
||||
regexMap.set(k, joinRegexes(v));
|
||||
}
|
||||
return regexMap;
|
||||
}
|
||||
if (regexes.length === 1) {
|
||||
return new RegExp("^" + regexes[0] + "$");
|
||||
}
|
||||
|
||||
function joinRegexes(regexes: string[]) {
|
||||
if (regexes.length == 0) {
|
||||
return new RegExp("");
|
||||
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
|
||||
return new RegExp("^" + joinedRegexes + "$");
|
||||
}
|
||||
|
||||
if (regexes.length == 1) {
|
||||
return new RegExp("^" + regexes[0] + "$");
|
||||
private static wrapParens(str: string) {
|
||||
return "(" + str + ")";
|
||||
}
|
||||
|
||||
const joinedRegexes = regexes.map(wrapParens).join("|");
|
||||
return new RegExp("^" + joinedRegexes + "$");
|
||||
}
|
||||
|
||||
function wrapParens(str: string) {
|
||||
return "(" + str + ")";
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
function catchWrap(f: () => void) {
|
||||
return () => {
|
||||
try {
|
||||
f();
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
console.log(error.stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function registerShortcutDbg(title: string, text: string, keySequence: string, callback: () => void) {
|
||||
KWin.registerShortcut(title, text, keySequence, catchWrap(callback));
|
||||
}
|
||||
|
||||
function registerNumShortcuts(title: string, text: string, keySequence: string, callback: (i: number) => void, n: number) {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const numKey = String(i + 1);
|
||||
registerShortcutDbg(title+numKey, text+numKey, keySequence+numKey, () => callback(i));
|
||||
}
|
||||
}
|
||||
|
||||
function registerShortcuts(world: World) {
|
||||
const actions = initActions(world);
|
||||
|
||||
registerShortcutDbg("karousel-window-toggle-floating", "Karousel: Toggle floating", "Meta+Space", actions.windowToggleFloating);
|
||||
|
||||
registerShortcutDbg("karousel-focus-left", "Karousel: Move focus left", "Meta+A", actions.focusLeft);
|
||||
registerShortcutDbg("karousel-focus-right", "Karousel: Move focus right", "Meta+D", actions.focusRight);
|
||||
registerShortcutDbg("karousel-focus-up", "Karousel: Move focus up", "Meta+W", actions.focusUp);
|
||||
registerShortcutDbg("karousel-focus-down", "Karousel: Move focus down", "Meta+S", actions.focusDown);
|
||||
registerShortcutDbg("karousel-focus-start", "Karousel: Move focus to start", "Meta+Home", actions.focusStart);
|
||||
registerShortcutDbg("karousel-focus-end", "Karousel: Move focus to end", "Meta+End", actions.focusEnd);
|
||||
|
||||
registerShortcutDbg("karousel-window-move-left", "Karousel: Move window left", "Meta+Shift+A", actions.windowMoveLeft);
|
||||
registerShortcutDbg("karousel-window-move-right", "Karousel: Move window right", "Meta+Shift+D", actions.windowMoveRight);
|
||||
registerShortcutDbg("karousel-window-move-up", "Karousel: Move window up", "Meta+Shift+W", actions.windowMoveUp);
|
||||
registerShortcutDbg("karousel-window-move-down", "Karousel: Move window down", "Meta+Shift+S", actions.windowMoveDown);
|
||||
registerShortcutDbg("karousel-window-move-start", "Karousel: Move window to start", "Meta+Shift+Home", actions.windowMoveStart);
|
||||
registerShortcutDbg("karousel-window-move-end", "Karousel: Move window to end", "Meta+Shift+End", actions.windowMoveEnd);
|
||||
registerShortcutDbg("karousel-window-expand", "Karousel: Expand window", "Meta+X", actions.windowExpand);
|
||||
|
||||
registerShortcutDbg("karousel-column-move-left", "Karousel: Move column left", "Meta+Ctrl+Shift+A", actions.columnMoveLeft);
|
||||
registerShortcutDbg("karousel-column-move-right", "Karousel: Move column right", "Meta+Ctrl+Shift+D", actions.columnMoveRight);
|
||||
registerShortcutDbg("karousel-column-move-start", "Karousel: Move column to start", "Meta+Ctrl+Shift+Home", actions.columnMoveStart);
|
||||
registerShortcutDbg("karousel-column-move-end", "Karousel: Move column to end", "Meta+Ctrl+Shift+End", actions.columnMoveEnd);
|
||||
registerShortcutDbg("karousel-column-expand", "Karousel: Expand column", "Meta+Ctrl+X", actions.columnExpand);
|
||||
|
||||
registerShortcutDbg("karousel-grid-scroll-focused", "Karousel: Scroll to focused window", "Meta+Alt+Return", actions.gridScrollFocused);
|
||||
registerShortcutDbg("karousel-grid-scroll-left-column", "Karousel: Scroll one column to the left", "Meta+Alt+A", actions.gridScrollLeftColumn);
|
||||
registerShortcutDbg("karousel-grid-scroll-left-column", "Karousel: Scroll one column to the left", "Meta+Alt+A", actions.gridScrollLeftColumn);
|
||||
registerShortcutDbg("karousel-grid-scroll-right-column", "Karousel: Scroll one column to the right", "Meta+Alt+D", actions.gridScrollRightColumn);
|
||||
registerShortcutDbg("karousel-grid-scroll-left", "Karousel: Scroll left", "Meta+Alt+PgUp", actions.gridScrollLeft);
|
||||
registerShortcutDbg("karousel-grid-scroll-right", "Karousel: Scroll right", "Meta+Alt+PgDown", actions.gridScrollRight);
|
||||
registerShortcutDbg("karousel-grid-scroll-start", "Karousel: Scroll to start", "Meta+Alt+Home", actions.gridScrollStart);
|
||||
registerShortcutDbg("karousel-grid-scroll-end", "Karousel: Scroll to end", "Meta+Alt+End", actions.gridScrollEnd);
|
||||
|
||||
registerNumShortcuts("karousel-focus-", "Karousel: Move focus to column ", "Meta+", actions.focusColumn, 9);
|
||||
registerNumShortcuts("karousel-window-move-to-column-", "Karousel: Move window to column ", "Meta+Shift+", actions.windowMoveToColumn, 9);
|
||||
registerNumShortcuts("karousel-column-move-to-column-", "Karousel: Move column to position ", "Meta+Ctrl+Shift+", actions.columnMoveToColumn, 9);
|
||||
registerNumShortcuts("karousel-column-move-to-desktop-", "Karousel: Move column to desktop ", "Meta+Ctrl+Shift+F", actions.columnMoveToDesktop, 12);
|
||||
registerNumShortcuts("karousel-tail-move-to-desktop-", "Karousel: Move this and all following columns to desktop ", "Meta+Ctrl+Shift+Alt+F", actions.tailMoveToDesktop, 12);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
class Delayer {
|
||||
private readonly timer: QQmlTimer;
|
||||
private readonly timer: QmlTimer;
|
||||
|
||||
constructor(delay: number, f: () => void) {
|
||||
this.timer = initQmlTimer();
|
||||
@@ -7,11 +7,11 @@ class Delayer {
|
||||
this.timer.triggered.connect(f);
|
||||
}
|
||||
|
||||
run() {
|
||||
public run() {
|
||||
this.timer.restart();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
public destroy() {
|
||||
this.timer.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ class Doer {
|
||||
this.nCalls = 0;
|
||||
}
|
||||
|
||||
do (f: () => void) {
|
||||
public do (f: () => void) {
|
||||
this.nCalls++;
|
||||
f();
|
||||
this.nCalls--;
|
||||
}
|
||||
|
||||
isDoing() {
|
||||
public isDoing() {
|
||||
return this.nCalls > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class LinkedList<T> {
|
||||
private firstNode: LinkedListNode<T>|null;
|
||||
private lastNode: LinkedListNode<T>|null;
|
||||
private readonly itemMap: Map<T, LinkedListNode<T>>;
|
||||
private firstNode: LinkedList.Node<T>|null;
|
||||
private lastNode: LinkedList.Node<T>|null;
|
||||
private readonly itemMap: Map<T, LinkedList.Node<T>>;
|
||||
|
||||
constructor() {
|
||||
this.firstNode = null;
|
||||
@@ -17,31 +17,31 @@ class LinkedList<T> {
|
||||
return node;
|
||||
}
|
||||
|
||||
insertBefore(item: T, nextItem: T) {
|
||||
public insertBefore(item: T, nextItem: T) {
|
||||
const nextNode = this.getNode(nextItem);
|
||||
this.insert(item, nextNode.prev, nextNode);
|
||||
}
|
||||
|
||||
insertAfter(item: T, prevItem: T) {
|
||||
public insertAfter(item: T, prevItem: T) {
|
||||
const prevNode = this.getNode(prevItem);
|
||||
this.insert(item, prevNode, prevNode.next);
|
||||
}
|
||||
|
||||
insertStart(item: T) {
|
||||
public insertStart(item: T) {
|
||||
this.insert(item, null, this.firstNode);
|
||||
}
|
||||
|
||||
insertEnd(item: T) {
|
||||
public insertEnd(item: T) {
|
||||
this.insert(item, this.lastNode, null);
|
||||
}
|
||||
|
||||
private insert(item: T, prevNode: LinkedListNode<T>|null, nextNode: LinkedListNode<T>|null) {
|
||||
const node = new LinkedListNode(item);
|
||||
private insert(item: T, prevNode: LinkedList.Node<T>|null, nextNode: LinkedList.Node<T>|null) {
|
||||
const node = new LinkedList.Node(item);
|
||||
this.itemMap.set(item, node);
|
||||
this.insertNode(node, prevNode, nextNode);
|
||||
}
|
||||
|
||||
private insertNode(node: LinkedListNode<T>, prevNode: LinkedListNode<T>|null, nextNode: LinkedListNode<T>|null) {
|
||||
private insertNode(node: LinkedList.Node<T>, prevNode: LinkedList.Node<T>|null, nextNode: LinkedList.Node<T>|null) {
|
||||
node.prev = prevNode;
|
||||
node.next = nextNode;
|
||||
if (nextNode !== null) {
|
||||
@@ -60,31 +60,31 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
getPrev(item: T) {
|
||||
public getPrev(item: T) {
|
||||
const prevNode = this.getNode(item).prev;
|
||||
return prevNode === null ? null : prevNode.item;
|
||||
}
|
||||
|
||||
getNext(item: T) {
|
||||
public getNext(item: T) {
|
||||
const nextNode = this.getNode(item).next;
|
||||
return nextNode === null ? null : nextNode.item;
|
||||
}
|
||||
|
||||
getFirst() {
|
||||
public getFirst() {
|
||||
if (this.firstNode === null) {
|
||||
return null;
|
||||
}
|
||||
return this.firstNode.item;
|
||||
}
|
||||
|
||||
getLast() {
|
||||
public getLast() {
|
||||
if (this.lastNode === null) {
|
||||
return null;
|
||||
}
|
||||
return this.lastNode.item;
|
||||
}
|
||||
|
||||
getItemAtIndex(index: number) {
|
||||
public getItemAtIndex(index: number) {
|
||||
let node = this.firstNode;
|
||||
if (node === null) {
|
||||
return null;
|
||||
@@ -98,13 +98,13 @@ class LinkedList<T> {
|
||||
return node.item;
|
||||
}
|
||||
|
||||
remove(item: T) {
|
||||
public remove(item: T) {
|
||||
const node = this.getNode(item);
|
||||
this.itemMap.delete(item);
|
||||
this.removeNode(node);
|
||||
}
|
||||
|
||||
private removeNode(node: LinkedListNode<T>) {
|
||||
private removeNode(node: LinkedList.Node<T>) {
|
||||
const prevNode = node.prev;
|
||||
const nextNode = node.next;
|
||||
if (prevNode !== null) {
|
||||
@@ -121,11 +121,11 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
contains(item: T) {
|
||||
public contains(item: T) {
|
||||
return this.itemMap.has(item);
|
||||
}
|
||||
|
||||
private swap(node0: LinkedListNode<T>, node1: LinkedListNode<T>) {
|
||||
private swap(node0: LinkedList.Node<T>, node1: LinkedList.Node<T>) {
|
||||
console.assert(node0.next === node1 && node1.prev === node0);
|
||||
const prevNode = node0.prev;
|
||||
const nextNode = node1.next;
|
||||
@@ -150,7 +150,7 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
move(item: T, prevItem: T|null) {
|
||||
public move(item: T, prevItem: T|null) {
|
||||
const node = this.getNode(item);
|
||||
this.removeNode(node);
|
||||
if (prevItem === null) {
|
||||
@@ -161,7 +161,7 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
moveBack(item: T) {
|
||||
public moveBack(item: T) {
|
||||
const node = this.getNode(item);
|
||||
if (node.prev !== null) {
|
||||
console.assert(node !== this.firstNode);
|
||||
@@ -169,7 +169,7 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
moveForward(item: T) {
|
||||
public moveForward(item: T) {
|
||||
const node = this.getNode(item);
|
||||
if (node.next !== null) {
|
||||
console.assert(node !== this.lastNode);
|
||||
@@ -177,32 +177,34 @@ class LinkedList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
length() {
|
||||
public length() {
|
||||
return this.itemMap.size;
|
||||
}
|
||||
|
||||
*iterator() {
|
||||
public *iterator() {
|
||||
for (let node = this.firstNode; node !== null; node = node.next) {
|
||||
yield node.item;
|
||||
}
|
||||
}
|
||||
|
||||
*iteratorFrom(startItem: T) {
|
||||
for (let node: LinkedListNode<T>|null = this.getNode(startItem); node !== null; node = node.next) {
|
||||
public *iteratorFrom(startItem: T) {
|
||||
for (let node: LinkedList.Node<T>|null = this.getNode(startItem); node !== null; node = node.next) {
|
||||
yield node.item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (optimization): reuse nodes
|
||||
class LinkedListNode<T> {
|
||||
public readonly item: T;
|
||||
public prev: LinkedListNode<T>|null;
|
||||
public next: LinkedListNode<T>|null;
|
||||
namespace LinkedList {
|
||||
// TODO (optimization): reuse nodes
|
||||
export class Node<T> {
|
||||
public readonly item: T;
|
||||
public prev: Node<T> | null;
|
||||
public next: Node<T> | null;
|
||||
|
||||
constructor(item: T) {
|
||||
this.item = item;
|
||||
this.prev = null;
|
||||
this.next = null;
|
||||
constructor(item: T) {
|
||||
this.item = item;
|
||||
this.prev = null;
|
||||
this.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
class SignalManager {
|
||||
private connections: { signal: QSignal, handler: (...args: any[]) => void }[];
|
||||
private connections: { signal: QSignal<any>, handler: (...args: any) => void }[];
|
||||
|
||||
constructor() {
|
||||
this.connections = [];
|
||||
}
|
||||
|
||||
connect(signal: QSignal, handler: (...args: any[]) => void) {
|
||||
public connect<T extends unknown[]>(signal: QSignal<T>, handler: (...args: [...T]) => void) {
|
||||
signal.connect(handler);
|
||||
this.connections.push({ signal: signal, handler: handler });
|
||||
}
|
||||
|
||||
destroy() {
|
||||
public destroy() {
|
||||
for (const connection of this.connections) {
|
||||
connection.signal.disconnect(connection.handler);
|
||||
}
|
||||
|
||||
3
src/utils/log.ts
Normal file
3
src/utils/log.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
function log(...args: any[]) {
|
||||
console.log("Karousel:", ...args);
|
||||
}
|
||||
@@ -8,9 +8,7 @@ function clamp(value: number, min: number, max: number) {
|
||||
return value;
|
||||
}
|
||||
|
||||
function rectEqual(a: QRect, b: QRect) {
|
||||
return a.x === b.x &&
|
||||
a.y === b.y &&
|
||||
a.width === b.width &&
|
||||
a.height === b.height;
|
||||
function union<T>(array0: T[], array1: T[]) {
|
||||
const set = new Set([...array0, ...array1]);
|
||||
return [...set];
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
function initWorkspaceSignalHandlers(world: World) {
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(workspace.clientAdded, (kwinClient: AbstractClient) => {
|
||||
console.assert(!world.hasClient(kwinClient));
|
||||
if (canTileEver(kwinClient)) {
|
||||
manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => {
|
||||
if (Clients.canTileEver(kwinClient)) {
|
||||
// never open new tileable clients on all desktops or activities
|
||||
if (kwinClient.desktop <= 0) {
|
||||
kwinClient.desktop = workspace.currentDesktop;
|
||||
@@ -12,48 +11,56 @@ function initWorkspaceSignalHandlers(world: World) {
|
||||
kwinClient.activities = [workspace.currentActivity];
|
||||
}
|
||||
}
|
||||
world.addClient(kwinClient);
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientRemoved, (kwinClient: AbstractClient) => {
|
||||
console.assert(world.hasClient(kwinClient));
|
||||
world.removeClient(kwinClient, true);
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientMinimized, (kwinClient: AbstractClient) => {
|
||||
world.minimizeClient(kwinClient);
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientUnminimized, (kwinClient: AbstractClient) => {
|
||||
world.unminimizeClient(kwinClient);
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientMaximizeSet, (kwinClient: AbstractClient, horizontally: boolean, vertically: boolean) => {
|
||||
world.doIfTiled(kwinClient, false, (window, column, grid) => {
|
||||
window.onMaximizedChanged(horizontally, vertically);
|
||||
grid.arrange();
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.addClient(kwinClient)
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientActivated, (kwinClient: AbstractClient) => {
|
||||
manager.connect(workspace.clientRemoved, (kwinClient: KwinClient) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.removeClient(kwinClient, true);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientMinimized, (kwinClient: KwinClient) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.minimizeClient(kwinClient);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientUnminimized, (kwinClient: KwinClient) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.unminimizeClient(kwinClient);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientMaximizeSet, (kwinClient: KwinClient, horizontally: boolean, vertically: boolean) => {
|
||||
if ((horizontally || vertically) && kwinClient.tile !== null) {
|
||||
kwinClient.tile = null;
|
||||
}
|
||||
world.doIfTiled(kwinClient, false, (clientManager, desktopManager, window, column, grid) => {
|
||||
window.onMaximizedChanged(horizontally, vertically);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
|
||||
if (kwinClient === null) {
|
||||
return;
|
||||
}
|
||||
world.onClientFocused(kwinClient);
|
||||
world.doIfTiled(kwinClient, true, (window, column, grid) => {
|
||||
window.onFocused();
|
||||
grid.arrange();
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.onClientFocused(kwinClient);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(workspace.clientFullScreenSet, (kwinClient: X11Client, fullScreen: boolean, user: boolean) => {
|
||||
world.doIfTiled(kwinClient, false, (window, column, grid) => {
|
||||
window.onFullScreenChanged(fullScreen);
|
||||
grid.arrange();
|
||||
});
|
||||
manager.connect(workspace.currentDesktopChanged, () => {
|
||||
world.do(() => {}); // re-arrange desktop
|
||||
});
|
||||
|
||||
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfDesktops: number) => {
|
||||
manager.connect(workspace.currentActivityChanged, () => {
|
||||
world.do(() => {}); // re-arrange desktop
|
||||
});
|
||||
|
||||
manager.connect(workspace.numberDesktopsChanged, () => {
|
||||
world.updateDesktops();
|
||||
});
|
||||
|
||||
|
||||
210
src/world/ClientManager.ts
Normal file
210
src/world/ClientManager.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
class ClientManager {
|
||||
private readonly world: World;
|
||||
private readonly config: ClientManager.Config;
|
||||
private readonly desktopManager: DesktopManager;
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly clientMap: Map<KwinClient, ClientWrapper>;
|
||||
private lastFocusedClient: KwinClient|null;
|
||||
private readonly windowRuleEnforcer: WindowRuleEnforcer;
|
||||
|
||||
constructor(config: Config, world: World, desktopManager: DesktopManager, pinManager: PinManager) {
|
||||
this.world = world;
|
||||
this.config = { keepAbove: config.floatingKeepAbove };
|
||||
this.desktopManager = desktopManager;
|
||||
this.pinManager = pinManager;
|
||||
this.clientMap = new Map();
|
||||
this.lastFocusedClient = null;
|
||||
|
||||
let parsedWindowRules: WindowRule[] = [];
|
||||
try {
|
||||
parsedWindowRules = JSON.parse(config.windowRules);
|
||||
} catch (error: any) {
|
||||
notificationInvalidWindowRules.sendEvent();
|
||||
log("failed to parse windowRules:", error);
|
||||
}
|
||||
this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules);
|
||||
}
|
||||
|
||||
public addClient(kwinClient: KwinClient) {
|
||||
console.assert(!this.hasClient(kwinClient));
|
||||
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
|
||||
|
||||
let constructState: (client: ClientWrapper) => ClientState.State;
|
||||
if (kwinClient.dock) {
|
||||
constructState = () => new ClientState.Docked(this.world, kwinClient);
|
||||
} else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
|
||||
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
|
||||
} else {
|
||||
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
|
||||
}
|
||||
|
||||
const client = new ClientWrapper(
|
||||
kwinClient,
|
||||
constructState,
|
||||
this.findTransientFor(kwinClient),
|
||||
this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient),
|
||||
);
|
||||
this.clientMap.set(kwinClient, client);
|
||||
}
|
||||
|
||||
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
|
||||
console.assert(this.hasClient(kwinClient));
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
|
||||
this.clientMap.delete(kwinClient);
|
||||
}
|
||||
|
||||
private findTransientFor(kwinClient: KwinClient) {
|
||||
if (!kwinClient.transient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transientFor = this.clientMap.get(kwinClient.transientFor);
|
||||
if (transientFor === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transientFor;
|
||||
}
|
||||
|
||||
public minimizeClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||
client.stateManager.setState(() => new ClientState.TiledMinimized(), kwinClient === this.lastFocusedClient);
|
||||
}
|
||||
}
|
||||
|
||||
public unminimizeClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.TiledMinimized) {
|
||||
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop !== undefined) {
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
|
||||
} else {
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public tileClient(kwinClient: KwinClient, grid: Grid) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
|
||||
}
|
||||
|
||||
public untileClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
|
||||
}
|
||||
}
|
||||
|
||||
public pinClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
|
||||
this.pinManager.addClient(kwinClient);
|
||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public unpinClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
|
||||
this.pinManager.removeClient(kwinClient);
|
||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public toggleFloatingClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientState = client.stateManager.getState();
|
||||
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
|
||||
Clients.makeTileable(kwinClient);
|
||||
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
|
||||
} else if (clientState instanceof ClientState.Tiled) {
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
|
||||
}
|
||||
}
|
||||
|
||||
public hasClient(kwinClient: KwinClient) {
|
||||
return this.clientMap.has(kwinClient);
|
||||
}
|
||||
|
||||
public onClientFocused(kwinClient: KwinClient) {
|
||||
this.lastFocusedClient = kwinClient;
|
||||
const window = this.findTiledWindow(kwinClient, true);
|
||||
if (window !== null) {
|
||||
window.onFocused();
|
||||
}
|
||||
}
|
||||
|
||||
public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.findTiledWindowOfClient(client, followTransient);
|
||||
}
|
||||
|
||||
private findTiledWindowOfClient(client: ClientWrapper, followTransient: boolean): Window|null {
|
||||
const clientState = client.stateManager.getState();
|
||||
if (clientState instanceof ClientState.Tiled) {
|
||||
return clientState.window;
|
||||
} else if (followTransient && client.transientFor !== null) {
|
||||
return this.findTiledWindowOfClient(client.transientFor, true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private removeAllClients() {
|
||||
for (const kwinClient of Array.from(this.clientMap.keys())) {
|
||||
this.removeClient(kwinClient, false);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.removeAllClients();
|
||||
}
|
||||
}
|
||||
|
||||
namespace ClientManager {
|
||||
export type Config = {
|
||||
keepAbove: boolean,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
class ClientStateDocked {
|
||||
private readonly world: World;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, kwinClient: AbstractClient) {
|
||||
this.world = world;
|
||||
this.signalManager = ClientStateDocked.initSignalManager(world, kwinClient);
|
||||
world.onScreenResized();
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
this.world.onScreenResized();
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, kwinClient: AbstractClient) {
|
||||
const manager = new SignalManager();
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
|
||||
world.onScreenResized();
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
class ClientStateManager {
|
||||
private state: ClientState;
|
||||
|
||||
constructor(initialState: ClientState) {
|
||||
this.state = initialState;
|
||||
}
|
||||
|
||||
setState(newState: ClientState, passFocus: boolean) {
|
||||
this.state.destroy(passFocus);
|
||||
this.state = newState;
|
||||
}
|
||||
|
||||
getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
this.state.destroy(passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
type ClientState = ClientStateTiled | ClientStateTiledMinimized | ClientStateFloating | ClientStateDocked;
|
||||
|
||||
class ClientStateTiledMinimized {
|
||||
destroy(passFocus: boolean) {}
|
||||
}
|
||||
|
||||
class ClientStateFloating {
|
||||
destroy(passFocus: boolean) {}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
class ClientStateTiled {
|
||||
readonly window: Window;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, client: ClientWrapper) {
|
||||
client.prepareForTiling();
|
||||
|
||||
const grid = world.getClientGrid(client.kwinClient);
|
||||
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
|
||||
const window = new Window(client, column);
|
||||
grid.arrange();
|
||||
|
||||
this.window = window;
|
||||
this.signalManager = ClientStateTiled.initSignalManager(world, window);
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
|
||||
const window = this.window;
|
||||
const grid = window.column.grid;
|
||||
const clientWrapper = window.client;
|
||||
window.destroy(passFocus);
|
||||
grid.arrange();
|
||||
|
||||
clientWrapper.prepareForFloating(grid.clientArea);
|
||||
}
|
||||
|
||||
static initSignalManager(world: World, window: Window) {
|
||||
const client = window.client;
|
||||
const kwinClient = client.kwinClient;
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(kwinClient.desktopChanged, () => {
|
||||
if (kwinClient.desktop === -1) {
|
||||
// windows on all desktops are not supported
|
||||
world.untileClient(kwinClient);
|
||||
return;
|
||||
}
|
||||
ClientStateTiled.moveWindowToCorrectGrid(world, window);
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.activitiesChanged, (kwinClient: AbstractClient) => {
|
||||
if (kwinClient.activities.length !== 1) {
|
||||
// windows on multiple activities are not supported
|
||||
world.untileClient(kwinClient);
|
||||
return;
|
||||
}
|
||||
ClientStateTiled.moveWindowToCorrectGrid(world, window);
|
||||
})
|
||||
|
||||
let lastResize = false;
|
||||
manager.connect(kwinClient.moveResizedChanged, () => {
|
||||
if (kwinClient.move) {
|
||||
world.untileClient(kwinClient);
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = window.column.grid;
|
||||
const resize = kwinClient.resize;
|
||||
if (!lastResize && resize) {
|
||||
grid.onUserResizeStarted();
|
||||
}
|
||||
if (lastResize && !resize) {
|
||||
grid.onUserResizeFinished();
|
||||
}
|
||||
lastResize = resize;
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
|
||||
console.assert(!kwinClient.move, "moved clients are removed in kwinClient.moveResizedChanged");
|
||||
const grid = window.column.grid;
|
||||
if (kwinClient.resize) {
|
||||
window.onUserResize(oldGeometry);
|
||||
grid.arrange();
|
||||
} else {
|
||||
const maximized = rectEqual(kwinClient.frameGeometry, grid.clientArea);
|
||||
if (!client.isManipulatingGeometry() && !kwinClient.fullScreen && !maximized) {
|
||||
window.onProgrammaticResize(oldGeometry);
|
||||
grid.arrange();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
static moveWindowToCorrectGrid(world: World, window: Window) {
|
||||
const kwinClient = window.client.kwinClient;
|
||||
|
||||
const oldGrid = window.column.grid;
|
||||
const newGrid = world.getClientGrid(kwinClient);
|
||||
if (oldGrid === newGrid) {
|
||||
// window already on the correct grid
|
||||
return;
|
||||
}
|
||||
|
||||
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
|
||||
window.moveToColumn(newColumn);
|
||||
oldGrid.arrange();
|
||||
newGrid.arrange();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,111 @@
|
||||
class ClientWrapper {
|
||||
public readonly kwinClient: AbstractClient;
|
||||
public readonly stateManager: ClientStateManager;
|
||||
public readonly kwinClient: KwinClient;
|
||||
public readonly stateManager: ClientState.Manager;
|
||||
public transientFor: ClientWrapper | null;
|
||||
private readonly transients: ClientWrapper[];
|
||||
private readonly signalManager: SignalManager;
|
||||
private readonly rulesSignalManager: SignalManager | null;
|
||||
public preferredWidth: number;
|
||||
private readonly manipulatingGeometry: Doer;
|
||||
private lastPlacement: QmlRect | null; // workaround for issue #19
|
||||
|
||||
constructor(
|
||||
kwinClient: AbstractClient,
|
||||
initialState: ClientState,
|
||||
kwinClient: KwinClient,
|
||||
constructInitialState: (client: ClientWrapper) => ClientState.State,
|
||||
transientFor: ClientWrapper | null,
|
||||
rulesSignalManager: SignalManager | null,
|
||||
) {
|
||||
this.kwinClient = kwinClient;
|
||||
this.stateManager = new ClientStateManager(initialState);
|
||||
this.transientFor = transientFor;
|
||||
this.transients = [];
|
||||
if (transientFor !== null) {
|
||||
transientFor.addTransient(this);
|
||||
}
|
||||
this.signalManager = ClientWrapper.initSignalManager(this);
|
||||
this.rulesSignalManager = rulesSignalManager;
|
||||
this.preferredWidth = kwinClient.frameGeometry.width;
|
||||
this.manipulatingGeometry = new Doer();
|
||||
this.lastPlacement = null;
|
||||
this.stateManager = new ClientState.Manager(constructInitialState(this));
|
||||
}
|
||||
|
||||
place(x: number, y: number, width: number, height: number) {
|
||||
public place(x: number, y: number, width: number, height: number) {
|
||||
this.manipulatingGeometry.do(() => {
|
||||
if (this.kwinClient.resize) {
|
||||
// window is being manually resized, prevent fighting with the user
|
||||
return;
|
||||
}
|
||||
this.kwinClient.frameGeometry = Qt.rect(x, y, width, height);
|
||||
const clientWrapper = this; // workaround for bug in Qt5's JS engine
|
||||
clientWrapper.lastPlacement = Qt.rect(x, y, width, height);
|
||||
clientWrapper.kwinClient.frameGeometry = clientWrapper.lastPlacement;
|
||||
if (clientWrapper.kwinClient.frameGeometry !== clientWrapper.lastPlacement) {
|
||||
// frameGeometry assignment failed. This sometimes happens on Wayland
|
||||
// when a window is off-screen, effectively making it stuck there.
|
||||
clientWrapper.kwinClient.frameGeometry.x = x; // This makes it unstuck.
|
||||
clientWrapper.kwinClient.frameGeometry = clientWrapper.lastPlacement;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private moveTransient(dx: number, dy: number) {
|
||||
// TODO: prevent moving off the grid
|
||||
if (this.stateManager.getState() instanceof ClientStateFloating) {
|
||||
const frame = this.kwinClient.frameGeometry;
|
||||
this.kwinClient.frameGeometry = Qt.rect(
|
||||
frame.x + dx,
|
||||
frame.y + dy,
|
||||
frame.width,
|
||||
frame.height,
|
||||
);
|
||||
private moveTransient(dx: number, dy: number, desktopNumber: number) {
|
||||
if (this.stateManager.getState() instanceof ClientState.Floating) {
|
||||
if (this.kwinClient.desktop === desktopNumber) {
|
||||
const frame = this.kwinClient.frameGeometry;
|
||||
this.kwinClient.frameGeometry = Qt.rect(
|
||||
frame.x + dx,
|
||||
frame.y + dy,
|
||||
frame.width,
|
||||
frame.height,
|
||||
);
|
||||
}
|
||||
|
||||
for (const transient of this.transients) {
|
||||
transient.moveTransient(dx, dy);
|
||||
transient.moveTransient(dx, dy, desktopNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
public moveTransients(dx: number, dy: number) {
|
||||
for (const transient of this.transients) {
|
||||
transient.moveTransient(dx, dy, this.kwinClient.desktop);
|
||||
}
|
||||
}
|
||||
|
||||
public focus() {
|
||||
workspace.activeClient = this.kwinClient;
|
||||
}
|
||||
|
||||
isFocused() {
|
||||
public isFocused() {
|
||||
return workspace.activeClient === this.kwinClient;
|
||||
}
|
||||
|
||||
setMaximize(horizontally: boolean, vertically: boolean) {
|
||||
public setMaximize(horizontally: boolean, vertically: boolean) {
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.setMaximize(vertically, horizontally);
|
||||
});
|
||||
}
|
||||
|
||||
setFullScreen(fullScreen: boolean) {
|
||||
public setFullScreen(fullScreen: boolean) {
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.fullScreen = fullScreen;
|
||||
});
|
||||
}
|
||||
|
||||
setShade(shade: boolean) {
|
||||
public setShade(shade: boolean) {
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.shade = shade;
|
||||
});
|
||||
}
|
||||
|
||||
isShaded() {
|
||||
public isShaded() {
|
||||
return this.kwinClient.shade;
|
||||
}
|
||||
|
||||
isManipulatingGeometry() {
|
||||
public isManipulatingGeometry(newGeometry: QmlRect | null) {
|
||||
if (newGeometry !== null && newGeometry === this.lastPlacement) {
|
||||
return true;
|
||||
}
|
||||
return this.manipulatingGeometry.isDoing();
|
||||
}
|
||||
|
||||
prepareForTiling() {
|
||||
this.kwinClient.keepBelow = true;
|
||||
this.setFullScreen(false);
|
||||
this.setMaximize(false, false);
|
||||
}
|
||||
|
||||
prepareForFloating(screenSize: QRect) {
|
||||
this.kwinClient.keepBelow = false;
|
||||
this.setShade(false);
|
||||
this.setFullScreen(false);
|
||||
this.setMaximize(false, false);
|
||||
|
||||
const clientRect = this.kwinClient.frameGeometry;
|
||||
const width = this.preferredWidth;
|
||||
this.place(
|
||||
clamp(clientRect.x, screenSize.left, screenSize.right - width),
|
||||
clientRect.y,
|
||||
width,
|
||||
Math.min(clientRect.height, Math.round(screenSize.height / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
private addTransient(transient: ClientWrapper) {
|
||||
this.transients.push(transient);
|
||||
}
|
||||
@@ -119,9 +115,29 @@ class ClientWrapper {
|
||||
this.transients.splice(i, 1);
|
||||
}
|
||||
|
||||
destroy(passFocus: boolean) {
|
||||
public ensureTransientsVisible(screenSize: QmlRect) {
|
||||
for (const transient of this.transients) {
|
||||
if (transient.stateManager.getState() instanceof ClientState.Floating) {
|
||||
transient.ensureVisible(screenSize);
|
||||
transient.ensureTransientsVisible(screenSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ensureVisible(screenSize: QmlRect) {
|
||||
if (this.kwinClient.desktop !== workspace.currentDesktop) {
|
||||
return;
|
||||
}
|
||||
const frame = this.kwinClient.frameGeometry;
|
||||
if (frame.left < screenSize.left) {
|
||||
frame.x = screenSize.left;
|
||||
} else if (frame.right > screenSize.right) {
|
||||
frame.x = screenSize.right - frame.width;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.stateManager.destroy(passFocus);
|
||||
this.signalManager.destroy();
|
||||
if (this.rulesSignalManager !== null) {
|
||||
this.rulesSignalManager.destroy();
|
||||
}
|
||||
@@ -132,23 +148,4 @@ class ClientWrapper {
|
||||
transient.transientFor = null;
|
||||
}
|
||||
}
|
||||
|
||||
static initSignalManager(client: ClientWrapper) {
|
||||
const manager = new SignalManager();
|
||||
manager.connect(client.kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
|
||||
if (client.stateManager.getState() instanceof ClientStateTiled) {
|
||||
const newGeometry = client.kwinClient.frameGeometry;
|
||||
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
|
||||
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
|
||||
const newCenterX = newGeometry.x + newGeometry.width/2;
|
||||
const newCenterY = newGeometry.y + newGeometry.height/2;
|
||||
const dx = Math.round(newCenterX - oldCenterX);
|
||||
const dy = Math.round(newCenterY - oldCenterY);
|
||||
for (const transient of client.transients) {
|
||||
transient.moveTransient(dx, dy);
|
||||
}
|
||||
}
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
||||
35
src/world/Clients.ts
Normal file
35
src/world/Clients.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace Clients {
|
||||
export function canTileEver(kwinClient: KwinClient) {
|
||||
return kwinClient.resizeable;
|
||||
}
|
||||
|
||||
export function canTileNow(kwinClient: KwinClient) {
|
||||
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
|
||||
}
|
||||
|
||||
export function makeTileable(kwinClient: KwinClient) {
|
||||
if (kwinClient.minimized) {
|
||||
kwinClient.minimized = false;
|
||||
}
|
||||
if (kwinClient.desktop <= 0) {
|
||||
kwinClient.desktop = workspace.currentDesktop;
|
||||
}
|
||||
if (kwinClient.activities.length !== 1) {
|
||||
kwinClient.activities = [workspace.currentActivity];
|
||||
}
|
||||
}
|
||||
|
||||
export function isMaximizedGeometry(kwinClient: KwinClient) {
|
||||
const maximizeArea = workspace.clientArea(ClientAreaOption.MaximizeArea, kwinClient.screen, kwinClient.desktop);
|
||||
return kwinClient.frameGeometry === maximizeArea;
|
||||
}
|
||||
|
||||
export function isFullScreenGeometry(kwinClient: KwinClient) {
|
||||
const fullScreenArea = workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.screen, kwinClient.desktop);
|
||||
return kwinClient.frameGeometry === fullScreenArea;
|
||||
}
|
||||
|
||||
export function isOnVirtualDesktop(kwinClient: KwinClient, desktopNumber: number) {
|
||||
return kwinClient.desktop === desktopNumber || kwinClient.desktop === -1;
|
||||
}
|
||||
}
|
||||
149
src/world/DesktopManager.ts
Normal file
149
src/world/DesktopManager.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
class DesktopManager {
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly config: Desktop.Config;
|
||||
public readonly layoutConfig: LayoutConfig;
|
||||
private readonly desktopsPerActivity: Map<string, Desktop[]>;
|
||||
private nVirtualDesktops: number;
|
||||
|
||||
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) {
|
||||
this.pinManager = pinManager;
|
||||
this.config = config;
|
||||
this.layoutConfig = layoutConfig;
|
||||
this.desktopsPerActivity = new Map();
|
||||
this.nVirtualDesktops = 0;
|
||||
this.update()
|
||||
this.addActivity(currentActivity);
|
||||
}
|
||||
|
||||
public update() {
|
||||
this.setNVirtualDesktops(workspace.desktops);
|
||||
}
|
||||
|
||||
public getDesktop(activity: string, desktopNumber: number) {
|
||||
const desktopIndex = desktopNumber - 1;
|
||||
if (desktopIndex >= this.nVirtualDesktops || desktopIndex < 0) {
|
||||
throw new Error("invalid desktop number: " + String(desktopNumber));
|
||||
}
|
||||
if (!this.desktopsPerActivity.has(activity)) {
|
||||
this.addActivity(activity);
|
||||
}
|
||||
return this.desktopsPerActivity.get(activity)![desktopIndex];
|
||||
}
|
||||
|
||||
public getCurrentDesktop() {
|
||||
return this.getDesktop(workspace.currentActivity, workspace.currentDesktop);
|
||||
}
|
||||
|
||||
public getDesktopInCurrentActivity(desktopNumber: number) {
|
||||
return this.getDesktop(workspace.currentActivity, desktopNumber);
|
||||
}
|
||||
|
||||
public getDesktopForClient(kwinClient: KwinClient) {
|
||||
if (kwinClient.activities.length !== 1 || kwinClient.desktop <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return this.getDesktop(kwinClient.activities[0], kwinClient.desktop);
|
||||
}
|
||||
|
||||
private setNVirtualDesktops(nVirtualDesktops: number) {
|
||||
if (nVirtualDesktops > this.nVirtualDesktops) {
|
||||
this.addDesktopsToActivities(nVirtualDesktops - this.nVirtualDesktops);
|
||||
} else if (nVirtualDesktops < this.nVirtualDesktops) {
|
||||
this.removeDesktopsFromActivities(this.nVirtualDesktops - nVirtualDesktops);
|
||||
}
|
||||
this.nVirtualDesktops = nVirtualDesktops;
|
||||
}
|
||||
|
||||
private addDesktopsToActivities(n: number) {
|
||||
for (const desktops of this.desktopsPerActivity.values()) {
|
||||
this.addDesktops(desktops, n);
|
||||
}
|
||||
}
|
||||
|
||||
private addDesktops(desktops: Desktop[], n: number) {
|
||||
const nStart = desktops.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const desktopNumber = nStart + i + 1;
|
||||
desktops.push(new Desktop(desktopNumber, this.pinManager, this.config, this.layoutConfig));
|
||||
}
|
||||
}
|
||||
|
||||
private removeDesktopsFromActivities(n: number) {
|
||||
const lastRemainingDesktopIndex = this.nVirtualDesktops - n - 1;
|
||||
for (const desktops of this.desktopsPerActivity.values()) {
|
||||
const targetDesktop = desktops[lastRemainingDesktopIndex];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const removedDesktop = desktops.pop()!;
|
||||
removedDesktop.grid.evacuate(targetDesktop.grid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addActivity(activity: string) {
|
||||
const desktops: Desktop[] = [];
|
||||
this.addDesktops(desktops, this.nVirtualDesktops);
|
||||
this.desktopsPerActivity.set(activity, desktops);
|
||||
}
|
||||
|
||||
private removeActivity(activity: string) {
|
||||
const removedDesktops = this.desktopsPerActivity.get(activity)!;
|
||||
this.desktopsPerActivity.delete(activity);
|
||||
const targetActivityDesktops = this.desktopsPerActivity.values().next().value;
|
||||
for (let i = 0; i < removedDesktops.length; i++) {
|
||||
removedDesktops[i].grid.evacuate(targetActivityDesktops[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const desktop of this.desktops()) {
|
||||
desktop.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public *desktops() {
|
||||
for (const desktops of this.desktopsPerActivity.values()) {
|
||||
for (const desktop of desktops) {
|
||||
yield desktop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public *getDesktopsForClient(kwinClient: KwinClient) {
|
||||
const activities = kwinClient.activities.length > 0 ? kwinClient.activities : this.desktopsPerActivity.keys();
|
||||
for (const activity of activities) {
|
||||
if (!this.desktopsPerActivity.has(activity)) {
|
||||
this.addActivity(activity);
|
||||
}
|
||||
const activityDesktops = this.desktopsPerActivity.get(activity)!;
|
||||
if (kwinClient.desktop === -1) {
|
||||
for (const desktop of activityDesktops) {
|
||||
yield desktop;
|
||||
}
|
||||
} else {
|
||||
const desktopIndex = kwinClient.desktop - 1;
|
||||
yield activityDesktops[desktopIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// empty array means all
|
||||
public *getDesktops(desktopNumbers: number[], inputActivities: string[]) {
|
||||
const activities = inputActivities.length > 0 ? inputActivities : this.desktopsPerActivity.keys();
|
||||
for (const activity of activities) {
|
||||
if (!this.desktopsPerActivity.has(activity)) {
|
||||
this.addActivity(activity);
|
||||
}
|
||||
const activityDesktops = this.desktopsPerActivity.get(activity)!;
|
||||
if (desktopNumbers.length === 0) {
|
||||
for (const desktop of activityDesktops) {
|
||||
yield desktop;
|
||||
}
|
||||
} else {
|
||||
for (const desktopNumber of desktopNumbers) {
|
||||
const desktopIndex = desktopNumber - 1;
|
||||
yield activityDesktops[desktopIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
class GridManager {
|
||||
private readonly world: World;
|
||||
private readonly gridsPerActivity: Map<string, Grid[]>;
|
||||
private nDesktops: number;
|
||||
|
||||
constructor(world: World, currentActivity: string, nDesktops: number) {
|
||||
this.world = world;
|
||||
this.gridsPerActivity = new Map();
|
||||
this.nDesktops = 0;
|
||||
this.setNDesktops(nDesktops);
|
||||
this.addActivity(currentActivity);
|
||||
}
|
||||
|
||||
get(activity: string, desktopNumber: number) {
|
||||
const desktopIndex = desktopNumber - 1;
|
||||
if (desktopIndex >= this.nDesktops || this.nDesktops < 0) {
|
||||
throw new Error("invalid desktop number: " + String(desktopNumber));
|
||||
}
|
||||
if (!this.gridsPerActivity.has(activity)) {
|
||||
this.addActivity(activity);
|
||||
}
|
||||
return this.gridsPerActivity.get(activity)![desktopIndex];
|
||||
}
|
||||
|
||||
setNDesktops(nDesktops: number) {
|
||||
if (nDesktops > this.nDesktops) {
|
||||
this.addDesktopsToActivities(nDesktops - this.nDesktops);
|
||||
} else if (nDesktops < this.nDesktops) {
|
||||
this.removeDesktopsFromActivities(this.nDesktops - nDesktops);
|
||||
}
|
||||
this.nDesktops = nDesktops;
|
||||
}
|
||||
|
||||
private addDesktopsToActivities(n: number) {
|
||||
for (const grids of this.gridsPerActivity.values()) {
|
||||
this.addDesktops(grids, n);
|
||||
}
|
||||
}
|
||||
|
||||
private addDesktops(grids: Grid[], n: number) {
|
||||
const nStart = grids.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const desktopNumber = nStart + i + 1;
|
||||
grids.push(new Grid(this.world, desktopNumber));
|
||||
}
|
||||
}
|
||||
|
||||
private removeDesktopsFromActivities(n: number) {
|
||||
const lastRemainingDesktopIndex = this.nDesktops - n - 1;
|
||||
for (const grids of this.gridsPerActivity.values()) {
|
||||
const targetGrid = grids[lastRemainingDesktopIndex];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const removedGrid = grids.pop()!;
|
||||
removedGrid.evacuate(targetGrid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addActivity(activity: string) {
|
||||
const grids: Grid[] = [];
|
||||
this.addDesktops(grids, this.nDesktops);
|
||||
this.gridsPerActivity.set(activity, grids);
|
||||
}
|
||||
|
||||
removeActivity(activity: string) {
|
||||
const removedGrids = this.gridsPerActivity.get(activity)!;
|
||||
this.gridsPerActivity.delete(activity);
|
||||
const targetActivityGrids = this.gridsPerActivity.values().next().value;
|
||||
for (let i = 0; i < removedGrids.length; i++) {
|
||||
removedGrids[i].evacuate(targetActivityGrids[i]);
|
||||
}
|
||||
}
|
||||
|
||||
*grids() {
|
||||
for (const grids of this.gridsPerActivity.values()) {
|
||||
for (const grid of grids) {
|
||||
yield grid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/world/PinManager.ts
Normal file
86
src/world/PinManager.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
class PinManager {
|
||||
private readonly pinnedClients: Set<KwinClient>;
|
||||
|
||||
constructor() {
|
||||
this.pinnedClients = new Set();
|
||||
}
|
||||
|
||||
public addClient(kwinClient: KwinClient) {
|
||||
this.pinnedClients.add(kwinClient);
|
||||
}
|
||||
|
||||
public removeClient(kwinClient: KwinClient) {
|
||||
this.pinnedClients.delete(kwinClient);
|
||||
}
|
||||
|
||||
public getAvailableSpace(desktopNumber: number, screen: QmlRect) {
|
||||
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
|
||||
let lots = [baseLot];
|
||||
for (const client of this.pinnedClients) {
|
||||
if (!Clients.isOnVirtualDesktop(client, desktopNumber)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newLots: PinManager.Lot[] = [];
|
||||
for (const lot of lots) {
|
||||
lot.split(newLots, client.frameGeometry);
|
||||
}
|
||||
lots = newLots;
|
||||
}
|
||||
|
||||
let largestLot = baseLot;
|
||||
let largestArea = 0;
|
||||
for (const lot of lots) {
|
||||
const area = lot.area();
|
||||
if (area > largestArea) {
|
||||
largestArea = area;
|
||||
largestLot = lot;
|
||||
}
|
||||
}
|
||||
return largestLot;
|
||||
}
|
||||
}
|
||||
|
||||
namespace PinManager {
|
||||
export class Lot {
|
||||
private static readonly minWidth = 200;
|
||||
private static readonly minHeight = 200;
|
||||
|
||||
constructor(
|
||||
public readonly top: number,
|
||||
public readonly bottom: number,
|
||||
public readonly left: number,
|
||||
public readonly right: number,
|
||||
) {}
|
||||
|
||||
public split(destLots: Lot[], obstacle: QmlRect) {
|
||||
if (!this.contains(obstacle)) {
|
||||
// don't split
|
||||
destLots.push(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (obstacle.top - this.top >= Lot.minHeight) {
|
||||
destLots.push(new Lot(this.top, obstacle.top, this.left, this.right));
|
||||
}
|
||||
if (this.bottom - obstacle.bottom >= Lot.minHeight) {
|
||||
destLots.push(new Lot(obstacle.bottom, this.bottom, this.left, this.right));
|
||||
}
|
||||
if (obstacle.left - this.left >= Lot.minWidth) {
|
||||
destLots.push(new Lot(this.top, this.bottom, this.left, obstacle.left));
|
||||
}
|
||||
if (this.right - obstacle.right >= Lot.minWidth) {
|
||||
destLots.push(new Lot(this.top, this.bottom, obstacle.right, this.right));
|
||||
}
|
||||
}
|
||||
|
||||
private contains(obstacle: QmlRect) {
|
||||
return obstacle.right > this.left && obstacle.left < this.right &&
|
||||
obstacle.bottom > this.top && obstacle.top < this.bottom;
|
||||
}
|
||||
|
||||
public area() {
|
||||
return (this.bottom - this.top) * (this.right - this.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,226 +1,109 @@
|
||||
class World {
|
||||
public readonly config: Config;
|
||||
private readonly gridManager: GridManager;
|
||||
private readonly clientMap: Map<AbstractClient, ClientWrapper>;
|
||||
private lastFocusedClient: AbstractClient|null;
|
||||
public readonly untileOnDrag: boolean;
|
||||
private readonly desktopManager: DesktopManager;
|
||||
public readonly clientManager: ClientManager;
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly workspaceSignalManager: SignalManager;
|
||||
private readonly windowRuleEnforcer: WindowRuleEnforcer;
|
||||
private readonly screenResizedDelayer: Delayer;
|
||||
|
||||
constructor(config: Config) {
|
||||
this.config = config;
|
||||
this.clientMap = new Map();
|
||||
this.lastFocusedClient = null;
|
||||
this.untileOnDrag = config.untileOnDrag;
|
||||
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
|
||||
|
||||
let parsedWindowRules: WindowRule[] = [];
|
||||
try {
|
||||
parsedWindowRules = JSON.parse(config.windowRules);
|
||||
} catch (error: any) {
|
||||
console.log("failed to parse windowRules:", error);
|
||||
}
|
||||
this.windowRuleEnforcer = new WindowRuleEnforcer(this, parsedWindowRules);
|
||||
|
||||
this.screenResizedDelayer = new Delayer(1000, () => {
|
||||
// this delay ensures that docks get taken into account by `workspace.clientArea`
|
||||
const gridManager = this.gridManager; // workaround for bug in Qt5's JS engine
|
||||
for (const grid of gridManager.grids()) {
|
||||
grid.arrange();
|
||||
// this delay ensures that docks are taken into account by `workspace.clientArea`
|
||||
const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine
|
||||
for (const desktop of desktopManager.desktops()) {
|
||||
desktop.onLayoutChanged();
|
||||
}
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.gridManager = new GridManager(this, workspace.currentActivity, workspace.desktops);
|
||||
this.addExistingClients();
|
||||
}
|
||||
this.pinManager = new PinManager();
|
||||
|
||||
updateDesktops() {
|
||||
this.gridManager.setNDesktops(workspace.desktops);
|
||||
const layoutConfig = {
|
||||
gapsInnerHorizontal: config.gapsInnerHorizontal,
|
||||
gapsInnerVertical: config.gapsInnerVertical,
|
||||
offScreenOpacity: config.offScreenOpacity / 100.0,
|
||||
stackColumnsByDefault: config.stackColumnsByDefault,
|
||||
resizeNeighborColumn: config.resizeNeighborColumn,
|
||||
reMaximize: config.reMaximize,
|
||||
skipSwitcher: config.skipSwitcher,
|
||||
tiledKeepBelow: config.tiledKeepBelow,
|
||||
maximizedKeepAbove: config.floatingKeepAbove,
|
||||
};
|
||||
|
||||
this.desktopManager = new DesktopManager(
|
||||
this.pinManager,
|
||||
{
|
||||
marginTop: config.gapsOuterTop,
|
||||
marginBottom: config.gapsOuterBottom,
|
||||
marginLeft: config.gapsOuterLeft,
|
||||
marginRight: config.gapsOuterRight,
|
||||
scroller: config.scrollingLazy ? new LazyScroller() :
|
||||
config.scrollingCentered ? new CenteredScroller() :
|
||||
config.scrollingGrouped ? new GroupedScroller() :
|
||||
console.assert(false),
|
||||
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
|
||||
},
|
||||
layoutConfig,
|
||||
workspace.currentActivity,
|
||||
);
|
||||
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
|
||||
this.addExistingClients();
|
||||
this.update();
|
||||
}
|
||||
|
||||
private addExistingClients() {
|
||||
const kwinClients = workspace.clientList();
|
||||
for (let i = 0; i < kwinClients.length; i++) {
|
||||
const kwinClient = kwinClients[i];
|
||||
this.addClient(kwinClient);
|
||||
this.clientManager.addClient(kwinClient);
|
||||
}
|
||||
}
|
||||
|
||||
getGrid(activity: string, desktopNumber: number) {
|
||||
console.assert(desktopNumber > 0 && desktopNumber <= workspace.desktops);
|
||||
return this.gridManager.get(activity, desktopNumber);
|
||||
public updateDesktops() {
|
||||
this.desktopManager.update();
|
||||
}
|
||||
|
||||
getGridInCurrentActivity(desktopNumber: number) {
|
||||
return this.getGrid(workspace.currentActivity, desktopNumber);
|
||||
private update() {
|
||||
this.desktopManager.getCurrentDesktop().arrange();
|
||||
}
|
||||
|
||||
getCurrentGrid() {
|
||||
return this.getGrid(workspace.currentActivity, workspace.currentDesktop);
|
||||
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
|
||||
f(this.clientManager, this.desktopManager);
|
||||
this.update();
|
||||
}
|
||||
|
||||
getClientGrid(kwinClient: AbstractClient) {
|
||||
console.assert(kwinClient.activities.length === 1);
|
||||
return this.getGrid(kwinClient.activities[0], kwinClient.desktop);
|
||||
}
|
||||
|
||||
addClient(kwinClient: AbstractClient) {
|
||||
const client = new ClientWrapper(
|
||||
kwinClient,
|
||||
new ClientStateFloating(),
|
||||
this.findTransientFor(kwinClient),
|
||||
this.windowRuleEnforcer.initClientSignalManager(this, kwinClient),
|
||||
);
|
||||
this.clientMap.set(kwinClient, client);
|
||||
|
||||
if (kwinClient.dock) {
|
||||
client.stateManager.setState(new ClientStateDocked(this, kwinClient), false);
|
||||
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
|
||||
client.stateManager.setState(new ClientStateTiled(this, client), false);
|
||||
}
|
||||
}
|
||||
|
||||
removeClient(kwinClient: AbstractClient, passFocus: boolean) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
public doIfTiled(
|
||||
kwinClient: KwinClient,
|
||||
followTransient: boolean,
|
||||
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
|
||||
) {
|
||||
const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
|
||||
if (window === null) {
|
||||
return;
|
||||
}
|
||||
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
|
||||
this.clientMap.delete(kwinClient);
|
||||
const column = window.column;
|
||||
const grid = column.grid;
|
||||
f(this.clientManager, this.desktopManager, window, column, grid);
|
||||
this.update();
|
||||
}
|
||||
|
||||
findTransientFor(kwinClient: AbstractClient) {
|
||||
if (!kwinClient.transient) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transientFor = this.clientMap.get(kwinClient.transientFor);
|
||||
if (transientFor === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return transientFor;
|
||||
}
|
||||
|
||||
minimizeClient(kwinClient: AbstractClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientStateTiled) {
|
||||
client.stateManager.setState(new ClientStateTiledMinimized(), kwinClient === this.lastFocusedClient);
|
||||
}
|
||||
}
|
||||
|
||||
unminimizeClient(kwinClient: AbstractClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientStateTiledMinimized) {
|
||||
client.stateManager.setState(new ClientStateTiled(this, client), false);
|
||||
}
|
||||
}
|
||||
|
||||
tileClient(kwinClient: AbstractClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientStateTiled) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(new ClientStateTiled(this, client), false);
|
||||
}
|
||||
|
||||
untileClient(kwinClient: AbstractClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientStateTiled) {
|
||||
client.stateManager.setState(new ClientStateFloating(), false);
|
||||
}
|
||||
}
|
||||
|
||||
toggleFloatingClient(kwinClient: AbstractClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clientState = client.stateManager.getState();
|
||||
if (clientState instanceof ClientStateFloating && canTileEver(kwinClient)) {
|
||||
makeTileable(kwinClient);
|
||||
client.stateManager.setState(new ClientStateTiled(this, client), false);
|
||||
} else if (clientState instanceof ClientStateTiled) {
|
||||
client.stateManager.setState(new ClientStateFloating(), false);
|
||||
}
|
||||
}
|
||||
|
||||
hasClient(kwinClient: AbstractClient) {
|
||||
return this.clientMap.has(kwinClient);
|
||||
}
|
||||
|
||||
onClientFocused(kwinClient: AbstractClient) {
|
||||
this.lastFocusedClient = kwinClient;
|
||||
}
|
||||
|
||||
private doIfTiledInner(client: ClientWrapper, followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
|
||||
const clientState = client.stateManager.getState();
|
||||
if (clientState instanceof ClientStateTiled) {
|
||||
const window = clientState.window;
|
||||
const column = window.column;
|
||||
const grid = column.grid;
|
||||
f(window, column, grid);
|
||||
} else if (followTransient && client.transientFor !== null) {
|
||||
this.doIfTiledInner(client.transientFor, true, f);
|
||||
}
|
||||
}
|
||||
|
||||
doIfTiled(kwinClient: AbstractClient, followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
this.doIfTiledInner(client, followTransient, f);
|
||||
}
|
||||
|
||||
doIfTiledFocused(followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
|
||||
public doIfTiledFocused(
|
||||
followTransient: boolean,
|
||||
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
|
||||
) {
|
||||
this.doIfTiled(workspace.activeClient, followTransient, f);
|
||||
}
|
||||
|
||||
getFocusedWindow() {
|
||||
const activeClient = workspace.activeClient;
|
||||
if (activeClient === null) {
|
||||
return null;
|
||||
}
|
||||
const client = this.clientMap.get(activeClient);
|
||||
if (client === undefined) {
|
||||
return null;
|
||||
}
|
||||
const clientState = client.stateManager.getState();
|
||||
if (clientState instanceof ClientStateTiled) {
|
||||
return clientState.window;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
removeAllClients() {
|
||||
for (const kwinClient of Array.from(this.clientMap.keys())) {
|
||||
this.removeClient(kwinClient, false);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
public destroy() {
|
||||
this.workspaceSignalManager.destroy();
|
||||
this.removeAllClients();
|
||||
for (const grid of this.gridManager.grids()) {
|
||||
grid.destroy();
|
||||
}
|
||||
this.clientManager.destroy();
|
||||
this.desktopManager.destroy();
|
||||
}
|
||||
|
||||
onScreenResized() {
|
||||
public onScreenResized() {
|
||||
this.screenResizedDelayer.run();
|
||||
}
|
||||
}
|
||||
|
||||
25
src/world/clientState/Docked.ts
Normal file
25
src/world/clientState/Docked.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace ClientState {
|
||||
export class Docked implements State {
|
||||
private readonly world: World;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, kwinClient: KwinClient) {
|
||||
this.world = world;
|
||||
this.signalManager = Docked.initSignalManager(world, kwinClient);
|
||||
world.onScreenResized();
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
this.world.onScreenResized();
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, kwinClient: KwinClient) {
|
||||
const manager = new SignalManager();
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => {
|
||||
world.onScreenResized();
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
}
|
||||
62
src/world/clientState/Floating.ts
Normal file
62
src/world/clientState/Floating.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
namespace ClientState {
|
||||
export class Floating implements State {
|
||||
private readonly client: ClientWrapper;
|
||||
private readonly config: ClientManager.Config;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
|
||||
this.client = client;
|
||||
this.config = config;
|
||||
if (config.keepAbove) {
|
||||
client.kwinClient.keepAbove = true;
|
||||
}
|
||||
if (limitHeight && client.kwinClient.tile === null) {
|
||||
Floating.limitHeight(client);
|
||||
}
|
||||
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
if (this.config.keepAbove) {
|
||||
this.client.kwinClient.keepAbove = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static limitHeight(client: ClientWrapper) {
|
||||
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, client.kwinClient.screen, client.kwinClient.desktop);
|
||||
const clientRect = client.kwinClient.frameGeometry;
|
||||
const width = client.preferredWidth;
|
||||
client.place(
|
||||
clientRect.x,
|
||||
clientRect.y,
|
||||
width,
|
||||
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
|
||||
);
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, kwinClient: KwinClient) {
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(kwinClient.tileChanged, () => {
|
||||
// on X11, this fires after `frameGeometryChanged`
|
||||
if (kwinClient.tile !== null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.pinClient(kwinClient);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, () => {
|
||||
// on Wayland, this fires after `tileChanged`
|
||||
if (kwinClient.tile !== null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.pinClient(kwinClient);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/world/clientState/Manager.ts
Normal file
26
src/world/clientState/Manager.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace ClientState {
|
||||
export class Manager {
|
||||
private state: State;
|
||||
|
||||
constructor(initialState: State) {
|
||||
this.state = initialState;
|
||||
}
|
||||
|
||||
public setState(constructNewState: () => State, passFocus: boolean) {
|
||||
this.state.destroy(passFocus);
|
||||
this.state = constructNewState();
|
||||
}
|
||||
|
||||
public getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.state.destroy(passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
export type State = {
|
||||
destroy(passFocus: boolean): void,
|
||||
};
|
||||
}
|
||||
87
src/world/clientState/Pinned.ts
Normal file
87
src/world/clientState/Pinned.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
namespace ClientState {
|
||||
export class Pinned implements State {
|
||||
private readonly kwinClient: KwinClient;
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly desktopManager: DesktopManager;
|
||||
private readonly config: ClientManager.Config;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, pinManager: PinManager, desktopManager: DesktopManager, kwinClient: KwinClient, config: ClientManager.Config) {
|
||||
this.kwinClient = kwinClient;
|
||||
this.pinManager = pinManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.config = config;
|
||||
if (config.keepAbove) {
|
||||
kwinClient.keepAbove = true;
|
||||
}
|
||||
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
if (this.config.keepAbove) {
|
||||
this.kwinClient.keepAbove = true;
|
||||
}
|
||||
this.pinManager.removeClient(this.kwinClient);
|
||||
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
|
||||
const manager = new SignalManager();
|
||||
let oldDesktopNumber = kwinClient.desktop;
|
||||
let oldActivities = kwinClient.activities;
|
||||
|
||||
manager.connect(kwinClient.tileChanged, () => {
|
||||
if (kwinClient.tile === null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.unpinClient(kwinClient);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => {
|
||||
if (kwinClient.tile === null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.unpinClient(kwinClient);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
world.do((clientManager, desktopManager) => {
|
||||
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.desktopChanged, () => {
|
||||
const changedDesktops = oldDesktopNumber === -1 || kwinClient.desktop === -1 ?
|
||||
[] :
|
||||
[oldDesktopNumber, kwinClient.desktop];
|
||||
world.do((clientManager, desktopManager) => {
|
||||
for (const desktop of desktopManager.getDesktops(changedDesktops, kwinClient.activities)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
});
|
||||
oldDesktopNumber = kwinClient.desktop;
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.activitiesChanged, () => {
|
||||
const desktops = kwinClient.desktop === -1 ? [] : [kwinClient.desktop];
|
||||
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
|
||||
[] :
|
||||
union(oldActivities, kwinClient.activities);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
for (const desktop of desktopManager.getDesktops(desktops, changedActivities)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
});
|
||||
oldActivities = kwinClient.activities;
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/world/clientState/Tiled.ts
Normal file
181
src/world/clientState/Tiled.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
namespace ClientState {
|
||||
export class Tiled implements State {
|
||||
public readonly window: Window;
|
||||
private readonly defaultState: Tiled.WindowState;
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, client: ClientWrapper, grid: Grid) {
|
||||
this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
|
||||
Tiled.prepareClientForTiling(client, grid.config);
|
||||
|
||||
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
|
||||
const window = new Window(client, column);
|
||||
|
||||
this.window = window;
|
||||
this.signalManager = Tiled.initSignalManager(world, window);
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
this.signalManager.destroy();
|
||||
|
||||
const window = this.window;
|
||||
const grid = window.column.grid;
|
||||
const client = window.client;
|
||||
window.destroy(passFocus);
|
||||
|
||||
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, window: Window) {
|
||||
const client = window.client;
|
||||
const kwinClient = client.kwinClient;
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(kwinClient.desktopChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop === undefined) {
|
||||
// windows on all desktops are not supported
|
||||
clientManager.untileClient(kwinClient);
|
||||
return;
|
||||
}
|
||||
Tiled.moveWindowToGrid(window, desktop.grid);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.activitiesChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop === undefined) {
|
||||
// windows on multiple activities are not supported
|
||||
clientManager.untileClient(kwinClient);
|
||||
return;
|
||||
}
|
||||
Tiled.moveWindowToGrid(window, desktop.grid);
|
||||
});
|
||||
})
|
||||
|
||||
let lastResize = false;
|
||||
manager.connect(kwinClient.moveResizedChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
if (kwinClient.move) {
|
||||
if (world.untileOnDrag) {
|
||||
clientManager.untileClient(kwinClient);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const grid = window.column.grid;
|
||||
const resize = kwinClient.resize;
|
||||
if (!lastResize && resize) {
|
||||
grid.onUserResizeStarted();
|
||||
}
|
||||
if (lastResize && !resize) {
|
||||
grid.onUserResizeFinished();
|
||||
}
|
||||
lastResize = resize;
|
||||
});
|
||||
});
|
||||
|
||||
let cursorChangedAfterResizeStart = false;
|
||||
manager.connect(kwinClient.moveResizeCursorChanged, () => {
|
||||
cursorChangedAfterResizeStart = true;
|
||||
});
|
||||
manager.connect(kwinClient.clientStartUserMovedResized, () => {
|
||||
cursorChangedAfterResizeStart = false;
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => {
|
||||
// on Wayland, this fires after `tileChanged`
|
||||
if (kwinClient.tile !== null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.pinClient(kwinClient);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newGeometry = client.kwinClient.frameGeometry;
|
||||
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
|
||||
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
|
||||
const newCenterX = newGeometry.x + newGeometry.width/2;
|
||||
const newCenterY = newGeometry.y + newGeometry.height/2;
|
||||
const dx = Math.round(newCenterX - oldCenterX);
|
||||
const dy = Math.round(newCenterY - oldCenterY);
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
client.moveTransients(dx, dy);
|
||||
}
|
||||
|
||||
if (kwinClient.resize) {
|
||||
world.do(() => window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart));
|
||||
} else if (
|
||||
!window.column.grid.isUserResizing() &&
|
||||
!client.isManipulatingGeometry(newGeometry) &&
|
||||
!Clients.isMaximizedGeometry(kwinClient) &&
|
||||
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
|
||||
) {
|
||||
world.do(() => window.onFrameGeometryChanged());
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.fullScreenChanged, () => {
|
||||
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.tileChanged, () => {
|
||||
// on X11, this fires after `frameGeometryChanged`
|
||||
if (kwinClient.tile !== null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.pinClient(kwinClient);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
private static moveWindowToGrid(window: Window, grid: Grid) {
|
||||
if (grid === window.column.grid) {
|
||||
// window already on the given grid
|
||||
return;
|
||||
}
|
||||
|
||||
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
|
||||
window.moveToColumn(newColumn);
|
||||
}
|
||||
|
||||
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
|
||||
if (config.skipSwitcher) {
|
||||
client.kwinClient.skipSwitcher = true;
|
||||
}
|
||||
if (config.tiledKeepBelow) {
|
||||
client.kwinClient.keepBelow = true;
|
||||
}
|
||||
client.setFullScreen(false);
|
||||
if (client.kwinClient.tile !== null) {
|
||||
client.setMaximize(false, true); // disable quick tile mode
|
||||
}
|
||||
client.setMaximize(false, false);
|
||||
}
|
||||
|
||||
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, defaultState: Tiled.WindowState, screenSize: QmlRect) {
|
||||
if (config.skipSwitcher) {
|
||||
client.kwinClient.skipSwitcher = defaultState.skipSwitcher;
|
||||
}
|
||||
if (config.tiledKeepBelow) {
|
||||
client.kwinClient.keepBelow = false;
|
||||
}
|
||||
client.setShade(false);
|
||||
client.setFullScreen(false);
|
||||
if (client.kwinClient.tile === null) {
|
||||
client.setMaximize(false, false);
|
||||
}
|
||||
client.ensureVisible(screenSize);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Tiled {
|
||||
export type WindowState = {
|
||||
skipSwitcher: boolean,
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/world/clientState/TiledMinimized.ts
Normal file
5
src/world/clientState/TiledMinimized.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace ClientState {
|
||||
export class TiledMinimized implements State {
|
||||
public destroy(passFocus: boolean) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user