Compare commits
437 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d4eab03b9 | ||
|
|
7070e59044 | ||
|
|
7f5745b2cf | ||
|
|
c7752bf20a | ||
|
|
8149100aac | ||
|
|
99bf71f0b9 | ||
|
|
2b882768d9 | ||
|
|
bb42e4d3ad | ||
|
|
c7effc8913 | ||
|
|
2c433867f3 | ||
|
|
e995555074 | ||
|
|
6a1e018df1 | ||
|
|
872a67e6e1 | ||
|
|
5a57ba76d8 | ||
|
|
55c6932338 | ||
|
|
b1d6193e48 | ||
|
|
456bbf30b4 | ||
|
|
24c1fa0a38 | ||
|
|
ac7566d2cf | ||
|
|
195f4e6d30 | ||
|
|
e8f2a50420 | ||
|
|
9910bc7041 | ||
|
|
1b592c5b4b | ||
|
|
75384d9fb4 | ||
|
|
dba92d3826 | ||
|
|
dbb95e0470 | ||
|
|
056149440d | ||
|
|
33be23c6dc | ||
|
|
e31669e499 | ||
|
|
caf2b5a146 | ||
|
|
b7f1876a84 | ||
|
|
f108c4a45e | ||
|
|
0aa5d8c3fa | ||
|
|
1674d14453 | ||
|
|
ca8b78ef04 | ||
|
|
877767cea3 | ||
|
|
f1a18b8276 | ||
|
|
8725bc84e0 | ||
|
|
8c23535e86 | ||
|
|
c0e7234fec | ||
|
|
03acbe1280 | ||
|
|
7ef0c60cb8 | ||
|
|
7e1517bbcb | ||
|
|
a23acd056b | ||
|
|
e908f7fe8a | ||
|
|
ba63c1d4e7 | ||
|
|
bf060cef17 | ||
|
|
0f21f94d02 | ||
|
|
6dd356dc53 | ||
|
|
c99cad96c3 | ||
|
|
b4fe71f91b | ||
|
|
099b9f5d6a | ||
|
|
92f6942eef | ||
|
|
9621c2a75b | ||
|
|
36bc1be8c0 | ||
|
|
68b659744c | ||
|
|
c715289282 | ||
|
|
465945429a | ||
|
|
1d7636508b | ||
|
|
47213a71f5 | ||
|
|
75a548977c | ||
|
|
d746b91a88 | ||
|
|
a0d9c49287 | ||
|
|
862cc445bd | ||
|
|
5019a5d702 | ||
|
|
36c7cab137 | ||
|
|
df3c1f4512 | ||
|
|
5f3eaf1eec | ||
|
|
4a680177f6 | ||
|
|
8d807c979b | ||
|
|
c8e37aeb87 | ||
|
|
ad0fe7472c | ||
|
|
a51e45667c | ||
|
|
6615fe6f93 | ||
|
|
6e69139b80 | ||
|
|
97430d5043 | ||
|
|
47f4bbd9b6 | ||
|
|
2d4ad73d16 | ||
|
|
bb4e4f8ebd | ||
|
|
0742975334 | ||
|
|
64457429d0 | ||
|
|
02154f2f5e | ||
|
|
0a2bb4f65d | ||
|
|
6f207e59c4 | ||
|
|
4c987b6c5b | ||
|
|
bca0158df9 | ||
|
|
9feeb0f23e | ||
|
|
0241846ea5 | ||
|
|
3bf3f16f49 | ||
|
|
782a6db56d | ||
|
|
93b6850ffd | ||
|
|
5f0c637d1a | ||
|
|
8829d0b291 | ||
|
|
d37b4bc5d1 | ||
|
|
ead29e5e69 | ||
|
|
ff75d931f6 | ||
|
|
d00d514d30 | ||
|
|
3b919909dc | ||
|
|
0004b6f921 | ||
|
|
24265c56f9 | ||
|
|
dcbc0a474d | ||
|
|
88f170f5c1 | ||
|
|
78ab48ee09 | ||
|
|
b2d81796f8 | ||
|
|
7d27331ce5 | ||
|
|
55e1037a7b | ||
|
|
7820c7d00e | ||
|
|
3d8ca0bc14 | ||
|
|
eaf68b87f9 | ||
|
|
b2dfad6042 | ||
|
|
054808cb38 | ||
|
|
97059fa4f7 | ||
|
|
5e7959c7f4 | ||
|
|
a9c3aa7eae | ||
|
|
3aec148f16 | ||
|
|
c60cfeb521 | ||
|
|
0bbf82d84d | ||
|
|
44c5c43c25 | ||
|
|
98b2d8b882 | ||
|
|
4e78d27620 | ||
|
|
32700d1193 | ||
|
|
817351ec44 | ||
|
|
c2dd832e5c | ||
|
|
2f78f9afb1 | ||
|
|
2cc716f59e | ||
|
|
685323546a | ||
|
|
c8e40185dc | ||
|
|
0b0cb77e9e | ||
|
|
ce4f810372 | ||
|
|
3ccc6dd870 | ||
|
|
92e1c2ffd4 | ||
|
|
3a1c911974 | ||
|
|
e197c3200e | ||
|
|
4fea81423a | ||
|
|
b459ba6d4e | ||
|
|
a16e2edb1e | ||
|
|
fb1047c8ba | ||
|
|
66bc14287d | ||
|
|
86ab4a20f4 | ||
|
|
f467d5a523 | ||
|
|
082d4571d8 | ||
|
|
aa061e586e | ||
|
|
ff5121f1c4 | ||
|
|
984edbec90 | ||
|
|
ea27ce4a03 | ||
|
|
ae0a4e142a | ||
|
|
09b7b44e2c | ||
|
|
fb88300dbe | ||
|
|
9dafb92f47 | ||
|
|
92144e8e83 | ||
|
|
9b54dd543e | ||
|
|
5145b04cb7 | ||
|
|
4372e13869 | ||
|
|
7694bb1f8d | ||
|
|
3615952bd7 | ||
|
|
cba6ecbb3d | ||
|
|
db55af462b | ||
|
|
32f384df75 | ||
|
|
25ac6d4ce8 | ||
|
|
194f774519 | ||
|
|
430f731804 | ||
|
|
2cdf308a72 | ||
|
|
acf4c5c6ae | ||
|
|
703ed2eb40 | ||
|
|
ec5e7002dc | ||
|
|
fe92b1aa13 | ||
|
|
d812fa706f | ||
|
|
0bb9ae9e8d | ||
|
|
315ea6b560 | ||
|
|
a4236534fc | ||
|
|
e95378c0dd | ||
|
|
884dc9977d | ||
|
|
f3cf45e5f3 | ||
|
|
5e7f191e95 | ||
|
|
ec14d5295f | ||
|
|
116372c954 | ||
|
|
7290a0741d | ||
|
|
5bbd0da172 | ||
|
|
fb415042d8 | ||
|
|
ff5ba6b455 | ||
|
|
ef0e840812 | ||
|
|
b0cb9eaba0 | ||
|
|
0eb624d6ff | ||
|
|
6e53bbad2f | ||
|
|
218ab65d61 | ||
|
|
e91c9c825e | ||
|
|
c2931c9fbe | ||
|
|
1c61f22353 | ||
|
|
3ff7688e42 | ||
|
|
f5ca5e71c0 | ||
|
|
3f222f0ef8 | ||
|
|
9a08841bb8 | ||
|
|
2081b9997c | ||
|
|
93fe2a3cfd | ||
|
|
9cee3f2808 | ||
|
|
16618e6ad3 | ||
|
|
90c27bc58b | ||
|
|
79055dcdf3 | ||
|
|
1e328c7b57 | ||
|
|
2be6a04fc4 | ||
|
|
9b21a720be | ||
|
|
fcfed28a0a | ||
|
|
7dce802b70 | ||
|
|
94393393f5 | ||
|
|
1ba128af05 | ||
|
|
8094b64ce7 | ||
|
|
335418e84b | ||
|
|
c675fc63d1 | ||
|
|
9d81262b18 | ||
|
|
5d36f02631 | ||
|
|
977facbed7 | ||
|
|
278a2d9c7b | ||
|
|
8076635719 | ||
|
|
c8f0ed87a4 | ||
|
|
b3f581c386 | ||
|
|
279333dd1d | ||
|
|
dac1d488b7 | ||
|
|
df587fc37b | ||
|
|
987a4c852f | ||
|
|
ebff074a4c | ||
|
|
ef4c509e75 | ||
|
|
d0c1438724 | ||
|
|
d926be2e12 | ||
|
|
b18516646d | ||
|
|
b0405858c5 | ||
|
|
0cb3513532 | ||
|
|
0b2d876074 | ||
|
|
cb1b70d4f0 | ||
|
|
da6b983e4a | ||
|
|
6181057fa8 | ||
|
|
abc8671c6f | ||
|
|
36bf942266 | ||
|
|
d239ac24b3 | ||
|
|
79571146be | ||
|
|
7273196e0c | ||
|
|
212ade5ab6 | ||
|
|
239f8d95cb | ||
|
|
e430a20638 | ||
|
|
0beaebd874 | ||
|
|
6100090db3 | ||
|
|
8d07958962 | ||
|
|
80bafee5ed | ||
|
|
38e72a9504 | ||
|
|
d0c4cee63d | ||
|
|
494192179b | ||
|
|
0dcba7cbc0 | ||
|
|
8312364202 | ||
|
|
6d8dfad4e7 | ||
|
|
0d970a8bec | ||
|
|
12e8d71ef3 | ||
|
|
407b24df08 | ||
|
|
4ecced369a | ||
|
|
e3979d94f7 | ||
|
|
b6a5080d5d | ||
|
|
e974f0ebbd | ||
|
|
23e13436d8 | ||
|
|
aaef587c00 | ||
|
|
8f4c20138e | ||
|
|
a754da9b78 | ||
|
|
b94d3e2304 | ||
|
|
11e1458180 | ||
|
|
e7e68628dd | ||
|
|
edac1a679c | ||
|
|
f711248619 | ||
|
|
502f4bee26 | ||
|
|
35d2096811 | ||
|
|
f682c160db | ||
|
|
ef9cb01755 | ||
|
|
d51b9caec5 | ||
|
|
62bc96f205 | ||
|
|
35802eead7 | ||
|
|
ba886fe5f6 | ||
|
|
30e4e3e273 | ||
|
|
6bcb30b0bd | ||
|
|
747a90f5d7 | ||
|
|
da899171c4 | ||
|
|
109238e645 | ||
|
|
7d1ebcf126 | ||
|
|
07ce0cceb5 | ||
|
|
c7aaa66c5c | ||
|
|
5c77f4f276 | ||
|
|
80608e721f | ||
|
|
54b07ebe56 | ||
|
|
aeba236720 | ||
|
|
1984442ed3 | ||
|
|
2a4c5eac3b | ||
|
|
266beb85ae | ||
|
|
5877e466da | ||
|
|
9a60f94eda | ||
|
|
0f7092a5b6 | ||
|
|
0b3608a530 | ||
|
|
aaeac977e6 | ||
|
|
adc78f11bc | ||
|
|
f1206b18b1 | ||
|
|
c6066c354d | ||
|
|
914202f091 | ||
|
|
b85c86e7db | ||
|
|
fdb4b88333 | ||
|
|
4e3d924366 | ||
|
|
3d3e8cff17 | ||
|
|
a79229da75 | ||
|
|
53d04c1d33 | ||
|
|
a18ff61d9e | ||
|
|
99ffad9223 | ||
|
|
e776df509b | ||
|
|
63de2d4cae | ||
|
|
85d361f16f | ||
|
|
f3b75807be | ||
|
|
88bce26456 | ||
|
|
3a75ddab0f | ||
|
|
ee0aa93308 | ||
|
|
33ca138420 | ||
|
|
79f4aaeef8 | ||
|
|
0aca6e1146 | ||
|
|
28e54434aa | ||
|
|
ee14509228 | ||
|
|
1596edc43f | ||
|
|
b897ab5b9f | ||
|
|
5e6dad8459 | ||
|
|
beeba74442 | ||
|
|
3e14440180 | ||
|
|
36836ad258 | ||
|
|
bebc009cc6 | ||
|
|
a4ba8516dc | ||
|
|
675a70d907 | ||
|
|
3ba66f1c89 | ||
|
|
4556198b2e | ||
|
|
7b8de5955d | ||
|
|
68a687b7d4 | ||
|
|
20a3ece4b5 | ||
|
|
3cad8102ee | ||
|
|
7fd45eed8f | ||
|
|
7299341608 | ||
|
|
842ec1ac63 | ||
|
|
0523465b84 | ||
|
|
c7cfa261b9 | ||
|
|
56955e4df3 | ||
|
|
bb308cfbfb | ||
|
|
6c00245943 | ||
|
|
2efdbe5a7b | ||
|
|
092cbf3ff1 | ||
|
|
f9ae299ce8 | ||
|
|
695f5edf6a | ||
|
|
9b80b535a1 | ||
|
|
752df86db5 | ||
|
|
f05eefe19b | ||
|
|
f550285778 | ||
|
|
5247a6a0d3 | ||
|
|
2b114a63dc | ||
|
|
63e4015f3a | ||
|
|
02db31266b | ||
|
|
67d4d89700 | ||
|
|
755cf90b1a | ||
|
|
e6a01217a5 | ||
|
|
21d7bbd6c4 | ||
|
|
605215acdc | ||
|
|
4b6808dba1 | ||
|
|
f9749c6f56 | ||
|
|
9b40b2f777 | ||
|
|
33470b4d7b | ||
|
|
8947719621 | ||
|
|
4bf4f8e8a1 | ||
|
|
080de7cf97 | ||
|
|
c29902dc15 | ||
|
|
1736b0a398 | ||
|
|
a1c44647ca | ||
|
|
0ea75d6348 | ||
|
|
12901e45ce | ||
|
|
29b4ccd1dd | ||
|
|
7b547bc5b8 | ||
|
|
78a127111b | ||
|
|
333b7601b2 | ||
|
|
1927ae445d | ||
|
|
1f563dae01 | ||
|
|
6b82eedbfe | ||
|
|
b479735130 | ||
|
|
c8f022d66f | ||
|
|
7f71750a8e | ||
|
|
13ebf24732 | ||
|
|
ec6b3247b7 | ||
|
|
50681d3a07 | ||
|
|
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 |
19
.github/ISSUE_TEMPLATE/1-compatibility-issue.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/1-compatibility-issue.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
name: Compatibility issue
|
||||
about: Report an issue with a specific application or window
|
||||
title: "[Compatibility]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Karousel version:
|
||||
Plasma version:
|
||||
X11 / Wayland:
|
||||
|
||||
Window class:
|
||||
Window caption (title):
|
||||
Window type:
|
||||
(Get this info [here](https://github.com/peterfajdiga/karousel/wiki/Getting-window-info))
|
||||
|
||||
Description:
|
||||
14
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/2-bug-report.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Generic bug report
|
||||
about: Report a bug
|
||||
title: "[Bug]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Karousel version:
|
||||
Plasma version:
|
||||
X11 / Wayland:
|
||||
|
||||
Description:
|
||||
22
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Request a feature
|
||||
title: "[Feature]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
These sections are just guidelines, feel free to remove them.
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
/package/contents/code/main.js
|
||||
/package/contents/config/main.xml
|
||||
/karousel.tar.gz
|
||||
/karousel*.tar.gz
|
||||
run-ts-tmp.js
|
||||
|
||||
/node_modules
|
||||
/.idea
|
||||
|
||||
48
Makefile
48
Makefile
@@ -1,31 +1,43 @@
|
||||
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
|
||||
CHECKS := true
|
||||
|
||||
.PHONY: *
|
||||
|
||||
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
|
||||
|
||||
config:
|
||||
build: lint tests
|
||||
tsc -p ./src/main --outFile ./package/contents/code/main.js
|
||||
mkdir -p ./package/contents/config
|
||||
tsc ${TSC_SCRIPT_FLAGS} ./src/config/definition.ts ./generators/config/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
|
||||
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
|
||||
|
||||
build:
|
||||
tsc --outFile ./package/contents/code/main.js
|
||||
npm-install:
|
||||
npm install
|
||||
|
||||
install: build config
|
||||
kpackagetool5 --type=KWin/Script -i ./package || kpackagetool5 --type=KWin/Script -u ./package
|
||||
lint: npm-install
|
||||
ifeq (${CHECKS}, true)
|
||||
npx eslint ./src
|
||||
endif
|
||||
|
||||
lint-fix: npm-install
|
||||
npx eslint ./src --fix
|
||||
|
||||
tests:
|
||||
ifeq (${CHECKS}, true)
|
||||
./run-ts.sh ./src/tests
|
||||
endif
|
||||
|
||||
install: build
|
||||
kpackagetool6 --type=KWin/Script --install=./package || kpackagetool6 --type=KWin/Script --upgrade=./package
|
||||
|
||||
uninstall:
|
||||
kpackagetool5 --type=KWin/Script -r ./package
|
||||
kpackagetool6 --type=KWin/Script --remove=karousel
|
||||
|
||||
package:
|
||||
tar -czf ./karousel.tar.gz ./package
|
||||
|
||||
logs:
|
||||
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
|
||||
package: build
|
||||
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package --transform s/package/karousel/
|
||||
|
||||
docs-key-bindings-bbcode:
|
||||
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsBbcode.ts --outFile /dev/stdout | node -
|
||||
@./run-ts.sh ./src/generators/docs/keyBindingsBbcode
|
||||
|
||||
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-markdown:
|
||||
@./run-ts.sh ./src/generators/docs/keyBindingsMarkdown
|
||||
|
||||
docs-key-bindings-fmt:
|
||||
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsFmt.ts --outFile /dev/stdout | node -
|
||||
@./run-ts.sh ./src/generators/docs/keyBindingsFmt
|
||||
|
||||
36
README.md
36
README.md
@@ -1,24 +1,19 @@
|
||||
# Karousel
|
||||
KWin tiling script with scrolling. Works especially well with ultrawide screens.
|
||||
Scrollable tiling Kwin script. Works especially well with ultrawide screens.
|
||||
Use with [this](https://github.com/peterfajdiga/kwin4_effect_geometry_change) for animations.
|
||||
|
||||
https://github.com/peterfajdiga/karousel/assets/22796326/2ab62d18-09c7-45f9-8fda-e5e36b8d7a02
|
||||
|
||||
Karousel works differently from most tiling window managers in that it does not maximize the width
|
||||
of windows, as this can be undesirable with wider screens, where it results in excessively wide
|
||||
windows that require large return sweeps when reading their content.
|
||||
Instead, it leaves the width of windows to the user's control. This additionally prevents
|
||||
unprompted reflow of window content.
|
||||
A scrollable tiling window manager tiles windows, but it does not maximize their widths. Instead, it leaves the width of windows to the user's control.
|
||||
Windows are automatically centered when possible. And when running out of width, windows can be scrolled through horizontally.
|
||||
|
||||
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
|
||||
- QtQuick 6.0
|
||||
- org.kde.kwin 3.0
|
||||
- org.kde.notification 1.0
|
||||
|
||||
@@ -27,6 +22,13 @@ Karousel requires the following QML modules:
|
||||
- Doesn't support windows on all desktops
|
||||
- Doesn't support windows on multiple activities
|
||||
|
||||
## Installation
|
||||
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
|
||||
|
||||
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
|
||||
|
||||
Or clone the repo and run `make install` (requires npm, node, and tsc).
|
||||
|
||||
## Key bindings
|
||||
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
|
||||
Here's the default ones:
|
||||
@@ -37,21 +39,30 @@ Here's the default ones:
|
||||
| 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) |
|
||||
| (unassigned) | Move focus to the next window in grid |
|
||||
| (unassigned) | Move focus to the previous window in grid |
|
||||
| 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 |
|
||||
| (unassigned) | Move window to the next position in grid |
|
||||
| (unassigned) | Move window to the previous position in grid |
|
||||
| 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+X | Toggle stacked layout for focused column (Only the active window visible) |
|
||||
| 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+R | Cycle through preset column widths |
|
||||
| Meta+Shift+R | Cycle through preset column widths in reverse |
|
||||
| Meta+Ctrl+X | Equalize widths of visible columns |
|
||||
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||
| Meta+Ctrl+D | Squeeze right column onto the screen |
|
||||
| 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 |
|
||||
@@ -59,6 +70,7 @@ Here's the default ones:
|
||||
| Meta+Alt+PgDown | Scroll right |
|
||||
| Meta+Alt+Home | Scroll to start |
|
||||
| Meta+Alt+End | Scroll to end |
|
||||
| Meta+Ctrl+Return | Move Karousel grid to the current screen |
|
||||
| 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+!) |
|
||||
|
||||
15
eslint.config.mjs
Normal file
15
eslint.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
// @ts-check
|
||||
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
extends: [tseslint.configs.stylistic],
|
||||
rules: {
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"semi": "error",
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"indent": ["error", 4],
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
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]`);
|
||||
@@ -1,14 +0,0 @@
|
||||
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);
|
||||
@@ -1,18 +0,0 @@
|
||||
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, " |");
|
||||
1464
package-lock.json
generated
Normal file
1464
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"eslint": "^9.24.0",
|
||||
"typescript-eslint": "^8.30.1"
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import QtQuick 2.15
|
||||
import org.kde.kwin 3.0
|
||||
import org.kde.notification 1.0
|
||||
import "./main.js" as Karousel
|
||||
|
||||
Item {
|
||||
id: qmlBase
|
||||
|
||||
property var karouselInstance
|
||||
|
||||
Component.onCompleted: {
|
||||
qmlBase.karouselInstance = Karousel.init();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
qmlBase.karouselInstance.destroy();
|
||||
}
|
||||
|
||||
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,6 +11,182 @@
|
||||
<layout class="QVBoxLayout" name="layout_main">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabContainer">
|
||||
<widget class="QWidget" name="tab_behavior">
|
||||
<attribute name="title">
|
||||
<string>Behavior</string>
|
||||
</attribute>
|
||||
<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_cursorFollowsFocus">
|
||||
<property name="text">
|
||||
<string>Cursor follows focus (experimental)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>When a window gains focus, move the cursor to it</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 (only the active window visible)</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>Touchpad scrolling (Wayland only)</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_gestureScroll">
|
||||
<property name="text">
|
||||
<string>Enable scrolling with touchpad gestures
|
||||
(please don't forget to disable KDE's workspace switching gestures)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Scroll with a three-finger horizontal swipe gesture</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<widget class="QCheckBox" name="kcfg_gestureScrollInvert">
|
||||
<property name="text">
|
||||
<string>Invert scroll direction (Natural scrolling)</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>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="kcfg_noLayering">
|
||||
<property name="text">
|
||||
<string>No layering</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>
|
||||
@@ -143,14 +319,14 @@
|
||||
</item>
|
||||
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_overscroll">
|
||||
<widget class="QLabel" name="label_stackOffsetX">
|
||||
<property name="text">
|
||||
<string>Overscroll amount:</string>
|
||||
<string>Horizontal offset for stacked columns:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_overscroll">
|
||||
<widget class="QSpinBox" name="kcfg_stackOffsetX">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
</property>
|
||||
@@ -164,14 +340,14 @@
|
||||
</item>
|
||||
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_manualScrollStep">
|
||||
<widget class="QLabel" name="label_stackOffsetY">
|
||||
<property name="text">
|
||||
<string>Manual scroll step size:</string>
|
||||
<string>Vertical offset for stacked columns:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_manualScrollStep">
|
||||
<widget class="QSpinBox" name="kcfg_stackOffsetY">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
</property>
|
||||
@@ -185,13 +361,82 @@
|
||||
</item>
|
||||
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_manualScrollStep">
|
||||
<property name="text">
|
||||
<string>Manual scroll step size:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_manualScrollStep">
|
||||
<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">
|
||||
<widget class="QLabel" name="label_gestureScrollStep">
|
||||
<property name="text">
|
||||
<string>Touchpad gesture scrolling speed:</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>The amount to scroll per edge-to-edge gesture</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_gestureScrollStep">
|
||||
<property name="suffix">
|
||||
<string> px</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>10000</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>1920</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_presetWidths">
|
||||
<property name="text">
|
||||
<string>Preset widths:</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Widths used for cycling through widths</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLineEdit" name="kcfg_presetWidths">
|
||||
<property name="toolTip">
|
||||
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="label_offScreenOpacity">
|
||||
<property name="text">
|
||||
<string>Obscured window opacity:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<item row="11" column="1">
|
||||
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
|
||||
<property name="suffix">
|
||||
<string> %</string>
|
||||
@@ -204,136 +449,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
|
||||
<widget class="QWidget" name="tab_behavior">
|
||||
<attribute name="title">
|
||||
<string>Behavior</string>
|
||||
</attribute>
|
||||
<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>Prevent needlessly obscuring 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>
|
||||
|
||||
@@ -342,6 +458,27 @@
|
||||
<string>Window Rules</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_desktops">
|
||||
<property name="text">
|
||||
<string>Tiled desktops:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="kcfg_tiledDesktops">
|
||||
<property name="toolTip">
|
||||
<string>Regex string to match desktops by desktop name"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="kcfg_windowRules">
|
||||
<property name="tabChangesFocus">
|
||||
|
||||
73
package/contents/ui/main.qml
Normal file
73
package/contents/ui/main.qml
Normal file
@@ -0,0 +1,73 @@
|
||||
import QtQuick 6.0
|
||||
import org.kde.kwin 3.0
|
||||
import org.kde.notification 1.0
|
||||
import "../code/main.js" as Karousel
|
||||
|
||||
Item {
|
||||
id: qmlBase
|
||||
|
||||
property var karouselInstance
|
||||
|
||||
Component.onCompleted: {
|
||||
qmlBase.karouselInstance = Karousel.init();
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
qmlBase.karouselInstance.destroy();
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: notificationInvalidTiledDesktops
|
||||
componentName: "plasma_workspace"
|
||||
eventId: "notification"
|
||||
title: "Karousel"
|
||||
text: "Your Tiled Desktops regex is malformed, please review your Karousel configuration"
|
||||
flags: Notification.Persistent
|
||||
urgency: Notification.HighUrgency
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Notification {
|
||||
id: notificationInvalidPresetWidths
|
||||
componentName: "plasma_workspace"
|
||||
eventId: "notification"
|
||||
title: "Karousel"
|
||||
text: "Your preset widths are malformed, please review your Karousel configuration"
|
||||
flags: Notification.Persistent
|
||||
urgency: Notification.HighUrgency
|
||||
}
|
||||
|
||||
SwipeGestureHandler {
|
||||
direction: SwipeGestureHandler.Direction.Left
|
||||
fingerCount: 3
|
||||
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
|
||||
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
|
||||
onProgressChanged: qmlBase.karouselInstance.gestureScroll(-progress)
|
||||
}
|
||||
|
||||
SwipeGestureHandler {
|
||||
direction: SwipeGestureHandler.Direction.Right
|
||||
fingerCount: 3
|
||||
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
|
||||
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
|
||||
onProgressChanged: qmlBase.karouselInstance.gestureScroll(progress)
|
||||
}
|
||||
|
||||
DBusCall {
|
||||
id: moveCursorToFocus
|
||||
|
||||
service: "org.kde.kglobalaccel"
|
||||
path: "/component/kwin"
|
||||
method: "invokeShortcut"
|
||||
arguments: ["MoveMouseToFocus"]
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"KPackageStructure": "KWin/Script",
|
||||
"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",
|
||||
"Name": "Peter Fajdiga"
|
||||
}],
|
||||
"Id": "karousel",
|
||||
"ServiceTypes": ["KWin/Script"],
|
||||
"Version": "0.6",
|
||||
"Version": "0.15",
|
||||
"License": "GPLv3",
|
||||
"Website": "https://github.com/peterfajdiga/karousel",
|
||||
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
|
||||
},
|
||||
"X-Plasma-API": "declarativescript",
|
||||
"X-Plasma-MainScript": "code/main.qml",
|
||||
"X-Plasma-API-Minimum-Version": "6.0",
|
||||
"X-Plasma-MainScript": "ui/main.qml",
|
||||
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted"
|
||||
}
|
||||
|
||||
8
run-ts.sh
Executable file
8
run-ts.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
JS_FILE='./run-ts-tmp.js'
|
||||
|
||||
tsc -p "$1" --outFile "$JS_FILE"
|
||||
node "$JS_FILE"
|
||||
329
src/Actions.ts
329
src/Actions.ts
@@ -1,329 +0,0 @@
|
||||
namespace Actions {
|
||||
export function init(world: World, config: Config) {
|
||||
return {
|
||||
focusLeft: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
const prevColumn = grid.getPrevColumn(column);
|
||||
if (prevColumn === null) {
|
||||
return;
|
||||
}
|
||||
prevColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusRight: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
const nextColumn = grid.getNextColumn(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
nextColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusUp: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
const prevWindow = column.getPrevWindow(window);
|
||||
if (prevWindow === null) {
|
||||
return;
|
||||
}
|
||||
prevWindow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
focusDown: () => {
|
||||
world.doIfTiledFocused(true, (world, 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, (world, 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, (world, 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, (world, desktopManager, window, column, grid) => {
|
||||
column.moveWindowUp(window);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveDown: () => {
|
||||
// TODO (optimization): only arrange moved windows
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
column.moveWindowDown(window);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
const newColumn = new Column(grid, null);
|
||||
window.moveToColumn(newColumn);
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (world, 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, (world, desktopManager, window, column, grid) => {
|
||||
grid.moveColumnLeft(column);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveRight: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
grid.moveColumnRight(column);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveStart: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
column.moveAfter(null);
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveEnd: () => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
column.moveAfter(grid.getLastColumn());
|
||||
});
|
||||
},
|
||||
|
||||
columnToggleStacked: () => {
|
||||
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
|
||||
column.toggleStacked();
|
||||
});
|
||||
},
|
||||
|
||||
columnWidthIncrease: () => {
|
||||
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
|
||||
grid.increaseColumnWidth(column);
|
||||
});
|
||||
},
|
||||
|
||||
columnWidthDecrease: () => {
|
||||
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
|
||||
grid.decreaseColumnWidth(column);
|
||||
});
|
||||
},
|
||||
|
||||
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, (world, 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 null;
|
||||
}
|
||||
targetColumn.focus();
|
||||
});
|
||||
},
|
||||
|
||||
windowMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return null;
|
||||
}
|
||||
window.moveToColumn(targetColumn);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
});
|
||||
},
|
||||
|
||||
columnMoveToColumn: (columnIndex: number) => {
|
||||
world.doIfTiledFocused(true, (world, desktopManager, 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));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
enum ClientAreaOption {
|
||||
PlacementArea,
|
||||
MovementArea,
|
||||
MaximizeArea,
|
||||
MaximizeFullArea,
|
||||
FullScreenArea,
|
||||
WorkArea,
|
||||
FullArea,
|
||||
ScreenArea,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
type Config = {
|
||||
gapsOuterTop: number,
|
||||
gapsOuterBottom: number,
|
||||
gapsOuterLeft: number,
|
||||
gapsOuterRight: number,
|
||||
gapsInnerHorizontal: number,
|
||||
gapsInnerVertical: number,
|
||||
overscroll: number,
|
||||
manualScrollStep: 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,
|
||||
};
|
||||
@@ -1,174 +0,0 @@
|
||||
const defaultWindowRules = `[
|
||||
{
|
||||
"class": "ksmserver-logout-greeter",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"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": "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 Cloud Meetings|zoom|zoom <2>",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-idea",
|
||||
"caption": "splash",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-studio",
|
||||
"caption": "splash",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-idea",
|
||||
"caption": "Unstash Changes|Paths Affected by stash@.*",
|
||||
"tile": true
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-studio",
|
||||
"caption": "Unstash Changes|Paths Affected by stash@.*",
|
||||
"tile": true
|
||||
}
|
||||
]`;
|
||||
|
||||
const configDef = [
|
||||
{
|
||||
"name": "gapsOuterTop",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterBottom",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterLeft",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "gapsOuterRight",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "gapsInnerHorizontal",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "gapsInnerVertical",
|
||||
"type": "UInt",
|
||||
"default": 18
|
||||
},
|
||||
{
|
||||
"name": "overscroll",
|
||||
"type": "UInt",
|
||||
"default": 0
|
||||
},
|
||||
{
|
||||
"name": "manualScrollStep",
|
||||
"type": "UInt",
|
||||
"default": 200
|
||||
},
|
||||
{
|
||||
"name": "offScreenOpacity",
|
||||
"type": "UInt",
|
||||
"default": 100
|
||||
},
|
||||
{
|
||||
"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
|
||||
}
|
||||
];
|
||||
10
src/extern/global.d.ts
vendored
10
src/extern/global.d.ts
vendored
@@ -1,6 +1,8 @@
|
||||
declare const Qt: Qt;
|
||||
declare const KWin: KWin;
|
||||
declare const Workspace: Workspace;
|
||||
declare const qmlBase: QmlObject;
|
||||
declare const notificationInvalidTiledDesktops: Notification;
|
||||
declare const notificationInvalidWindowRules: Notification;
|
||||
|
||||
type Notification = {
|
||||
sendEvent(): void;
|
||||
};
|
||||
declare const notificationInvalidPresetWidths: Notification;
|
||||
declare const moveCursorToFocus: DBusCall;
|
||||
|
||||
77
src/extern/kwin.d.ts
vendored
77
src/extern/kwin.d.ts
vendored
@@ -1,77 +0,0 @@
|
||||
declare const KWin: {
|
||||
// Functions
|
||||
readConfig(key: string, defaultValue: any): any;
|
||||
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
|
||||
};
|
||||
|
||||
declare const workspace: {
|
||||
// Read-write Properties
|
||||
readonly desktops: number;
|
||||
readonly currentDesktop: number;
|
||||
readonly currentActivity: string;
|
||||
|
||||
// Read-write Properties
|
||||
activeClient: KwinClient;
|
||||
|
||||
// Signals
|
||||
currentDesktopChanged: QSignal<[oldDesktopNumber: number]>
|
||||
clientAdded: QSignal<[KwinClient]>;
|
||||
clientRemoved: QSignal<[KwinClient]>;
|
||||
clientMinimized: QSignal<[KwinClient]>;
|
||||
clientUnminimized: QSignal<[KwinClient]>;
|
||||
clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
|
||||
clientActivated: QSignal<[KwinClient]>;
|
||||
numberDesktopsChanged: QSignal<[oldNumberOfVirtualDesktops: number]>;
|
||||
currentActivityChanged: QSignal<[newActivity: string]>;
|
||||
virtualScreenSizeChanged: QSignal<[void]>;
|
||||
|
||||
// Functions
|
||||
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
|
||||
clientList(): KwinClient[];
|
||||
};
|
||||
|
||||
type Tile = any;
|
||||
|
||||
interface KwinClient {
|
||||
// Read-only Properties
|
||||
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;
|
||||
|
||||
// Read-write Properties
|
||||
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;
|
||||
|
||||
// Signals
|
||||
fullScreenChanged: QSignal<[void]>;
|
||||
desktopChanged: QSignal<[void]>;
|
||||
activitiesChanged: QSignal<[KwinClient]>;
|
||||
captionChanged: QSignal<[void]>;
|
||||
tileChanged: QSignal<[Tile]>;
|
||||
moveResizedChanged: QSignal<[void]>;
|
||||
moveResizeCursorChanged: QSignal<[void]>;
|
||||
clientStartUserMovedResized: QSignal<[void]>;
|
||||
frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QmlRect]>;
|
||||
|
||||
// Functions
|
||||
setMaximize(vertically: boolean, horizontally: boolean): void;
|
||||
}
|
||||
41
src/extern/qt.d.ts
vendored
41
src/extern/qt.d.ts
vendored
@@ -1,41 +0,0 @@
|
||||
declare const console: {
|
||||
log(...args: any[]);
|
||||
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;
|
||||
triggered: QSignal<[void]>;
|
||||
restart(): void;
|
||||
destroy(): void;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
8
src/generators/config/tsconfig.json
Normal file
8
src/generators/config/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"../../extern/**/*",
|
||||
"../../lib/**/*",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,36 +1,25 @@
|
||||
type KeyBinding = {
|
||||
name: string;
|
||||
interface DocsKeyBinding {
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultKeySequence: string;
|
||||
action: string;
|
||||
keySequence: 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 formatDescription(item: {description: string, comment?: string}) {
|
||||
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
|
||||
return `${applyMacro(item.description, "N")}${suffix}`;
|
||||
}
|
||||
|
||||
function printCols(...columns: (string[] | string)[]) {
|
||||
const nCols = columns.length;
|
||||
if (nCols == 0) {
|
||||
if (nCols === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let nRows = Math.min(...columns.filter(
|
||||
(column: string[] | string) => column instanceof Array
|
||||
(column: string[] | string) => column instanceof Array,
|
||||
).map(
|
||||
(column: string[] | string) => column.length
|
||||
(column: string[] | string) => column.length,
|
||||
));
|
||||
if (nRows == Infinity) {
|
||||
if (nRows === Infinity) {
|
||||
// we only have single string columns
|
||||
nRows = 1;
|
||||
}
|
||||
@@ -39,12 +28,12 @@ function printCols(...columns: (string[] | string)[]) {
|
||||
(column: string[] | string) => {
|
||||
if (column instanceof Array) {
|
||||
return Math.max(...column.map(
|
||||
(cell: string) => cell.length
|
||||
))
|
||||
(cell: string) => cell.length,
|
||||
));
|
||||
} else {
|
||||
return column.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function getCell(col: number, row: number) {
|
||||
@@ -65,3 +54,15 @@ function printCols(...columns: (string[] | string)[]) {
|
||||
console.log(line);
|
||||
}
|
||||
}
|
||||
|
||||
const empty: any = {};
|
||||
const keyBindings: DocsKeyBinding[] = Array.prototype.concat(
|
||||
getKeyBindings(empty, empty).map(binding => ({
|
||||
description: formatDescription(binding),
|
||||
keySequence: binding.defaultKeySequence || "(unassigned)",
|
||||
})),
|
||||
getNumKeyBindings(empty, empty).map(binding => ({
|
||||
description: formatDescription(binding),
|
||||
keySequence: `${binding.defaultModifiers}+${binding.fKeys ? "F" : ""}[N]`,
|
||||
})),
|
||||
);
|
||||
7
src/generators/docs/keyBindingsBbcode/main.ts
Normal file
7
src/generators/docs/keyBindingsBbcode/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
console.log(`[list]`);
|
||||
|
||||
for (const binding of keyBindings) {
|
||||
console.log(` [*] ${binding.keySequence} — ${binding.description}`);
|
||||
}
|
||||
|
||||
console.log(`[/list]`);
|
||||
9
src/generators/docs/keyBindingsBbcode/tsconfig.json
Normal file
9
src/generators/docs/keyBindingsBbcode/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": [
|
||||
"../../../extern/**/*",
|
||||
"../../../lib/**/*",
|
||||
"../keyBindings.ts",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
9
src/generators/docs/keyBindingsFmt/main.ts
Normal file
9
src/generators/docs/keyBindingsFmt/main.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
const colLeft = [
|
||||
...keyBindings.map(binding => binding.keySequence),
|
||||
];
|
||||
|
||||
const colRight = [
|
||||
...keyBindings.map(binding => binding.description),
|
||||
];
|
||||
|
||||
printCols(colLeft, " ", colRight);
|
||||
9
src/generators/docs/keyBindingsFmt/tsconfig.json
Normal file
9
src/generators/docs/keyBindingsFmt/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": [
|
||||
"../../../extern/**/*",
|
||||
"../../../lib/**/*",
|
||||
"../keyBindings.ts",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
13
src/generators/docs/keyBindingsMarkdown/main.ts
Normal file
13
src/generators/docs/keyBindingsMarkdown/main.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const colLeft = [
|
||||
"Shortcut",
|
||||
"---",
|
||||
...keyBindings.map(binding => binding.keySequence),
|
||||
];
|
||||
|
||||
const colRight = [
|
||||
"Action",
|
||||
"---",
|
||||
...keyBindings.map(binding => binding.description),
|
||||
];
|
||||
|
||||
printCols("| ", colLeft, " | ", colRight, " |");
|
||||
9
src/generators/docs/keyBindingsMarkdown/tsconfig.json
Normal file
9
src/generators/docs/keyBindingsMarkdown/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"include": [
|
||||
"../../../extern/**/*",
|
||||
"../../../lib/**/*",
|
||||
"../keyBindings.ts",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
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",
|
||||
},
|
||||
];
|
||||
@@ -1,65 +0,0 @@
|
||||
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, config);
|
||||
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,232 +0,0 @@
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
// calculates a Range that scrolls the contained Range into view
|
||||
public calculateVisibleRange(containedRange: Desktop.Range) {
|
||||
const left = containedRange.getLeft();
|
||||
const right = containedRange.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 {
|
||||
return this.getVisibleRange(this.clampScrollX(this.scrollX));
|
||||
}
|
||||
|
||||
const overscroll = this.getTargetOverscroll(targetScrollX, left < initialVisibleRange.getLeft());
|
||||
return this.getVisibleRange(this.clampScrollX(targetScrollX + overscroll));
|
||||
}
|
||||
|
||||
private getTargetOverscroll(targetScrollX: number, scrollLeft: boolean) {
|
||||
if (this.config.overscroll === 0) {
|
||||
return 0;
|
||||
}
|
||||
const visibleColumnsWidth = this.grid.getVisibleColumnsWidth(this.getVisibleRange(targetScrollX), true);
|
||||
const remainingSpace = this.tilingArea.width - visibleColumnsWidth;
|
||||
const overscrollX = Math.min(this.config.overscroll, Math.round(remainingSpace / 2));
|
||||
const direction = scrollLeft ? -1 : 1;
|
||||
return overscrollX * direction;
|
||||
}
|
||||
|
||||
public scrollToRange(range: Desktop.Range) {
|
||||
this.setScroll(this.calculateVisibleRange(range).getLeft(), true);
|
||||
}
|
||||
|
||||
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 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.scroller.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;
|
||||
for (const column of visibleColumns) {
|
||||
const columnWidth = Math.round(remainingWidth / remainingColumns);
|
||||
column.setWidth(columnWidth, true);
|
||||
remainingWidth -= columnWidth;
|
||||
remainingColumns--;
|
||||
}
|
||||
|
||||
const targetVisibleRange = Desktop.RangeImpl.fromRanges(
|
||||
visibleColumns[0],
|
||||
visibleColumns[visibleColumns.length - 1],
|
||||
);
|
||||
this.setScroll(this.calculateVisibleRange(targetVisibleRange).getLeft(), false);
|
||||
}
|
||||
|
||||
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,
|
||||
overscroll: number,
|
||||
scroller: Desktop.Scroller,
|
||||
};
|
||||
|
||||
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 type Scroller = {
|
||||
scrollToColumn(desktop: Desktop, column: Column): void;
|
||||
clampScrollX(desktop: Desktop, x: number): number;
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
import Range = Desktop.Range;
|
||||
|
||||
class Grid {
|
||||
public readonly desktop: Desktop;
|
||||
public readonly config: LayoutConfig;
|
||||
private readonly columns: LinkedList<Column>;
|
||||
private lastFocusedColumn: Column|null;
|
||||
private width: number;
|
||||
private userResize: boolean; // is any part of the grid being resized by the user
|
||||
private readonly userResizeFinishedDelayer: Delayer;
|
||||
|
||||
constructor(desktop: Desktop, config: LayoutConfig) {
|
||||
this.desktop = desktop;
|
||||
this.config = config;
|
||||
this.columns = new LinkedList();
|
||||
this.lastFocusedColumn = null;
|
||||
this.width = 0;
|
||||
this.userResize = false;
|
||||
this.userResizeFinishedDelayer = new Delayer(50, () => {
|
||||
// this delay prevents windows' contents from freezing after resizing
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
this.desktop.arrange();
|
||||
});
|
||||
}
|
||||
|
||||
public moveColumnLeft(column: Column) {
|
||||
this.columns.moveBack(column);
|
||||
this.columnsSetX(column);
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public moveColumnRight(column: Column) {
|
||||
const nextColumn = this.columns.getNext(column);
|
||||
if (nextColumn === null) {
|
||||
return;
|
||||
}
|
||||
this.moveColumnLeft(nextColumn);
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public getPrevColumn(column: Column) {
|
||||
return this.columns.getPrev(column);
|
||||
}
|
||||
|
||||
public getNextColumn(column: Column) {
|
||||
return this.columns.getNext(column);
|
||||
}
|
||||
|
||||
public getFirstColumn() {
|
||||
return this.columns.getFirst();
|
||||
}
|
||||
|
||||
public getLastColumn() {
|
||||
return this.columns.getLast();
|
||||
}
|
||||
|
||||
public getColumnAtIndex(i: number) {
|
||||
return this.columns.getItemAtIndex(i);
|
||||
}
|
||||
|
||||
public getLastFocusedColumn() {
|
||||
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
|
||||
return null;
|
||||
}
|
||||
return this.lastFocusedColumn;
|
||||
}
|
||||
|
||||
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()) {
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
let last = null;
|
||||
for (const column of this.columns.iterator()) {
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
last = column;
|
||||
} else if (last !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
||||
yield column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 increaseColumnWidth(column: Column) {
|
||||
const visibleRange = this.desktop.calculateVisibleRange(column);
|
||||
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let leftVisibleColumn = this.getLeftmostVisibleColumn(visibleRange, true);
|
||||
let rightVisibleColumn = this.getRightmostVisibleColumn(visibleRange, true);
|
||||
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
||||
console.assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
|
||||
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
|
||||
if (leftSpace + rightSpace > 0) {
|
||||
column.adjustWidth(leftSpace + rightSpace, true);
|
||||
} else {
|
||||
// left and right columns are touching the screen's edges
|
||||
const leftSpace = leftVisibleColumn === column ? Infinity : leftVisibleColumn.getWidth() + this.config.gapsInnerHorizontal;
|
||||
const rightSpace = rightVisibleColumn === column ? Infinity : rightVisibleColumn.getWidth() + this.config.gapsInnerHorizontal;
|
||||
if (leftSpace < rightSpace) {
|
||||
column.adjustWidth(leftSpace, true);
|
||||
leftVisibleColumn = this.getNextColumn(leftVisibleColumn)!;
|
||||
} else {
|
||||
column.adjustWidth(rightSpace, true);
|
||||
rightVisibleColumn = this.getPrevColumn(rightVisibleColumn)!;
|
||||
}
|
||||
}
|
||||
|
||||
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
|
||||
}
|
||||
|
||||
public decreaseColumnWidth(column: Column) {
|
||||
const visibleRange = this.desktop.calculateVisibleRange(column);
|
||||
if (!column.isVisible(visibleRange, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.width <= visibleRange.getWidth()) {
|
||||
column.setWidth(Math.round(column.getWidth() / 2), true);
|
||||
return;
|
||||
}
|
||||
|
||||
const leftVisibleColumn = this.getLeftmostVisibleColumn(visibleRange, true);
|
||||
const rightVisibleColumn = this.getRightmostVisibleColumn(visibleRange, true);
|
||||
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
||||
console.assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let leftOffScreenColumn = this.getPrevColumn(leftVisibleColumn);
|
||||
if (leftOffScreenColumn === column) {
|
||||
leftOffScreenColumn = null;
|
||||
}
|
||||
let rightOffScreenColumn = this.getNextColumn(rightVisibleColumn);
|
||||
if (rightOffScreenColumn === column) {
|
||||
rightOffScreenColumn = null;
|
||||
}
|
||||
if (leftOffScreenColumn === null && rightOffScreenColumn === null) {
|
||||
console.assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const leftInvisibleWidth = leftOffScreenColumn === null ? Infinity : visibleRange.getLeft() - leftOffScreenColumn.getLeft();
|
||||
const rightInvisibleWidth = rightOffScreenColumn === null ? Infinity : rightOffScreenColumn.getRight() - visibleRange.getRight();
|
||||
|
||||
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
|
||||
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
|
||||
|
||||
if (leftInvisibleWidth < rightInvisibleWidth) {
|
||||
const deltaWidth = rightSpace - leftInvisibleWidth;
|
||||
column.adjustWidth(deltaWidth, true);
|
||||
console.assert(leftOffScreenColumn !== null);
|
||||
const newVisibleWidth = rightVisibleColumn.getRight() - leftOffScreenColumn!.getLeft();
|
||||
const leftVisibleColumn = newVisibleWidth <= visibleRange.getWidth() ? leftOffScreenColumn! : this.getNextColumn(leftOffScreenColumn!)!;
|
||||
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
|
||||
} else {
|
||||
const deltaWidth = leftSpace - rightInvisibleWidth;
|
||||
column.adjustWidth(deltaWidth, true);
|
||||
console.assert(rightOffScreenColumn !== null);
|
||||
const newVisibleWidth = rightOffScreenColumn!.getRight() - leftVisibleColumn.getLeft();
|
||||
const rightVisibleColumn = newVisibleWidth <= visibleRange.getWidth() ? rightOffScreenColumn! : this.getPrevColumn(rightOffScreenColumn!)!;
|
||||
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
|
||||
}
|
||||
}
|
||||
|
||||
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.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
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 = columnToFocus;
|
||||
}
|
||||
|
||||
this.columns.remove(column);
|
||||
this.columnsSetX(nextColumn);
|
||||
|
||||
this.desktop.onLayoutChanged();
|
||||
if (passFocus && columnToFocus !== null) {
|
||||
columnToFocus.focus();
|
||||
} else {
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
}
|
||||
|
||||
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.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public onColumnWidthChanged(column: Column, oldWidth: number, width: number) {
|
||||
const nextColumn = this.columns.getNext(column);
|
||||
this.columnsSetX(nextColumn);
|
||||
if (!this.userResize) {
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
this.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
public onColumnFocused(column: Column) {
|
||||
const lastFocusedColumn = this.getLastFocusedColumn();
|
||||
if (lastFocusedColumn !== null) {
|
||||
lastFocusedColumn.restoreToTiled();
|
||||
}
|
||||
this.lastFocusedColumn = column;
|
||||
this.desktop.scrollToColumn(column);
|
||||
}
|
||||
|
||||
public onScreenSizeChanged() {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.updateWidth();
|
||||
column.resizeWindows();
|
||||
}
|
||||
}
|
||||
|
||||
public onUserResizeStarted() {
|
||||
this.userResize = true;
|
||||
}
|
||||
|
||||
public onUserResizeFinished() {
|
||||
this.userResize = false;
|
||||
this.userResizeFinishedDelayer.run();
|
||||
}
|
||||
|
||||
public evacuateTail(targetGrid: Grid, startColumn: Column) {
|
||||
for (const column of this.columns.iteratorFrom(startColumn)) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public evacuate(targetGrid: Grid) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.userResizeFinishedDelayer.destroy();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
type LayoutConfig = {
|
||||
gapsInnerHorizontal: number,
|
||||
gapsInnerVertical: number,
|
||||
offScreenOpacity: number,
|
||||
stackColumnsByDefault: boolean,
|
||||
resizeNeighborColumn: boolean,
|
||||
reMaximize: boolean,
|
||||
skipSwitcher: boolean,
|
||||
tiledKeepBelow: boolean,
|
||||
maximizedKeepAbove: boolean,
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
class ScrollerCentered {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollCenterRange(column);
|
||||
}
|
||||
|
||||
public clampScrollX(desktop: Desktop, x: number) {
|
||||
return ScrollerCentered.clampScrollX(desktop, x);
|
||||
}
|
||||
|
||||
public static 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);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
class ScrollerGrouped {
|
||||
private readonly layoutConfig: LayoutConfig;
|
||||
|
||||
constructor(layoutConfig: LayoutConfig) {
|
||||
this.layoutConfig = layoutConfig;
|
||||
}
|
||||
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
const columnRange = new ScrollerGrouped.ColumnRange(column);
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
columnRange.addNeighbors(visibleRange, this.layoutConfig.gapsInnerHorizontal, true);
|
||||
columnRange.addNeighbors(visibleRange, this.layoutConfig.gapsInnerHorizontal, false);
|
||||
desktop.scrollCenterRange(columnRange);
|
||||
}
|
||||
|
||||
public clampScrollX(desktop: Desktop, x: number) {
|
||||
return ScrollerCentered.clampScrollX(desktop, x);
|
||||
}
|
||||
}
|
||||
|
||||
namespace ScrollerGrouped {
|
||||
import Range = Desktop.Range;
|
||||
|
||||
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: Range, gap: number, requireVisible: boolean) {
|
||||
const grid = this.left.grid;
|
||||
|
||||
let leftColumn: Column|null = this.left;
|
||||
while (true) {
|
||||
leftColumn = grid.getPrevColumn(leftColumn);
|
||||
if (
|
||||
leftColumn === null ||
|
||||
requireVisible && !leftColumn.isVisible(visibleRange, true) ||
|
||||
this.width + gap + leftColumn.getWidth() > visibleRange.getWidth()
|
||||
) {
|
||||
break;
|
||||
}
|
||||
this.addLeft(leftColumn, gap);
|
||||
}
|
||||
|
||||
let rightColumn: Column|null = this.right;
|
||||
while (true) {
|
||||
rightColumn = grid.getNextColumn(rightColumn);
|
||||
if (
|
||||
rightColumn === null ||
|
||||
requireVisible && !rightColumn.isVisible(visibleRange, true) ||
|
||||
this.width + gap + rightColumn.getWidth() > visibleRange.getWidth()
|
||||
) {
|
||||
break;
|
||||
}
|
||||
this.addRight(rightColumn, gap);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
class ScrollerLazy {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollToRange(column);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
62
src/lib/behavior/PresetWidths.ts
Normal file
62
src/lib/behavior/PresetWidths.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
class PresetWidths {
|
||||
private readonly presets: ((maxWidth: number) => number)[];
|
||||
|
||||
constructor(presetWidths: string, spacing: number) {
|
||||
this.presets = PresetWidths.parsePresetWidths(presetWidths, spacing);
|
||||
}
|
||||
|
||||
public next(currentWidth: number, minWidth: number, maxWidth: number) {
|
||||
const widths = this.getWidths(minWidth, maxWidth);
|
||||
const nextIndex = widths.findIndex(width => width > currentWidth);
|
||||
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
|
||||
}
|
||||
|
||||
public prev(currentWidth: number, minWidth: number, maxWidth: number) {
|
||||
const widths = this.getWidths(minWidth, maxWidth).reverse();
|
||||
const nextIndex = widths.findIndex(width => width < currentWidth);
|
||||
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
|
||||
}
|
||||
|
||||
public getWidths(minWidth: number, maxWidth: number) {
|
||||
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
|
||||
widths.sort((a, b) => a - b);
|
||||
return uniq(widths);
|
||||
}
|
||||
|
||||
private static parsePresetWidths(presetWidths: string, spacing: number): ((maxWidth: number) => number)[] {
|
||||
function getRatioFunction(ratio: number) {
|
||||
return (maxWidth: number) => Math.floor((maxWidth + spacing) * ratio - spacing);
|
||||
}
|
||||
|
||||
return presetWidths.split(",").map((widthStr: string) => {
|
||||
widthStr = widthStr.trim();
|
||||
|
||||
const widthPx = PresetWidths.parseNumberWithSuffix(widthStr, "px");
|
||||
if (widthPx !== undefined) {
|
||||
return () => widthPx;
|
||||
}
|
||||
|
||||
const widthPct = PresetWidths.parseNumberWithSuffix(widthStr, "%");
|
||||
if (widthPct !== undefined) {
|
||||
return getRatioFunction(widthPct / 100.0);
|
||||
}
|
||||
|
||||
return getRatioFunction(PresetWidths.parseNumberSafe(widthStr));
|
||||
});
|
||||
}
|
||||
|
||||
private static parseNumberSafe(str: string) {
|
||||
const num = Number(str);
|
||||
if (isNaN(num) || num <= 0) {
|
||||
throw new Error("Invalid number: " + str);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
private static parseNumberWithSuffix(str: string, suffix: string) {
|
||||
if (!str.endsWith(suffix)) {
|
||||
return undefined;
|
||||
}
|
||||
return PresetWidths.parseNumberSafe(str.substring(0, str.length-suffix.length).trim());
|
||||
}
|
||||
}
|
||||
89
src/lib/behavior/columnResizer/ContextualResizer.ts
Normal file
89
src/lib/behavior/columnResizer/ContextualResizer.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
class ContextualResizer {
|
||||
constructor(
|
||||
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
|
||||
) {}
|
||||
|
||||
public increaseWidth(column: Column) {
|
||||
const grid = column.grid;
|
||||
const desktop = grid.desktop;
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
const minWidth = column.getMinWidth();
|
||||
const maxWidth = column.getMaxWidth();
|
||||
if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
|
||||
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;
|
||||
}
|
||||
|
||||
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
|
||||
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
|
||||
|
||||
const newWidth = findMinPositive(
|
||||
[
|
||||
column.getWidth() + leftSpace + rightSpace,
|
||||
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
|
||||
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
|
||||
...this.presetWidths.getWidths(minWidth, maxWidth),
|
||||
],
|
||||
width => width - column.getWidth(),
|
||||
);
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
column.setWidth(newWidth, true);
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
|
||||
public decreaseWidth(column: Column) {
|
||||
const grid = column.grid;
|
||||
const desktop = grid.desktop;
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
const minWidth = column.getMinWidth();
|
||||
const maxWidth = column.getMaxWidth();
|
||||
if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
|
||||
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.getLeftColumn(leftVisibleColumn);
|
||||
if (leftOffScreenColumn === column) {
|
||||
leftOffScreenColumn = null;
|
||||
}
|
||||
let rightOffScreenColumn = grid.getRightColumn(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 = findMinPositive(
|
||||
[
|
||||
column.getWidth() - leftOffScreen,
|
||||
column.getWidth() - rightOffScreen,
|
||||
...this.presetWidths.getWidths(minWidth, maxWidth),
|
||||
],
|
||||
width => column.getWidth() - width,
|
||||
);
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
column.setWidth(newWidth, true);
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
}
|
||||
31
src/lib/behavior/columnResizer/RawResizer.ts
Normal file
31
src/lib/behavior/columnResizer/RawResizer.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
class RawResizer {
|
||||
constructor(
|
||||
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
|
||||
) {}
|
||||
|
||||
public increaseWidth(column: Column) {
|
||||
const newWidth = findMinPositive(
|
||||
[
|
||||
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
|
||||
],
|
||||
width => width - column.getWidth(),
|
||||
);
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
column.setWidth(newWidth, true);
|
||||
}
|
||||
|
||||
public decreaseWidth(column: Column) {
|
||||
const newWidth = findMinPositive(
|
||||
[
|
||||
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
|
||||
],
|
||||
width => column.getWidth() - width,
|
||||
);
|
||||
if (newWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
column.setWidth(newWidth, true);
|
||||
}
|
||||
}
|
||||
13
src/lib/behavior/scrollClamper/CenterClamper.ts
Normal file
13
src/lib/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()!;
|
||||
|
||||
const minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
|
||||
const maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
|
||||
return clamp(x, minScroll, maxScroll);
|
||||
}
|
||||
}
|
||||
10
src/lib/behavior/scrollClamper/EdgeClamper.ts
Normal file
10
src/lib/behavior/scrollClamper/EdgeClamper.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
class EdgeClamper {
|
||||
public clampScrollX(desktop: Desktop, x: number) {
|
||||
const minScroll = 0;
|
||||
const maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
|
||||
if (maxScroll < 0) {
|
||||
return Math.round(maxScroll / 2);
|
||||
}
|
||||
return clamp(x, minScroll, maxScroll);
|
||||
}
|
||||
}
|
||||
5
src/lib/behavior/scroller/CenteredScroller.ts
Normal file
5
src/lib/behavior/scroller/CenteredScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class CenteredScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollCenterRange(column);
|
||||
}
|
||||
}
|
||||
5
src/lib/behavior/scroller/GroupedScroller.ts
Normal file
5
src/lib/behavior/scroller/GroupedScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class GroupedScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollCenterVisible(column);
|
||||
}
|
||||
}
|
||||
5
src/lib/behavior/scroller/LazyScroller.ts
Normal file
5
src/lib/behavior/scroller/LazyScroller.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
class LazyScroller {
|
||||
public scrollToColumn(desktop: Desktop, column: Column) {
|
||||
desktop.scrollIntoView(column);
|
||||
}
|
||||
}
|
||||
29
src/lib/config/config.ts
Normal file
29
src/lib/config/config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
interface Config {
|
||||
gapsOuterTop: number;
|
||||
gapsOuterBottom: number;
|
||||
gapsOuterLeft: number;
|
||||
gapsOuterRight: number;
|
||||
gapsInnerHorizontal: number;
|
||||
gapsInnerVertical: number;
|
||||
stackOffsetX: number;
|
||||
stackOffsetY: number;
|
||||
manualScrollStep: number;
|
||||
presetWidths: string;
|
||||
offScreenOpacity: number;
|
||||
untileOnDrag: boolean;
|
||||
cursorFollowsFocus: boolean;
|
||||
stackColumnsByDefault: boolean;
|
||||
resizeNeighborColumn: boolean;
|
||||
reMaximize: boolean;
|
||||
skipSwitcher: boolean;
|
||||
scrollingLazy: boolean;
|
||||
scrollingCentered: boolean;
|
||||
scrollingGrouped: boolean;
|
||||
gestureScroll: boolean;
|
||||
gestureScrollInvert: boolean;
|
||||
gestureScrollStep: number;
|
||||
tiledKeepBelow: boolean;
|
||||
floatingKeepAbove: boolean;
|
||||
windowRules: string;
|
||||
tiledDesktops: string;
|
||||
}
|
||||
197
src/lib/config/definition.ts
Normal file
197
src/lib/config/definition.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
const defaultWindowRules = `[
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?plasmashell",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?polkit-kde-authentication-agent-1",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?kded6",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?kcalc",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?kfind",
|
||||
"tile": true
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?kruler",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?krunner",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "(org\\\\.kde\\\\.)?yakuake",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "steam",
|
||||
"caption": "Steam Big Picture Mode",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "zoom",
|
||||
"caption": "Zoom Cloud Meetings|zoom|zoom <2>",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-.*",
|
||||
"caption": "splash",
|
||||
"tile": false
|
||||
},
|
||||
{
|
||||
"class": "jetbrains-.*",
|
||||
"caption": "Unstash Changes|Paths Affected by stash@.*",
|
||||
"tile": true
|
||||
}
|
||||
]`;
|
||||
|
||||
const configDef = [
|
||||
{
|
||||
name: "gapsOuterTop",
|
||||
type: "UInt",
|
||||
default: 16,
|
||||
},
|
||||
{
|
||||
name: "gapsOuterBottom",
|
||||
type: "UInt",
|
||||
default: 16,
|
||||
},
|
||||
{
|
||||
name: "gapsOuterLeft",
|
||||
type: "UInt",
|
||||
default: 16,
|
||||
},
|
||||
{
|
||||
name: "gapsOuterRight",
|
||||
type: "UInt",
|
||||
default: 16,
|
||||
},
|
||||
{
|
||||
name: "gapsInnerHorizontal",
|
||||
type: "UInt",
|
||||
default: 8,
|
||||
},
|
||||
{
|
||||
name: "gapsInnerVertical",
|
||||
type: "UInt",
|
||||
default: 8,
|
||||
},
|
||||
{
|
||||
name: "stackOffsetX",
|
||||
type: "UInt",
|
||||
default: 8,
|
||||
},
|
||||
{
|
||||
name: "stackOffsetY",
|
||||
type: "UInt",
|
||||
default: 32,
|
||||
},
|
||||
{
|
||||
name: "manualScrollStep",
|
||||
type: "UInt",
|
||||
default: 200,
|
||||
},
|
||||
{
|
||||
name: "presetWidths",
|
||||
type: "String",
|
||||
default: "50%, 100%",
|
||||
},
|
||||
{
|
||||
name: "offScreenOpacity",
|
||||
type: "UInt",
|
||||
default: 100,
|
||||
},
|
||||
{
|
||||
name: "untileOnDrag",
|
||||
type: "Bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "cursorFollowsFocus",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
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: "gestureScroll",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "gestureScrollInvert",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "gestureScrollStep",
|
||||
type: "UInt",
|
||||
default: 1920,
|
||||
},
|
||||
{
|
||||
name: "tiledKeepBelow",
|
||||
type: "Bool",
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
name: "floatingKeepAbove",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "noLayering",
|
||||
type: "Bool",
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
name: "windowRules",
|
||||
type: "String",
|
||||
default: defaultWindowRules,
|
||||
},
|
||||
{
|
||||
name: "tiledDesktops",
|
||||
type: "String",
|
||||
default: ".*",
|
||||
},
|
||||
];
|
||||
3
src/lib/extern/dbuscall.ts
vendored
Normal file
3
src/lib/extern/dbuscall.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface DBusCall extends QmlObject {
|
||||
call(): void;
|
||||
}
|
||||
1
src/lib/extern/global.d.ts
vendored
Normal file
1
src/lib/extern/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare const console: Console;
|
||||
111
src/lib/extern/kwin.ts
vendored
Normal file
111
src/lib/extern/kwin.ts
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
interface KWin {
|
||||
__brand: "KWin";
|
||||
|
||||
readConfig(key: string, defaultValue: any): any;
|
||||
}
|
||||
|
||||
interface Workspace {
|
||||
__brand: "Workspace";
|
||||
|
||||
readonly activities: string[];
|
||||
readonly desktops: KwinDesktop[];
|
||||
readonly currentDesktop: KwinDesktop;
|
||||
readonly currentActivity: string;
|
||||
readonly activeScreen: Output;
|
||||
readonly windows: KwinClient[];
|
||||
readonly cursorPos: Readonly<QmlPoint>;
|
||||
|
||||
activeWindow: KwinClient|null;
|
||||
|
||||
readonly currentDesktopChanged: QSignal<[]>;
|
||||
readonly windowAdded: QSignal<[KwinClient]>;
|
||||
readonly windowRemoved: QSignal<[KwinClient]>;
|
||||
readonly windowActivated: QSignal<[KwinClient|null]>;
|
||||
readonly screensChanged: QSignal<[]>;
|
||||
readonly activitiesChanged: QSignal<[]>;
|
||||
readonly desktopsChanged: QSignal<[]>;
|
||||
readonly currentActivityChanged: QSignal<[]>;
|
||||
readonly virtualScreenSizeChanged: QSignal<[]>;
|
||||
|
||||
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
|
||||
}
|
||||
|
||||
const enum ClientAreaOption {
|
||||
PlacementArea,
|
||||
MovementArea,
|
||||
MaximizeArea,
|
||||
MaximizeFullArea,
|
||||
FullScreenArea,
|
||||
WorkArea,
|
||||
FullArea,
|
||||
ScreenArea,
|
||||
}
|
||||
|
||||
const enum MaximizedMode {
|
||||
Unmaximized,
|
||||
Vertically,
|
||||
Horizontally,
|
||||
Maximized,
|
||||
}
|
||||
|
||||
interface Tile { __brand: "Tile" }
|
||||
interface Output { __brand: "Output" }
|
||||
|
||||
interface KwinClient {
|
||||
__brand: "KwinClient";
|
||||
|
||||
readonly caption: string;
|
||||
readonly minSize: Readonly<QmlSize>;
|
||||
readonly transient: boolean;
|
||||
readonly transientFor: KwinClient | null;
|
||||
readonly clientGeometry: Readonly<QmlRect>;
|
||||
readonly move: boolean;
|
||||
readonly resize: boolean;
|
||||
readonly moveable: boolean;
|
||||
readonly resizeable: boolean;
|
||||
readonly fullScreenable: boolean;
|
||||
readonly maximizable: boolean;
|
||||
readonly output: Output;
|
||||
readonly resourceClass: string;
|
||||
readonly dock: boolean;
|
||||
readonly normalWindow: boolean;
|
||||
readonly managed: boolean;
|
||||
readonly popupWindow: boolean;
|
||||
readonly pid: number;
|
||||
|
||||
fullScreen: boolean;
|
||||
activities: string[]; // empty array means all activities
|
||||
skipSwitcher: boolean;
|
||||
keepAbove: boolean;
|
||||
keepBelow: boolean;
|
||||
minimized: boolean;
|
||||
frameGeometry: QmlRect;
|
||||
desktops: KwinDesktop[]; // empty array means all desktops
|
||||
tile: Tile|null;
|
||||
opacity: number;
|
||||
|
||||
readonly fullScreenChanged: QSignal<[]>;
|
||||
readonly desktopsChanged: QSignal<[]>;
|
||||
readonly activitiesChanged: QSignal<[]>;
|
||||
readonly minimizedChanged: QSignal<[]>;
|
||||
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>;
|
||||
readonly captionChanged: QSignal<[]>;
|
||||
readonly tileChanged: QSignal<[]>;
|
||||
readonly interactiveMoveResizeStarted: QSignal<[]>;
|
||||
readonly interactiveMoveResizeFinished: QSignal<[]>;
|
||||
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
|
||||
|
||||
setMaximize(vertically: boolean, horizontally: boolean): void;
|
||||
}
|
||||
|
||||
interface KwinDesktop {
|
||||
__brand: "KwinDesktop";
|
||||
|
||||
readonly id: string;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
interface ShortcutHandler extends QmlObject {
|
||||
readonly activated: QSignal<[]>;
|
||||
destroy(): void;
|
||||
}
|
||||
3
src/lib/extern/notification.ts
vendored
Normal file
3
src/lib/extern/notification.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
interface Notification extends QmlObject {
|
||||
sendEvent(): void;
|
||||
}
|
||||
56
src/lib/extern/qt.ts
vendored
Normal file
56
src/lib/extern/qt.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
interface Console {
|
||||
__brand: "Console";
|
||||
|
||||
log(...args: any[]): void;
|
||||
assert(assertion: boolean, message?: string): void;
|
||||
}
|
||||
|
||||
interface Qt {
|
||||
__brand: "Qt";
|
||||
|
||||
rect(x: number, y: number, width: number, height: number): QmlRect;
|
||||
createQmlObject(qml: string, parent: QmlObject): QmlObject;
|
||||
}
|
||||
|
||||
interface QmlObject { __brand: "QmlObject" }
|
||||
|
||||
interface QmlPoint {
|
||||
__brand: "QmlPoint";
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface QmlRect {
|
||||
__brand: "QmlRect";
|
||||
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
readonly top: number;
|
||||
readonly bottom: number; // top + height
|
||||
readonly left: number;
|
||||
readonly right: number; // left + width
|
||||
}
|
||||
|
||||
interface QmlSize {
|
||||
__brand: "QmlSize";
|
||||
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface QSignal<T extends unknown[]> {
|
||||
__brand: "QSignal";
|
||||
|
||||
connect(handler: (...args: [...T]) => void): void;
|
||||
disconnect(handler: (...args: [...T]) => void): void;
|
||||
}
|
||||
|
||||
interface QmlTimer extends QmlObject {
|
||||
interval: number;
|
||||
readonly triggered: QSignal<[]>;
|
||||
restart(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
475
src/lib/keyBindings/Actions.ts
Normal file
475
src/lib/keyBindings/Actions.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
class Actions {
|
||||
constructor(
|
||||
private readonly config: Actions.Config,
|
||||
) {}
|
||||
|
||||
public readonly focusLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const leftColumn = grid.getLeftColumn(column);
|
||||
if (leftColumn === null) {
|
||||
return;
|
||||
}
|
||||
leftColumn.getWindowToFocus().focus();
|
||||
};
|
||||
|
||||
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const rightColumn = grid.getRightColumn(column);
|
||||
if (rightColumn === null) {
|
||||
return;
|
||||
}
|
||||
rightColumn.getWindowToFocus().focus();
|
||||
};
|
||||
|
||||
public readonly focusUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const aboveWindow = column.getAboveWindow(window);
|
||||
if (aboveWindow === null) {
|
||||
return;
|
||||
}
|
||||
aboveWindow.focus();
|
||||
};
|
||||
|
||||
public readonly focusDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const belowWindow = column.getBelowWindow(window);
|
||||
if (belowWindow === null) {
|
||||
return;
|
||||
}
|
||||
belowWindow.focus();
|
||||
};
|
||||
|
||||
public readonly focusNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const belowWindow = column.getBelowWindow(window);
|
||||
if (belowWindow !== null) {
|
||||
belowWindow.focus();
|
||||
} else {
|
||||
const rightColumn = grid.getRightColumn(column);
|
||||
if (rightColumn === null) {
|
||||
return;
|
||||
}
|
||||
rightColumn.getFirstWindow().focus();
|
||||
}
|
||||
};
|
||||
|
||||
public readonly focusPrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const aboveWindow = column.getAboveWindow(window);
|
||||
if (aboveWindow !== null) {
|
||||
aboveWindow.focus();
|
||||
} else {
|
||||
const leftColumn = grid.getLeftColumn(column);
|
||||
if (leftColumn === null) {
|
||||
return;
|
||||
}
|
||||
leftColumn.getLastWindow().focus();
|
||||
}
|
||||
};
|
||||
|
||||
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
firstColumn.getWindowToFocus().focus();
|
||||
};
|
||||
|
||||
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
lastColumn.getWindowToFocus().focus();
|
||||
};
|
||||
|
||||
public readonly windowMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const leftColumn = grid.getLeftColumn(column);
|
||||
if (leftColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(leftColumn, true, FocusPassing.Type.None);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, grid.getLeftColumn(column));
|
||||
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom = true) => {
|
||||
if (column.getWindowCount() === 1) {
|
||||
// move from own column into existing column
|
||||
const rightColumn = grid.getRightColumn(column);
|
||||
if (rightColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(rightColumn, bottom, FocusPassing.Type.None);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
} else {
|
||||
// move from shared column into own column
|
||||
const newColumn = new Column(grid, column);
|
||||
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO (optimization): only arrange moved windows
|
||||
public readonly windowMoveUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
column.moveWindowUp(window);
|
||||
};
|
||||
|
||||
// TODO (optimization): only arrange moved windows
|
||||
public readonly windowMoveDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
column.moveWindowDown(window);
|
||||
};
|
||||
|
||||
public readonly windowMoveNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const canMoveDown = window !== column.getLastWindow();
|
||||
if (canMoveDown) {
|
||||
column.moveWindowDown(window);
|
||||
} else {
|
||||
this.windowMoveRight(cm, dm, window, column, grid, false);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly windowMovePrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const canMoveUp = window !== column.getFirstWindow();
|
||||
if (canMoveUp) {
|
||||
column.moveWindowUp(window);
|
||||
} else {
|
||||
this.windowMoveLeft(cm, dm, window, column, grid);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly windowMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const newColumn = new Column(grid, null);
|
||||
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||
};
|
||||
|
||||
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const newColumn = new Column(grid, grid.getLastColumn());
|
||||
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||
};
|
||||
|
||||
public readonly windowToggleFloating = (cm: ClientManager, dm: DesktopManager) => {
|
||||
if (Workspace.activeWindow === null) {
|
||||
return;
|
||||
}
|
||||
cm.toggleFloatingClient(Workspace.activeWindow);
|
||||
};
|
||||
|
||||
public readonly columnMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
grid.moveColumnLeft(column);
|
||||
};
|
||||
|
||||
public readonly columnMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
grid.moveColumnRight(column);
|
||||
};
|
||||
|
||||
public readonly columnMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
grid.moveColumn(column, null);
|
||||
};
|
||||
|
||||
public readonly columnMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
grid.moveColumn(column, grid.getLastColumn());
|
||||
};
|
||||
|
||||
public readonly columnToggleStacked = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
column.toggleStacked();
|
||||
};
|
||||
|
||||
public readonly columnWidthIncrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
this.config.columnResizer.increaseWidth(column);
|
||||
};
|
||||
|
||||
public readonly columnWidthDecrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
this.config.columnResizer.decreaseWidth(column);
|
||||
};
|
||||
|
||||
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
|
||||
column.setWidth(nextWidth, true);
|
||||
};
|
||||
|
||||
public readonly cyclePresetWidthsReverse = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const nextWidth = this.config.presetWidths.prev(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
|
||||
column.setWidth(nextWidth, true);
|
||||
};
|
||||
|
||||
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const visibleRange = desktop.getCurrentVisibleRange();
|
||||
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
|
||||
|
||||
const availableSpace = desktop.tilingArea.width;
|
||||
const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1);
|
||||
const widths = fillSpace(
|
||||
availableSpace - gapsWidth,
|
||||
visibleColumns.map(column => ({ min: column.getMinWidth(), max: column.getMaxWidth() })),
|
||||
);
|
||||
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
|
||||
|
||||
desktop.scrollCenterRange(Range.fromRanges(
|
||||
visibleColumns[0],
|
||||
visibleColumns[visibleColumns.length - 1],
|
||||
));
|
||||
};
|
||||
|
||||
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
||||
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
||||
if (!Range.contains(visibleRange, focusedColumn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
|
||||
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
|
||||
|
||||
const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedVisibleColumns = [targetColumn, ...currentVisibleColumns];
|
||||
while (true) {
|
||||
const success = this.squeezeColumns(wantedVisibleColumns);
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
const removedColumn = wantedVisibleColumns.pop();
|
||||
if (removedColumn === focusedColumn) {
|
||||
break; // don't scroll past the currently focused column
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
||||
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
||||
if (!Range.contains(visibleRange, focusedColumn)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
|
||||
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
|
||||
|
||||
const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wantedVisibleColumns = [...currentVisibleColumns, targetColumn];
|
||||
while (true) {
|
||||
const success = this.squeezeColumns(wantedVisibleColumns);
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
const removedColumn = wantedVisibleColumns.shift();
|
||||
if (removedColumn === focusedColumn) {
|
||||
break; // don't scroll past the currently focused column
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private readonly squeezeColumns = (columns: Column[]) => {
|
||||
const firstColumn = columns[0];
|
||||
const lastColumn = columns[columns.length-1];
|
||||
const grid = firstColumn.grid;
|
||||
const desktop = grid.desktop;
|
||||
|
||||
const availableSpace = desktop.tilingArea.width;
|
||||
const gapsWidth = grid.config.gapsInnerHorizontal * (columns.length-1);
|
||||
const columnConstraints = columns.map(column => ({ min: column.getMinWidth(), max: column.getWidth() }));
|
||||
const minTotalWidth = gapsWidth + columnConstraints.reduce((acc, constraint) => acc + constraint.min, 0);
|
||||
if (minTotalWidth > availableSpace) {
|
||||
// there's nothing we can do
|
||||
return false;
|
||||
}
|
||||
|
||||
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
|
||||
columns.forEach((column, index) => column.setWidth(widths[index], true));
|
||||
desktop.scrollCenterRange(Range.fromRanges(firstColumn, lastColumn));
|
||||
return true;
|
||||
};
|
||||
|
||||
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
|
||||
this.gridScroll(dm, -this.config.manualScrollStep);
|
||||
};
|
||||
|
||||
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
|
||||
this.gridScroll(dm, this.config.manualScrollStep);
|
||||
};
|
||||
|
||||
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
|
||||
const desktop = desktopManager.getCurrentDesktop();
|
||||
if (desktop !== undefined) {
|
||||
desktop.adjustScroll(amount, false);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const firstColumn = grid.getFirstColumn();
|
||||
if (firstColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.desktop.scrollToColumn(firstColumn, false);
|
||||
};
|
||||
|
||||
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const lastColumn = grid.getLastColumn();
|
||||
if (lastColumn === null) {
|
||||
return;
|
||||
}
|
||||
grid.desktop.scrollToColumn(lastColumn, false);
|
||||
};
|
||||
|
||||
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const scrollAmount = Range.minus(column, grid.desktop.getCurrentVisibleRange());
|
||||
if (scrollAmount !== 0) {
|
||||
grid.desktop.adjustScroll(scrollAmount, true);
|
||||
} else {
|
||||
grid.desktop.scrollToColumn(column, true);
|
||||
}
|
||||
};
|
||||
|
||||
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const leftColumn = grid.getLeftColumn(column);
|
||||
if (leftColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.desktop.scrollToColumn(leftColumn, false);
|
||||
};
|
||||
|
||||
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||
if (column === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rightColumn = grid.getRightColumn(column);
|
||||
if (rightColumn === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
grid.desktop.scrollToColumn(rightColumn, false);
|
||||
};
|
||||
|
||||
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
|
||||
dm.selectScreen(Workspace.activeScreen);
|
||||
};
|
||||
|
||||
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
|
||||
const desktop = dm.getCurrentDesktop();
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const grid = desktop.grid;
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
targetColumn.getWindowToFocus().focus();
|
||||
};
|
||||
|
||||
public readonly windowMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null) {
|
||||
return;
|
||||
}
|
||||
window.moveToColumn(targetColumn, true, FocusPassing.Type.None);
|
||||
grid.desktop.autoAdjustScroll();
|
||||
};
|
||||
|
||||
public readonly columnMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||
if (targetColumn === null || targetColumn === column) {
|
||||
return;
|
||||
}
|
||||
if (targetColumn.isToTheRightOf(column)) {
|
||||
grid.moveColumn(column, targetColumn);
|
||||
} else {
|
||||
grid.moveColumn(column, grid.getLeftColumn(targetColumn));
|
||||
}
|
||||
};
|
||||
|
||||
public readonly columnMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
|
||||
const kwinDesktop = Workspace.desktops[desktopIndex];
|
||||
if (kwinDesktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
|
||||
if (newDesktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const newGrid = newDesktop.grid;
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
column.moveToGrid(newGrid, newGrid.getLastColumn());
|
||||
};
|
||||
|
||||
public readonly tailMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
|
||||
const kwinDesktop = Workspace.desktops[desktopIndex];
|
||||
if (kwinDesktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
|
||||
if (newDesktop === undefined) {
|
||||
return;
|
||||
}
|
||||
const newGrid = newDesktop.grid;
|
||||
if (newGrid === null || newGrid === oldGrid) {
|
||||
return;
|
||||
}
|
||||
oldGrid.evacuateTail(newGrid, column);
|
||||
};
|
||||
}
|
||||
|
||||
namespace Actions {
|
||||
export interface Config {
|
||||
manualScrollStep: number;
|
||||
presetWidths: {
|
||||
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
|
||||
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
|
||||
};
|
||||
columnResizer: ColumnResizer;
|
||||
}
|
||||
|
||||
export interface ColumnResizer {
|
||||
increaseWidth(column: Column): void;
|
||||
decreaseWidth(column: Column): void;
|
||||
}
|
||||
}
|
||||
273
src/lib/keyBindings/definition.ts
Normal file
273
src/lib/keyBindings/definition.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
|
||||
return [
|
||||
{
|
||||
name: "window-toggle-floating",
|
||||
description: "Toggle floating",
|
||||
defaultKeySequence: "Meta+Space",
|
||||
action: () => world.do(actions.windowToggleFloating),
|
||||
},
|
||||
{
|
||||
name: "focus-left",
|
||||
description: "Move focus left",
|
||||
defaultKeySequence: "Meta+A",
|
||||
action: () => world.doIfTiledFocused(actions.focusLeft),
|
||||
},
|
||||
{
|
||||
name: "focus-right",
|
||||
description: "Move focus right",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+D",
|
||||
action: () => world.doIfTiledFocused(actions.focusRight),
|
||||
},
|
||||
{
|
||||
name: "focus-up",
|
||||
description: "Move focus up",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+W",
|
||||
action: () => world.doIfTiledFocused(actions.focusUp),
|
||||
},
|
||||
{
|
||||
name: "focus-down",
|
||||
description: "Move focus down",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+S",
|
||||
action: () => world.doIfTiledFocused(actions.focusDown),
|
||||
},
|
||||
{
|
||||
name: "focus-next",
|
||||
description: "Move focus to the next window in grid",
|
||||
action: () => world.doIfTiledFocused(actions.focusNext),
|
||||
},
|
||||
{
|
||||
name: "focus-previous",
|
||||
description: "Move focus to the previous window in grid",
|
||||
action: () => world.doIfTiledFocused(actions.focusPrevious),
|
||||
},
|
||||
{
|
||||
name: "focus-start",
|
||||
description: "Move focus to start",
|
||||
defaultKeySequence: "Meta+Home",
|
||||
action: () => world.do(actions.focusStart),
|
||||
},
|
||||
{
|
||||
name: "focus-end",
|
||||
description: "Move focus to end",
|
||||
defaultKeySequence: "Meta+End",
|
||||
action: () => world.do(actions.focusEnd),
|
||||
},
|
||||
{
|
||||
name: "window-move-left",
|
||||
description: "Move window left",
|
||||
comment: "Moves window out of and into columns",
|
||||
defaultKeySequence: "Meta+Shift+A",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveLeft),
|
||||
},
|
||||
{
|
||||
name: "window-move-right",
|
||||
description: "Move window right",
|
||||
comment: "Moves window out of and into columns",
|
||||
defaultKeySequence: "Meta+Shift+D",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveRight),
|
||||
},
|
||||
{
|
||||
name: "window-move-up",
|
||||
description: "Move window up",
|
||||
defaultKeySequence: "Meta+Shift+W",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveUp),
|
||||
},
|
||||
{
|
||||
name: "window-move-down",
|
||||
description: "Move window down",
|
||||
defaultKeySequence: "Meta+Shift+S",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveDown),
|
||||
},
|
||||
{
|
||||
name: "window-move-next",
|
||||
description: "Move window to the next position in grid",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveNext),
|
||||
},
|
||||
{
|
||||
name: "window-move-previous",
|
||||
description: "Move window to the previous position in grid",
|
||||
action: () => world.doIfTiledFocused(actions.windowMovePrevious),
|
||||
},
|
||||
{
|
||||
name: "window-move-start",
|
||||
description: "Move window to start",
|
||||
defaultKeySequence: "Meta+Shift+Home",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveStart),
|
||||
},
|
||||
{
|
||||
name: "window-move-end",
|
||||
description: "Move window to end",
|
||||
defaultKeySequence: "Meta+Shift+End",
|
||||
action: () => world.doIfTiledFocused(actions.windowMoveEnd),
|
||||
},
|
||||
{
|
||||
name: "column-toggle-stacked",
|
||||
description: "Toggle stacked layout for focused column",
|
||||
comment: "Only the active window visible",
|
||||
defaultKeySequence: "Meta+X",
|
||||
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
|
||||
},
|
||||
{
|
||||
name: "column-move-left",
|
||||
description: "Move column left",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+A",
|
||||
action: () => world.doIfTiledFocused(actions.columnMoveLeft),
|
||||
},
|
||||
{
|
||||
name: "column-move-right",
|
||||
description: "Move column right",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+D",
|
||||
action: () => world.doIfTiledFocused(actions.columnMoveRight),
|
||||
},
|
||||
{
|
||||
name: "column-move-start",
|
||||
description: "Move column to start",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+Home",
|
||||
action: () => world.doIfTiledFocused(actions.columnMoveStart),
|
||||
},
|
||||
{
|
||||
name: "column-move-end",
|
||||
description: "Move column to end",
|
||||
defaultKeySequence: "Meta+Ctrl+Shift+End",
|
||||
action: () => world.doIfTiledFocused(actions.columnMoveEnd),
|
||||
},
|
||||
{
|
||||
name: "column-width-increase",
|
||||
description: "Increase column width",
|
||||
defaultKeySequence: "Meta+Ctrl++",
|
||||
action: () => world.doIfTiledFocused(actions.columnWidthIncrease),
|
||||
},
|
||||
{
|
||||
name: "column-width-decrease",
|
||||
description: "Decrease column width",
|
||||
defaultKeySequence: "Meta+Ctrl+-",
|
||||
action: () => world.doIfTiledFocused(actions.columnWidthDecrease),
|
||||
},
|
||||
{
|
||||
name: "cycle-preset-widths",
|
||||
description: "Cycle through preset column widths",
|
||||
defaultKeySequence: "Meta+R",
|
||||
action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
|
||||
},
|
||||
{
|
||||
name: "cycle-preset-widths-reverse",
|
||||
description: "Cycle through preset column widths in reverse",
|
||||
defaultKeySequence: "Meta+Shift+R",
|
||||
action: () => world.doIfTiledFocused(actions.cyclePresetWidthsReverse),
|
||||
},
|
||||
{
|
||||
name: "columns-width-equalize",
|
||||
description: "Equalize widths of visible columns",
|
||||
defaultKeySequence: "Meta+Ctrl+X",
|
||||
action: () => world.do(actions.columnsWidthEqualize),
|
||||
},
|
||||
{
|
||||
name: "columns-squeeze-left",
|
||||
description: "Squeeze left column onto the screen",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultKeySequence: "Meta+Ctrl+A",
|
||||
action: () => world.doIfTiledFocused(actions.columnsSqueezeLeft),
|
||||
},
|
||||
{
|
||||
name: "columns-squeeze-right",
|
||||
description: "Squeeze right column onto the screen",
|
||||
defaultKeySequence: "Meta+Ctrl+D",
|
||||
action: () => world.doIfTiledFocused(actions.columnsSqueezeRight),
|
||||
},
|
||||
{
|
||||
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: () => world.doIfTiledFocused(actions.gridScrollFocused),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-left-column",
|
||||
description: "Scroll one column to the left",
|
||||
defaultKeySequence: "Meta+Alt+A",
|
||||
action: () => world.do(actions.gridScrollLeftColumn),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-right-column",
|
||||
description: "Scroll one column to the right",
|
||||
defaultKeySequence: "Meta+Alt+D",
|
||||
action: () => world.do(actions.gridScrollRightColumn),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-left",
|
||||
description: "Scroll left",
|
||||
defaultKeySequence: "Meta+Alt+PgUp",
|
||||
action: () => world.do(actions.gridScrollLeft),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-right",
|
||||
description: "Scroll right",
|
||||
defaultKeySequence: "Meta+Alt+PgDown",
|
||||
action: () => world.do(actions.gridScrollRight),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-start",
|
||||
description: "Scroll to start",
|
||||
defaultKeySequence: "Meta+Alt+Home",
|
||||
action: () => world.do(actions.gridScrollStart),
|
||||
},
|
||||
{
|
||||
name: "grid-scroll-end",
|
||||
description: "Scroll to end",
|
||||
defaultKeySequence: "Meta+Alt+End",
|
||||
action: () => world.do(actions.gridScrollEnd),
|
||||
},
|
||||
{
|
||||
name: "screen-switch",
|
||||
description: "Move Karousel grid to the current screen",
|
||||
defaultKeySequence: "Meta+Ctrl+Return",
|
||||
action: () => world.do(actions.screenSwitch),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getNumKeyBindings(world: World, actions: Actions): NumKeyBinding[] {
|
||||
return [
|
||||
{
|
||||
name: "focus-{}",
|
||||
description: "Move focus to column {}",
|
||||
comment: "Clashes with default KDE shortcuts, may require manual remapping",
|
||||
defaultModifiers: "Meta",
|
||||
fKeys: false,
|
||||
action: (i: number) => world.do(actions.focus.partial(i)),
|
||||
},
|
||||
{
|
||||
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: (i: number) => world.doIfTiledFocused(actions.windowMoveToColumn.partial(i)),
|
||||
},
|
||||
{
|
||||
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: (i: number) => world.doIfTiledFocused(actions.columnMoveToColumn.partial(i)),
|
||||
},
|
||||
{
|
||||
name: "column-move-to-desktop-{}",
|
||||
description: "Move column to desktop {}",
|
||||
defaultModifiers: "Meta+Ctrl+Shift",
|
||||
fKeys: true,
|
||||
action: (i: number) => world.doIfTiledFocused(actions.columnMoveToDesktop.partial(i)),
|
||||
},
|
||||
{
|
||||
name: "tail-move-to-desktop-{}",
|
||||
description: "Move this and all following columns to desktop {}",
|
||||
defaultModifiers: "Meta+Ctrl+Shift+Alt",
|
||||
fKeys: true,
|
||||
action: (i: number) => world.doIfTiledFocused(actions.tailMoveToDesktop.partial(i)),
|
||||
},
|
||||
];
|
||||
}
|
||||
68
src/lib/keyBindings/loader.ts
Normal file
68
src/lib/keyBindings/loader.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
interface KeyBinding {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultKeySequence?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
interface NumKeyBinding {
|
||||
name: string;
|
||||
description: string;
|
||||
comment?: string;
|
||||
defaultModifiers: string;
|
||||
fKeys: boolean;
|
||||
action: (i: number) => void;
|
||||
}
|
||||
|
||||
function catchWrap(f: () => void) {
|
||||
return () => {
|
||||
try {
|
||||
f();
|
||||
} catch (error: any) {
|
||||
log(error);
|
||||
log(error.stack);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function registerKeyBinding(shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
|
||||
shortcutActions.push(new ShortcutAction(
|
||||
keyBinding,
|
||||
catchWrap(keyBinding.action),
|
||||
));
|
||||
}
|
||||
|
||||
function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
|
||||
const numPrefix = numKeyBinding.fKeys ? "F" : "";
|
||||
const n = numKeyBinding.fKeys ? 12 : 9;
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const numKey = String(i + 1);
|
||||
const keySequence = i < n ?
|
||||
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
|
||||
"";
|
||||
shortcutActions.push(new ShortcutAction(
|
||||
{
|
||||
name: applyMacro(numKeyBinding.name, numKey),
|
||||
description: applyMacro(numKeyBinding.description, numKey),
|
||||
defaultKeySequence: keySequence,
|
||||
},
|
||||
catchWrap(() => numKeyBinding.action(i)),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
function registerKeyBindings(world: World, config: Actions.Config) {
|
||||
const actions = new Actions(config);
|
||||
const shortcutActions: ShortcutAction[] = [];
|
||||
|
||||
for (const keyBinding of getKeyBindings(world, actions)) {
|
||||
registerKeyBinding(shortcutActions, keyBinding);
|
||||
}
|
||||
|
||||
for (const numKeyBinding of getNumKeyBindings(world, actions)) {
|
||||
registerNumKeyBindings(shortcutActions, numKeyBinding);
|
||||
}
|
||||
|
||||
return shortcutActions;
|
||||
}
|
||||
@@ -5,46 +5,39 @@ class Column {
|
||||
private readonly windows: LinkedList<Window>;
|
||||
private stacked: boolean;
|
||||
private focusTaker: Window|null;
|
||||
private static readonly minWidth = 10;
|
||||
private static readonly minWidth = 40;
|
||||
|
||||
constructor(grid: Grid, prevColumn: Column|null) {
|
||||
constructor(grid: Grid, leftColumn: Column|null) {
|
||||
this.gridX = 0;
|
||||
this.width = 0;
|
||||
this.windows = new LinkedList();
|
||||
this.stacked = grid.config.stackColumnsByDefault;
|
||||
this.focusTaker = null;
|
||||
this.grid = grid;
|
||||
this.grid.onColumnAdded(this, prevColumn);
|
||||
this.grid.onColumnAdded(this, leftColumn);
|
||||
}
|
||||
|
||||
public moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
|
||||
public moveToGrid(targetGrid: Grid, leftColumn: Column|null) {
|
||||
if (targetGrid === this.grid) {
|
||||
this.grid.onColumnMoved(this, prevColumn);
|
||||
this.grid.moveColumn(this, leftColumn);
|
||||
} else {
|
||||
this.grid.onColumnRemoved(this, false);
|
||||
this.grid.onColumnRemoved(this, this.isFocused() ? FocusPassing.Type.Immediate : FocusPassing.Type.None);
|
||||
this.grid = targetGrid;
|
||||
targetGrid.onColumnAdded(this, prevColumn);
|
||||
targetGrid.onColumnAdded(this, leftColumn);
|
||||
for (const window of this.windows.iterator()) {
|
||||
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber;
|
||||
window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public moveAfter(prevColumn: Column|null) {
|
||||
if (prevColumn === this) {
|
||||
return;
|
||||
}
|
||||
this.grid.onColumnMoved(this, prevColumn);
|
||||
}
|
||||
|
||||
public isAfter(other: Column) {
|
||||
return this.gridX > other.gridX;
|
||||
}
|
||||
|
||||
public isBefore(other: Column) {
|
||||
public isToTheLeftOf(other: Column) {
|
||||
return this.gridX < other.gridX;
|
||||
}
|
||||
|
||||
public isToTheRightOf(other: Column) {
|
||||
return this.gridX > other.gridX;
|
||||
}
|
||||
|
||||
public moveWindowUp(window: Window) {
|
||||
this.windows.moveBack(window);
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
@@ -63,11 +56,19 @@ class Column {
|
||||
return this.getWindowCount() === 0;
|
||||
}
|
||||
|
||||
public getPrevWindow(window: Window) {
|
||||
public getFirstWindow(): Window {
|
||||
return this.windows.getFirst()!;
|
||||
}
|
||||
|
||||
public getLastWindow(): Window {
|
||||
return this.windows.getLast()!;
|
||||
}
|
||||
|
||||
public getAboveWindow(window: Window) {
|
||||
return this.windows.getPrev(window);
|
||||
}
|
||||
|
||||
public getNextWindow(window: Window) {
|
||||
public getBelowWindow(window: Window) {
|
||||
return this.windows.getNext(window);
|
||||
}
|
||||
|
||||
@@ -92,16 +93,17 @@ class Column {
|
||||
|
||||
public setWidth(width: number, setPreferred: boolean) {
|
||||
width = clamp(width, this.getMinWidth(), this.getMaxWidth());
|
||||
const oldWidth = this.width;
|
||||
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);
|
||||
}
|
||||
|
||||
public adjustWidth(widthDelta: number, setPreferred: boolean) {
|
||||
@@ -131,6 +133,27 @@ class Column {
|
||||
return this.gridX + this.width;
|
||||
}
|
||||
|
||||
public onUserResizeWidth(
|
||||
startWidth: number,
|
||||
currentDelta: number,
|
||||
resizingLeftSide: boolean,
|
||||
neighbor?: { column: Column, startWidth: number },
|
||||
) {
|
||||
const oldColumnWidth = this.getWidth();
|
||||
this.setWidth(startWidth + currentDelta, true);
|
||||
const actualDelta = this.getWidth() - startWidth;
|
||||
|
||||
let leftEdgeDeltaStep = resizingLeftSide ? oldColumnWidth - this.getWidth() : 0;
|
||||
if (neighbor !== undefined) {
|
||||
const oldNeighborWidth = neighbor.column.getWidth();
|
||||
neighbor.column.setWidth(neighbor.startWidth - actualDelta, true);
|
||||
if (resizingLeftSide) {
|
||||
leftEdgeDeltaStep -= neighbor.column.getWidth() - oldNeighborWidth;
|
||||
}
|
||||
}
|
||||
this.grid.desktop.adjustScroll(-leftEdgeDeltaStep, true);
|
||||
}
|
||||
|
||||
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
|
||||
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
|
||||
if (otherWindow === null) {
|
||||
@@ -172,58 +195,48 @@ class Column {
|
||||
return this.focusTaker;
|
||||
}
|
||||
|
||||
public focus() {
|
||||
const window = this.getFocusTaker() ?? this.windows.getFirst();
|
||||
if (window === null) {
|
||||
return;
|
||||
public getWindowToFocus() {
|
||||
return this.getFocusTaker() ?? this.windows.getFirst()!;
|
||||
}
|
||||
|
||||
public isFocused() {
|
||||
const lastFocusedWindow = this.grid.getLastFocusedWindow();
|
||||
if (lastFocusedWindow === null) {
|
||||
return false;
|
||||
}
|
||||
window.focus();
|
||||
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
|
||||
}
|
||||
|
||||
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;
|
||||
const opacity = Range.contains(visibleRange, this) ? 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()) {
|
||||
if (this.stacked && this.windows.length() >= 2) {
|
||||
this.arrangeStacked(x);
|
||||
return;
|
||||
}
|
||||
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.config.gapsInnerVertical;
|
||||
}
|
||||
}
|
||||
|
||||
public arrangeStacked(x: number) {
|
||||
const expandedWindow = this.getFocusTaker();
|
||||
let collapsedHeight;
|
||||
for (const window of this.windows.iterator()) {
|
||||
if (window === expandedWindow) {
|
||||
window.client.setShade(false);
|
||||
} else {
|
||||
window.client.setShade(true);
|
||||
collapsedHeight = window.client.kwinClient.frameGeometry.height;
|
||||
}
|
||||
}
|
||||
const nWindows = this.windows.length();
|
||||
const windowWidth = this.width - (nWindows - 1) * this.grid.config.stackOffsetX;
|
||||
const windowHeight = this.grid.desktop.tilingArea.height - (nWindows - 1) * this.grid.config.stackOffsetY;
|
||||
|
||||
const nCollapsed = this.getWindowCount() - 1;
|
||||
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
|
||||
let y = this.grid.desktop.tilingArea.y;
|
||||
let windowX = x;
|
||||
let windowY = 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 += this.grid.config.gapsInnerVertical;
|
||||
window.arrange(windowX, windowY, windowWidth, windowHeight);
|
||||
windowX += this.grid.config.stackOffsetX;
|
||||
windowY += this.grid.config.stackOffsetY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,27 +248,13 @@ class Column {
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
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();
|
||||
public onWindowAdded(window: Window, bottom: boolean) {
|
||||
if (bottom) {
|
||||
this.windows.insertEnd(window);
|
||||
} else {
|
||||
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
|
||||
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
|
||||
this.windows.insertStart(window);
|
||||
}
|
||||
}
|
||||
|
||||
public onWindowAdded(window: Window) {
|
||||
this.windows.insertEnd(window);
|
||||
if (this.width === 0) {
|
||||
this.setWidth(window.client.preferredWidth, false);
|
||||
}
|
||||
@@ -270,9 +269,9 @@ class Column {
|
||||
this.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
public onWindowRemoved(window: Window, passFocus: boolean) {
|
||||
public onWindowRemoved(window: Window, passFocus: FocusPassing.Type) {
|
||||
const lastWindow = this.windows.length() === 1;
|
||||
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
|
||||
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
|
||||
|
||||
this.windows.remove(window);
|
||||
|
||||
@@ -285,8 +284,15 @@ class Column {
|
||||
this.destroy(passFocus);
|
||||
} else {
|
||||
this.resizeWindows();
|
||||
if (passFocus && windowToFocus !== null) {
|
||||
windowToFocus.focus();
|
||||
if (windowToFocus !== null) {
|
||||
switch (passFocus) {
|
||||
case FocusPassing.Type.Immediate:
|
||||
windowToFocus.focus();
|
||||
break;
|
||||
case FocusPassing.Type.OnUnfocus:
|
||||
this.grid.focusPasser.request(windowToFocus.client.kwinClient);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,18 +300,18 @@ class Column {
|
||||
}
|
||||
|
||||
public onWindowFocused(window: Window) {
|
||||
this.grid.onColumnFocused(this);
|
||||
this.grid.onColumnFocused(this, window);
|
||||
this.focusTaker = window;
|
||||
}
|
||||
|
||||
public restoreToTiled() {
|
||||
public restoreToTiled(focusedWindow: Window) {
|
||||
const lastFocusedWindow = this.getFocusTaker();
|
||||
if (lastFocusedWindow !== null) {
|
||||
if (lastFocusedWindow !== null && lastFocusedWindow !== focusedWindow) {
|
||||
lastFocusedWindow.restoreToTiled();
|
||||
}
|
||||
}
|
||||
|
||||
private destroy(passFocus: boolean) {
|
||||
private destroy(passFocus: FocusPassing.Type) {
|
||||
this.grid.onColumnRemoved(this, passFocus);
|
||||
}
|
||||
}
|
||||
270
src/lib/layout/Desktop.ts
Normal file
270
src/lib/layout/Desktop.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
class Desktop {
|
||||
public readonly grid: Grid;
|
||||
private scrollX: number;
|
||||
private gestureScrollXInitial: number | null;
|
||||
private dirty: boolean;
|
||||
private dirtyScroll: boolean;
|
||||
private dirtyPins: boolean;
|
||||
public clientArea: QmlRect;
|
||||
public tilingArea: QmlRect;
|
||||
|
||||
constructor(
|
||||
public readonly kwinDesktop: KwinDesktop,
|
||||
private readonly pinManager: PinManager,
|
||||
private readonly config: Desktop.Config,
|
||||
private readonly getScreen: () => Output,
|
||||
layoutConfig: LayoutConfig,
|
||||
focusPasser: FocusPassing.Passer,
|
||||
) {
|
||||
this.scrollX = 0;
|
||||
this.gestureScrollXInitial = null;
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
this.dirtyPins = true;
|
||||
this.grid = new Grid(this, layoutConfig, focusPasser);
|
||||
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
|
||||
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
|
||||
}
|
||||
|
||||
private updateArea() {
|
||||
const newClientArea = Desktop.getClientArea(this.getScreen(), this.kwinDesktop);
|
||||
if (rectEquals(newClientArea, this.clientArea) && !this.dirtyPins) {
|
||||
return;
|
||||
}
|
||||
this.clientArea = newClientArea;
|
||||
this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
|
||||
this.dirty = true;
|
||||
this.dirtyScroll = true;
|
||||
this.dirtyPins = false;
|
||||
this.grid.onScreenSizeChanged();
|
||||
this.autoAdjustScroll();
|
||||
}
|
||||
|
||||
private static getClientArea(screen: Output, kwinDesktop: KwinDesktop) {
|
||||
return Workspace.clientArea(ClientAreaOption.PlacementArea, screen, kwinDesktop);
|
||||
}
|
||||
|
||||
private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
|
||||
const availableSpace = pinManager.getAvailableSpace(kwinDesktop, 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: 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: Range) {
|
||||
const scrollAmount = Range.minus(range, this.getCurrentVisibleRange());
|
||||
this.adjustScroll(scrollAmount, true);
|
||||
}
|
||||
|
||||
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, false);
|
||||
}
|
||||
|
||||
public scrollToColumn(column: Column, force: boolean) {
|
||||
if (force || this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
|
||||
this.config.scroller.scrollToColumn(this, column);
|
||||
}
|
||||
}
|
||||
|
||||
private getVisibleRange(scrollX: number) {
|
||||
return Range.create(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 gestureScroll(amount: number) {
|
||||
if (!this.config.gestureScroll) {
|
||||
return;
|
||||
}
|
||||
if (this.gestureScrollXInitial === null) {
|
||||
this.gestureScrollXInitial = this.scrollX;
|
||||
}
|
||||
|
||||
if (this.config.gestureScrollInvert) {
|
||||
amount = -amount;
|
||||
}
|
||||
this.setScroll(this.gestureScrollXInitial + this.config.gestureScrollStep * amount, false);
|
||||
}
|
||||
|
||||
public gestureScrollFinish() {
|
||||
this.gestureScrollXInitial = null;
|
||||
}
|
||||
|
||||
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 forceArrange() {
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
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 interface Config {
|
||||
marginTop: number;
|
||||
marginBottom: number;
|
||||
marginLeft: number;
|
||||
marginRight: number;
|
||||
gestureScroll: boolean;
|
||||
gestureScrollInvert: boolean;
|
||||
gestureScrollStep: number;
|
||||
scroller: Desktop.Scroller;
|
||||
clamper: Desktop.Clamper;
|
||||
}
|
||||
|
||||
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: 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.getLeftColumn(this.left);
|
||||
let rightColumn = grid.getRightColumn(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.getLeftColumn(leftColumn!);
|
||||
} else {
|
||||
this.addRight(rightColumn!, gap);
|
||||
rightColumn = grid.getRightColumn(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 interface Scroller {
|
||||
scrollToColumn(desktop: Desktop, column: Column): void;
|
||||
}
|
||||
|
||||
export interface Clamper {
|
||||
clampScrollX(desktop: Desktop, x: number): number;
|
||||
}
|
||||
}
|
||||
235
src/lib/layout/Grid.ts
Normal file
235
src/lib/layout/Grid.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
class Grid {
|
||||
public readonly desktop: Desktop;
|
||||
public readonly config: LayoutConfig;
|
||||
public readonly focusPasser: FocusPassing.Passer;
|
||||
private readonly columns: LinkedList<Column>;
|
||||
private lastFocusedColumn: Column|null;
|
||||
private width: number;
|
||||
private userResize: boolean; // is any part of the grid being resized by the user
|
||||
private readonly userResizeFinishedDelayer: Delayer;
|
||||
|
||||
constructor(desktop: Desktop, config: LayoutConfig, focusPasser: FocusPassing.Passer) {
|
||||
this.desktop = desktop;
|
||||
this.config = config;
|
||||
this.focusPasser = focusPasser;
|
||||
this.columns = new LinkedList();
|
||||
this.lastFocusedColumn = null;
|
||||
this.width = 0;
|
||||
this.userResize = false;
|
||||
this.userResizeFinishedDelayer = new Delayer(50, () => {
|
||||
// this delay prevents windows' contents from freezing after resizing
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
this.desktop.arrange();
|
||||
});
|
||||
}
|
||||
|
||||
public moveColumn(column: Column, leftColumn: Column|null) {
|
||||
if (column === leftColumn) {
|
||||
return;
|
||||
}
|
||||
const movedLeft = leftColumn === null ? true : column.isToTheRightOf(leftColumn);
|
||||
const firstMovedColumn = movedLeft ? column : this.getRightColumn(column);
|
||||
this.columns.move(column, leftColumn);
|
||||
this.columnsSetX(firstMovedColumn);
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public moveColumnLeft(column: Column) {
|
||||
this.columns.moveBack(column);
|
||||
this.columnsSetX(column);
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public moveColumnRight(column: Column) {
|
||||
const rightColumn = this.columns.getNext(column);
|
||||
if (rightColumn === null) {
|
||||
return;
|
||||
}
|
||||
this.moveColumnLeft(rightColumn);
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
|
||||
public isUserResizing() {
|
||||
return this.userResize;
|
||||
}
|
||||
|
||||
public getLeftColumn(column: Column) {
|
||||
return this.columns.getPrev(column);
|
||||
}
|
||||
|
||||
public getRightColumn(column: Column) {
|
||||
return this.columns.getNext(column);
|
||||
}
|
||||
|
||||
public getFirstColumn() {
|
||||
return this.columns.getFirst();
|
||||
}
|
||||
|
||||
public getLastColumn() {
|
||||
return this.columns.getLast();
|
||||
}
|
||||
|
||||
public getColumnAtIndex(i: number) {
|
||||
return this.columns.getItemAtIndex(i);
|
||||
}
|
||||
|
||||
public getLastFocusedColumn() {
|
||||
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
|
||||
return null;
|
||||
}
|
||||
return this.lastFocusedColumn;
|
||||
}
|
||||
|
||||
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: Range, fullyVisible: boolean) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
if (Range.contains(visibleRange, column)) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
|
||||
let last = null;
|
||||
for (const column of this.columns.iterator()) {
|
||||
if (Range.contains(visibleRange, column)) {
|
||||
last = column;
|
||||
} else if (last !== null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
if (Range.contains(visibleRange, column)) {
|
||||
yield column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, leftColumn: Column|null) {
|
||||
if (leftColumn === null) {
|
||||
this.columns.insertStart(column);
|
||||
} else {
|
||||
this.columns.insertAfter(column, leftColumn);
|
||||
}
|
||||
this.columnsSetX(column);
|
||||
this.desktop.onLayoutChanged();
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public onColumnRemoved(column: Column, passFocus: FocusPassing.Type) {
|
||||
const isLastColumn = this.columns.length() === 1;
|
||||
const rightColumn = this.getRightColumn(column);
|
||||
const columnToFocus = isLastColumn ? null : this.getLeftColumn(column) ?? rightColumn;
|
||||
if (column === this.lastFocusedColumn) {
|
||||
this.lastFocusedColumn = columnToFocus;
|
||||
}
|
||||
|
||||
this.columns.remove(column);
|
||||
this.columnsSetX(rightColumn);
|
||||
|
||||
this.desktop.onLayoutChanged();
|
||||
if (columnToFocus !== null) {
|
||||
switch (passFocus) {
|
||||
case FocusPassing.Type.Immediate:
|
||||
columnToFocus.getWindowToFocus().focus();
|
||||
return;
|
||||
case FocusPassing.Type.OnUnfocus:
|
||||
this.focusPasser.request(columnToFocus.getWindowToFocus().client.kwinClient);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
|
||||
public onColumnWidthChanged(column: Column) {
|
||||
const rightColumn = this.columns.getNext(column);
|
||||
this.columnsSetX(rightColumn);
|
||||
this.desktop.onLayoutChanged();
|
||||
if (!this.userResize) {
|
||||
this.desktop.autoAdjustScroll();
|
||||
}
|
||||
}
|
||||
|
||||
public onColumnFocused(column: Column, window: Window) {
|
||||
const lastFocusedColumn = this.getLastFocusedColumn();
|
||||
if (lastFocusedColumn !== null) {
|
||||
lastFocusedColumn.restoreToTiled(window);
|
||||
}
|
||||
this.lastFocusedColumn = column;
|
||||
this.desktop.scrollToColumn(column, false);
|
||||
}
|
||||
|
||||
public onScreenSizeChanged() {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.updateWidth();
|
||||
column.resizeWindows();
|
||||
}
|
||||
}
|
||||
|
||||
public onUserResizeStarted() {
|
||||
this.userResize = true;
|
||||
}
|
||||
|
||||
public onUserResizeFinished() {
|
||||
this.userResize = false;
|
||||
this.userResizeFinishedDelayer.run();
|
||||
}
|
||||
|
||||
public evacuateTail(targetGrid: Grid, startColumn: Column) {
|
||||
for (const column of this.columns.iteratorFrom(startColumn)) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public evacuate(targetGrid: Grid) {
|
||||
for (const column of this.columns.iterator()) {
|
||||
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.userResizeFinishedDelayer.destroy();
|
||||
}
|
||||
}
|
||||
14
src/lib/layout/LayoutConfig.ts
Normal file
14
src/lib/layout/LayoutConfig.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
interface LayoutConfig {
|
||||
gapsInnerHorizontal: number;
|
||||
gapsInnerVertical: number;
|
||||
stackOffsetX: number;
|
||||
stackOffsetY: number;
|
||||
offScreenOpacity: number;
|
||||
stackColumnsByDefault: boolean;
|
||||
resizeNeighborColumn: boolean;
|
||||
reMaximize: boolean;
|
||||
skipSwitcher: boolean;
|
||||
tiledKeepBelow: boolean;
|
||||
maximizedKeepAbove: boolean;
|
||||
untileOnDrag: boolean;
|
||||
}
|
||||
47
src/lib/layout/Range.ts
Normal file
47
src/lib/layout/Range.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
interface Range {
|
||||
getLeft(): number;
|
||||
getRight(): number;
|
||||
getWidth(): number;
|
||||
}
|
||||
|
||||
namespace Range {
|
||||
export function create(x: number, width: number) {
|
||||
return new Basic(x, width);
|
||||
}
|
||||
|
||||
export function fromRanges(leftRange: Range, rightRange: Range) {
|
||||
const left = leftRange.getLeft();
|
||||
const right = rightRange.getRight();
|
||||
return new Basic(left, right - left);
|
||||
}
|
||||
|
||||
export function contains(parent: Range, child: Range) {
|
||||
return child.getLeft() >= parent.getLeft() &&
|
||||
child.getRight() <= parent.getRight();
|
||||
}
|
||||
|
||||
export function minus(a: Range, b: Range) {
|
||||
const aCenter = a.getLeft() + a.getWidth() / 2;
|
||||
const bCenter = b.getLeft() + b.getWidth() / 2;
|
||||
return Math.round(aCenter - bCenter);
|
||||
}
|
||||
|
||||
class Basic {
|
||||
constructor(
|
||||
private readonly x: number,
|
||||
private readonly width: number,
|
||||
) {}
|
||||
|
||||
public getLeft() {
|
||||
return this.x;
|
||||
}
|
||||
|
||||
public getRight() {
|
||||
return this.x + this.width;
|
||||
}
|
||||
|
||||
public getWidth() {
|
||||
return this.width;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,23 +8,28 @@ class Window {
|
||||
constructor(client: ClientWrapper, column: Column) {
|
||||
this.client = client;
|
||||
this.height = client.kwinClient.frameGeometry.height;
|
||||
|
||||
let maximizedMode = this.client.getMaximizedMode();
|
||||
if (maximizedMode === undefined) {
|
||||
maximizedMode = MaximizedMode.Unmaximized; // defaulting to unmaximized, as this is set in Tiled.prepareClientForTiling
|
||||
}
|
||||
this.focusedState = {
|
||||
fullScreen: false,
|
||||
maximizedHorizontally: false,
|
||||
maximizedVertically: false,
|
||||
fullScreen: this.client.kwinClient.fullScreen,
|
||||
maximizedMode: maximizedMode,
|
||||
};
|
||||
this.skipArrange = false;
|
||||
|
||||
this.skipArrange = this.client.kwinClient.fullScreen || maximizedMode !== MaximizedMode.Unmaximized;
|
||||
this.column = column;
|
||||
column.onWindowAdded(this);
|
||||
column.onWindowAdded(this, true);
|
||||
}
|
||||
|
||||
public moveToColumn(targetColumn: Column) {
|
||||
public moveToColumn(targetColumn: Column, bottom: boolean, passFocus: FocusPassing.Type) {
|
||||
if (targetColumn === this.column) {
|
||||
return;
|
||||
}
|
||||
this.column.onWindowRemoved(this, false);
|
||||
this.column.onWindowRemoved(this, passFocus);
|
||||
this.column = targetColumn;
|
||||
targetColumn.onWindowAdded(this);
|
||||
targetColumn.onWindowAdded(this, bottom);
|
||||
}
|
||||
|
||||
public arrange(x: number, y: number, width: number, height: number) {
|
||||
@@ -37,8 +42,11 @@ class Window {
|
||||
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)
|
||||
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
|
||||
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
|
||||
if (this.focusedState.maximizedMode !== MaximizedMode.Unmaximized) {
|
||||
this.client.setMaximize(
|
||||
this.focusedState.maximizedMode === MaximizedMode.Horizontally || this.focusedState.maximizedMode === MaximizedMode.Maximized,
|
||||
this.focusedState.maximizedMode === MaximizedMode.Vertically || this.focusedState.maximizedMode === MaximizedMode.Maximized,
|
||||
);
|
||||
maximized = true;
|
||||
}
|
||||
if (this.focusedState.fullScreen) {
|
||||
@@ -52,11 +60,12 @@ class Window {
|
||||
}
|
||||
|
||||
public focus() {
|
||||
if (this.client.isShaded()) {
|
||||
// workaround for KWin deactivating clients when unshading immediately after activation
|
||||
this.client.setShade(false);
|
||||
}
|
||||
this.client.focus();
|
||||
const kwinClient = this.client.kwinClient;
|
||||
if (!this.isFocused()) {
|
||||
// in some situations focus assignment just doesn't work, let's do it later
|
||||
this.column.grid.focusPasser.request(kwinClient);
|
||||
}
|
||||
}
|
||||
|
||||
public isFocused() {
|
||||
@@ -64,6 +73,14 @@ class Window {
|
||||
}
|
||||
|
||||
public onFocused() {
|
||||
if (this.column.grid.config.reMaximize && (
|
||||
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
|
||||
this.focusedState.fullScreen
|
||||
)) {
|
||||
// We need to maximize/fullscreen this window, but we can't do it here.
|
||||
// We need to do it in `arrange` to ensure it happens after placement.
|
||||
this.column.grid.desktop.forceArrange();
|
||||
}
|
||||
this.column.onWindowFocused(this);
|
||||
}
|
||||
|
||||
@@ -73,11 +90,10 @@ class Window {
|
||||
}
|
||||
this.client.setFullScreen(false);
|
||||
this.client.setMaximize(false, false);
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
|
||||
const maximized = horizontally || vertically;
|
||||
public onMaximizedChanged(maximizedMode: MaximizedMode) {
|
||||
const maximized = maximizedMode !== MaximizedMode.Unmaximized;
|
||||
this.skipArrange = maximized;
|
||||
if (this.column.grid.config.tiledKeepBelow) {
|
||||
this.client.kwinClient.keepBelow = !maximized;
|
||||
@@ -86,8 +102,7 @@ class Window {
|
||||
this.client.kwinClient.keepAbove = maximized;
|
||||
}
|
||||
if (this.isFocused()) {
|
||||
this.focusedState.maximizedHorizontally = horizontally;
|
||||
this.focusedState.maximizedVertically = vertically;
|
||||
this.focusedState.maximizedMode = maximizedMode;
|
||||
}
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
@@ -106,46 +121,20 @@ class Window {
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public onFrameGeometryChanged() {
|
||||
const newGeometry = this.client.kwinClient.frameGeometry;
|
||||
this.column.setWidth(newGeometry.width, true);
|
||||
this.column.grid.desktop.onLayoutChanged();
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
this.column.onWindowRemoved(this, passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Window {
|
||||
export type State = {
|
||||
fullScreen: boolean,
|
||||
maximizedHorizontally: boolean,
|
||||
maximizedVertically: boolean,
|
||||
export interface State {
|
||||
fullScreen: boolean;
|
||||
maximizedMode: MaximizedMode;
|
||||
}
|
||||
}
|
||||
19
src/lib/rules/ClientMatcher.ts
Normal file
19
src/lib/rules/ClientMatcher.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
class ClientMatcher {
|
||||
private readonly regex: RegExp;
|
||||
|
||||
constructor(regex: RegExp) {
|
||||
this.regex = regex;
|
||||
}
|
||||
|
||||
public matches(kwinClient: KwinClient) {
|
||||
return this.regex.test(ClientMatcher.getClientString(kwinClient));
|
||||
}
|
||||
|
||||
public static getClientString(kwinClient: KwinClient) {
|
||||
return ClientMatcher.getRuleString(kwinClient.resourceClass, kwinClient.caption);
|
||||
}
|
||||
|
||||
public static getRuleString(ruleClass: string, ruleCaption: string) {
|
||||
return ruleClass + "\0" + ruleCaption;
|
||||
}
|
||||
}
|
||||
30
src/lib/rules/DesktopFilter.ts
Normal file
30
src/lib/rules/DesktopFilter.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
class DesktopFilter {
|
||||
private readonly desktopRegex: RegExp | null; // null means all desktops
|
||||
|
||||
constructor(desktopsConfig: string) {
|
||||
this.desktopRegex = DesktopFilter.parseDesktopConfig(desktopsConfig);
|
||||
}
|
||||
|
||||
public shouldWorkOnDesktop(kwinDesktop: KwinDesktop): boolean {
|
||||
if (this.desktopRegex === null) {
|
||||
return true; // Work on all desktops
|
||||
}
|
||||
return this.desktopRegex.test(kwinDesktop.name);
|
||||
}
|
||||
|
||||
private static parseDesktopConfig(config: string): RegExp | null {
|
||||
const trimmed = config.trim();
|
||||
|
||||
if (trimmed.length === 0) {
|
||||
return null; // Empty config means work on all desktops
|
||||
}
|
||||
|
||||
try {
|
||||
return new RegExp(`^${trimmed}$`);
|
||||
} catch (e) {
|
||||
notificationInvalidTiledDesktops.sendEvent();
|
||||
log(`Invalid regex pattern in tiledDesktops config: ${trimmed}. Working on all desktops.`);
|
||||
return null; // Invalid regex means work on all desktops as fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/lib/rules/WindowRule.ts
Normal file
5
src/lib/rules/WindowRule.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
interface WindowRule {
|
||||
class: string | undefined;
|
||||
caption: string | undefined;
|
||||
tile: boolean;
|
||||
}
|
||||
95
src/lib/rules/WindowRuleEnforcer.ts
Normal file
95
src/lib/rules/WindowRuleEnforcer.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
class WindowRuleEnforcer {
|
||||
private readonly preferFloating: ClientMatcher;
|
||||
private readonly preferTiling: ClientMatcher;
|
||||
private readonly followCaption: RegExp;
|
||||
|
||||
constructor(windowRules: WindowRule[]) {
|
||||
const [floatRegex, tileRegex, followCaptionRegex] = WindowRuleEnforcer.createWindowRuleRegexes(windowRules);
|
||||
this.preferFloating = new ClientMatcher(floatRegex);
|
||||
this.preferTiling = new ClientMatcher(tileRegex);
|
||||
this.followCaption = followCaptionRegex;
|
||||
}
|
||||
|
||||
public shouldTile(kwinClient: KwinClient) {
|
||||
return this.preferTiling.matches(kwinClient) || (
|
||||
kwinClient.normalWindow &&
|
||||
!kwinClient.transient &&
|
||||
kwinClient.managed &&
|
||||
kwinClient.pid > -1 &&
|
||||
!kwinClient.fullScreen &&
|
||||
!Clients.isFullScreenGeometry(kwinClient) &&
|
||||
!this.preferFloating.matches(kwinClient)
|
||||
);
|
||||
}
|
||||
|
||||
public initClientSignalManager(world: World, kwinClient: KwinClient) {
|
||||
if (!this.followCaption.test(kwinClient.resourceClass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const enforcer = this;
|
||||
const manager = new SignalManager();
|
||||
manager.connect(kwinClient.captionChanged, () => {
|
||||
const shouldTile = Clients.canTileNow(kwinClient) && enforcer.shouldTile(kwinClient);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(kwinClient);
|
||||
if (shouldTile && desktop !== undefined) {
|
||||
clientManager.tileKwinClient(kwinClient, desktop.grid);
|
||||
} else {
|
||||
clientManager.floatKwinClient(kwinClient);
|
||||
}
|
||||
});
|
||||
});
|
||||
return manager;
|
||||
}
|
||||
|
||||
private static createWindowRuleRegexes(windowRules: WindowRule[]) {
|
||||
const floatRegexes: string[] = [];
|
||||
const tileRegexes: string[] = [];
|
||||
const followCaptionRegexes: string[] = [];
|
||||
for (const windowRule of windowRules) {
|
||||
const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
|
||||
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
|
||||
const ruleString = ClientMatcher.getRuleString(
|
||||
WindowRuleEnforcer.wrapParens(ruleClass),
|
||||
WindowRuleEnforcer.wrapParens(ruleCaption),
|
||||
);
|
||||
|
||||
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
|
||||
if (ruleCaption !== ".*") {
|
||||
followCaptionRegexes.push(ruleClass);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
WindowRuleEnforcer.joinRegexes(floatRegexes),
|
||||
WindowRuleEnforcer.joinRegexes(tileRegexes),
|
||||
WindowRuleEnforcer.joinRegexes(followCaptionRegexes),
|
||||
];
|
||||
}
|
||||
|
||||
private static parseRegex(rawRule: string | undefined) {
|
||||
if (rawRule === undefined || rawRule === "" || rawRule === ".*") {
|
||||
return ".*";
|
||||
} else {
|
||||
return rawRule;
|
||||
}
|
||||
}
|
||||
|
||||
private static joinRegexes(regexes: string[]) {
|
||||
if (regexes.length === 0) {
|
||||
return new RegExp("a^"); // match nothing
|
||||
}
|
||||
|
||||
if (regexes.length === 1) {
|
||||
return new RegExp("^(" + regexes[0] + ")$");
|
||||
}
|
||||
|
||||
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
|
||||
return new RegExp("^(" + joinedRegexes + ")$");
|
||||
}
|
||||
|
||||
private static wrapParens(str: string) {
|
||||
return "(" + str + ")";
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,8 @@ class Delayer {
|
||||
|
||||
function initQmlTimer() {
|
||||
return Qt.createQmlObject(
|
||||
`import QtQuick 2.15
|
||||
`import QtQuick 6.0
|
||||
Timer {}`,
|
||||
qmlBase
|
||||
);
|
||||
qmlBase,
|
||||
) as QmlTimer;
|
||||
}
|
||||
24
src/lib/utils/RateLimiter.ts
Normal file
24
src/lib/utils/RateLimiter.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
class RateLimiter {
|
||||
private i = 0;
|
||||
private intervalStart = 0;
|
||||
|
||||
constructor(
|
||||
private readonly n: number,
|
||||
private readonly intervalMs: number,
|
||||
) {}
|
||||
|
||||
public acquire() {
|
||||
const now = Date.now();
|
||||
if (now - this.intervalStart >= this.intervalMs) {
|
||||
this.i = 0;
|
||||
this.intervalStart = now;
|
||||
}
|
||||
|
||||
if (this.i < this.n) {
|
||||
this.i++;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/lib/utils/ShortcutAction.ts
Normal file
37
src/lib/utils/ShortcutAction.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
class ShortcutAction {
|
||||
private readonly shortcutHandler: ShortcutHandler;
|
||||
|
||||
constructor(keyBinding: ShortcutAction.KeyBinding, f: () => void) {
|
||||
this.shortcutHandler = ShortcutAction.initShortcutHandler(keyBinding);
|
||||
this.shortcutHandler.activated.connect(f);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.shortcutHandler.destroy();
|
||||
}
|
||||
|
||||
private static initShortcutHandler(keyBinding: ShortcutAction.KeyBinding) {
|
||||
const sequenceLine = keyBinding.defaultKeySequence !== undefined ?
|
||||
` sequence: "${keyBinding.defaultKeySequence}";
|
||||
` :
|
||||
"";
|
||||
|
||||
return Qt.createQmlObject(
|
||||
`import QtQuick 6.0
|
||||
import org.kde.kwin 3.0
|
||||
ShortcutHandler {
|
||||
name: "karousel-${keyBinding.name}";
|
||||
text: "Karousel: ${keyBinding.description}";
|
||||
${sequenceLine}}`,
|
||||
qmlBase,
|
||||
) as ShortcutHandler;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ShortcutAction {
|
||||
export interface KeyBinding {
|
||||
name: string;
|
||||
description: string;
|
||||
defaultKeySequence?: string;
|
||||
}
|
||||
}
|
||||
39
src/lib/utils/collections.ts
Normal file
39
src/lib/utils/collections.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
function union<T>(array0: T[], array1: T[]) {
|
||||
const set = new Set([...array0, ...array1]);
|
||||
return [...set];
|
||||
}
|
||||
|
||||
function uniq(sortedArray: any[]) {
|
||||
const filtered = [];
|
||||
let lastItem;
|
||||
for (const item of sortedArray) {
|
||||
if (item !== lastItem) {
|
||||
filtered.push(item);
|
||||
lastItem = item;
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function mapGetOrInit<K, V>(map: Map<K, V>, key: K, defaultItem: V) {
|
||||
const item = map.get(key);
|
||||
if (item !== undefined) {
|
||||
return item;
|
||||
} else {
|
||||
map.set(key, defaultItem);
|
||||
return defaultItem;
|
||||
}
|
||||
}
|
||||
|
||||
function findMinPositive<T>(items: T[], evaluate: (item: T) => number) {
|
||||
let bestScore = Infinity;
|
||||
let bestItem = undefined;
|
||||
for (const item of items) {
|
||||
const score = evaluate(item);
|
||||
if (score > 0 && score < bestScore) {
|
||||
bestScore = score;
|
||||
bestItem = item;
|
||||
}
|
||||
}
|
||||
return bestItem;
|
||||
}
|
||||
98
src/lib/utils/fillSpace.ts
Normal file
98
src/lib/utils/fillSpace.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
function fillSpace(availableSpace: number, items: { min: number, max: number }[]) {
|
||||
if (items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const middleSize = findMiddleSize(availableSpace, items);
|
||||
const sizes = items.map(item => clamp(middleSize, item.min, item.max));
|
||||
if (middleSize !== Math.floor(availableSpace / items.length)) {
|
||||
distributeRemainder(availableSpace, middleSize, sizes, items);
|
||||
}
|
||||
return sizes;
|
||||
|
||||
function findMiddleSize(availableSpace: number, items: { min: number, max: number }[]) {
|
||||
const ranges = buildRanges(items);
|
||||
let requiredSpace = items.reduce((acc, item) => acc + item.min, 0);
|
||||
for (const range of ranges) {
|
||||
const rangeSize = range.end - range.start;
|
||||
const maxRequiredSpaceDelta = rangeSize * range.n;
|
||||
if (requiredSpace + maxRequiredSpaceDelta >= availableSpace) {
|
||||
const positionInRange = (availableSpace - requiredSpace) / maxRequiredSpaceDelta;
|
||||
return Math.floor(range.start + rangeSize * positionInRange);
|
||||
}
|
||||
requiredSpace += maxRequiredSpaceDelta;
|
||||
}
|
||||
return ranges[ranges.length-1].end;
|
||||
}
|
||||
|
||||
function buildRanges(items: { min: number, max: number }[]) {
|
||||
const fenceposts = extractFenceposts(items);
|
||||
if (fenceposts.length === 1) {
|
||||
return [{
|
||||
start: fenceposts[0].value,
|
||||
end: fenceposts[0].value,
|
||||
n: items.length,
|
||||
}];
|
||||
}
|
||||
|
||||
const ranges: Range[] = [];
|
||||
let n = 0;
|
||||
for (let i = 1; i < fenceposts.length; i++) {
|
||||
const startFencepost = fenceposts[i-1];
|
||||
const endFencepost = fenceposts[i];
|
||||
n = n - startFencepost.nMax + startFencepost.nMin;
|
||||
ranges.push({
|
||||
start: startFencepost.value,
|
||||
end: endFencepost.value,
|
||||
n: n,
|
||||
});
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
function extractFenceposts(items: { min: number, max: number }[]) {
|
||||
const fenceposts = new Map<number, Fencepost>();
|
||||
for (const item of items) {
|
||||
mapGetOrInit(fenceposts, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
|
||||
mapGetOrInit(fenceposts, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
|
||||
}
|
||||
|
||||
const array = Array.from(fenceposts.values());
|
||||
array.sort((a, b) => a.value - b.value);
|
||||
return array;
|
||||
}
|
||||
|
||||
function distributeRemainder(availableSpace: number, middleSize: number, sizes: number[], constraints: { max: number }[]) {
|
||||
const indexes = Array.from(sizes.keys())
|
||||
.filter(i => sizes[i] === middleSize);
|
||||
indexes.sort((a, b) => constraints[a].max - constraints[b].max);
|
||||
|
||||
const requiredSpace = sum(...sizes);
|
||||
let remainder = availableSpace - requiredSpace;
|
||||
let n = indexes.length;
|
||||
for (const i of indexes) {
|
||||
if (remainder <= 0) {
|
||||
break;
|
||||
}
|
||||
const enlargable = constraints[i].max - sizes[i];
|
||||
if (enlargable > 0) {
|
||||
const enlarge = Math.min(enlargable, Math.ceil(remainder / n));
|
||||
sizes[i] += enlarge;
|
||||
remainder -= enlarge;
|
||||
}
|
||||
n--;
|
||||
}
|
||||
}
|
||||
|
||||
interface Range {
|
||||
start: number,
|
||||
end: number,
|
||||
n: number,
|
||||
}
|
||||
|
||||
interface Fencepost {
|
||||
value: number,
|
||||
nMin: number,
|
||||
nMax: number,
|
||||
}
|
||||
}
|
||||
10
src/lib/utils/functions.ts
Normal file
10
src/lib/utils/functions.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
interface Function {
|
||||
partial<H extends any[], T extends any[], R>(
|
||||
this: (...args: [...H, ...T]) => R,
|
||||
...head: H
|
||||
) : (...tail: T) => R;
|
||||
}
|
||||
|
||||
Function.prototype.partial = function<H extends any[], T extends any[]>(...head: H) {
|
||||
return (...tail: T) => this(...head, ...tail);
|
||||
};
|
||||
32
src/lib/utils/math.ts
Normal file
32
src/lib/utils/math.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sum(...list: number[]) {
|
||||
return list.reduce((acc, val) => acc + val);
|
||||
}
|
||||
|
||||
function rectEquals(a: QmlRect, b: QmlRect) {
|
||||
return a.x === b.x &&
|
||||
a.y === b.y &&
|
||||
a.width === b.width &&
|
||||
a.height === b.height;
|
||||
}
|
||||
|
||||
function pointEquals(a: QmlPoint, b: QmlPoint) {
|
||||
return a.x === b.x &&
|
||||
a.y === b.y;
|
||||
}
|
||||
|
||||
function rectContainsPoint(rect: QmlRect, point: QmlPoint) {
|
||||
return rect.left <= point.x &&
|
||||
rect.right >= point.x &&
|
||||
rect.top <= point.y &&
|
||||
rect.bottom >= point.y;
|
||||
}
|
||||
3
src/lib/utils/strings.ts
Normal file
3
src/lib/utils/strings.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
function applyMacro(base: string, value: string) {
|
||||
return base.replace("{}", String(value));
|
||||
}
|
||||
58
src/lib/workspace.ts
Normal file
58
src/lib/workspace.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
function initWorkspaceSignalHandlers(world: World, focusPasser: FocusPassing.Passer) {
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.addClient(kwinClient);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.removeClient(kwinClient, FocusPassing.Type.Immediate);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
|
||||
if (kwinClient === null) {
|
||||
focusPasser.activate();
|
||||
} else {
|
||||
focusPasser.clearIfDifferent(kwinClient);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.onClientFocused(kwinClient);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(Workspace.currentDesktopChanged, () => {
|
||||
world.do(() => {}); // re-arrange desktop
|
||||
});
|
||||
|
||||
manager.connect(Workspace.currentActivityChanged, () => {
|
||||
world.do(() => {}); // re-arrange desktop
|
||||
});
|
||||
|
||||
manager.connect(Workspace.screensChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
desktopManager.selectScreen(Workspace.activeScreen);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(Workspace.activitiesChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
desktopManager.updateActivities();
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(Workspace.desktopsChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
desktopManager.updateDesktops();
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(Workspace.virtualScreenSizeChanged, () => {
|
||||
world.onScreenResized();
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
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) {
|
||||
constructor(
|
||||
config: Config,
|
||||
private readonly world: World,
|
||||
private readonly desktopManager: DesktopManager,
|
||||
private readonly pinManager: PinManager,
|
||||
) {
|
||||
this.world = world;
|
||||
this.config = { keepAbove: config.floatingKeepAbove };
|
||||
this.config = config;
|
||||
this.desktopManager = desktopManager;
|
||||
this.pinManager = pinManager;
|
||||
this.clientMap = new Map();
|
||||
@@ -29,11 +31,17 @@ class ClientManager {
|
||||
console.assert(!this.hasClient(kwinClient));
|
||||
|
||||
let constructState: (client: ClientWrapper) => ClientState.State;
|
||||
let desktop: Desktop | undefined;
|
||||
if (kwinClient.dock) {
|
||||
constructState = () => new ClientState.Docked(this.world, kwinClient);
|
||||
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
|
||||
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
|
||||
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, grid);
|
||||
} else if (
|
||||
Clients.canTileEver(kwinClient) &&
|
||||
this.windowRuleEnforcer.shouldTile(kwinClient) &&
|
||||
(desktop = this.desktopManager.getDesktopForClient(kwinClient)) !== undefined
|
||||
) {
|
||||
Clients.makeTileable(kwinClient);
|
||||
console.assert(Clients.canTileNow(kwinClient));
|
||||
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
|
||||
} else {
|
||||
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
|
||||
}
|
||||
@@ -47,18 +55,21 @@ class ClientManager {
|
||||
this.clientMap.set(kwinClient, client);
|
||||
}
|
||||
|
||||
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
|
||||
public removeClient(kwinClient: KwinClient, passFocus: FocusPassing.Type) {
|
||||
console.assert(this.hasClient(kwinClient));
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
|
||||
if (kwinClient !== this.lastFocusedClient) {
|
||||
passFocus = FocusPassing.Type.None;
|
||||
}
|
||||
client.destroy(passFocus);
|
||||
this.clientMap.delete(kwinClient);
|
||||
}
|
||||
|
||||
private findTransientFor(kwinClient: KwinClient) {
|
||||
if (!kwinClient.transient) {
|
||||
if (!kwinClient.transient || kwinClient.transientFor === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -76,41 +87,42 @@ class ClientManager {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||
client.stateManager.setState(() => new ClientState.TiledMinimized(), kwinClient === this.lastFocusedClient);
|
||||
const passFocus = kwinClient === this.lastFocusedClient ? FocusPassing.Type.Immediate : FocusPassing.Type.None;
|
||||
client.stateManager.setState(
|
||||
() => new ClientState.TiledMinimized(this.world, client),
|
||||
passFocus,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public unminimizeClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
if (client.stateManager.getState() instanceof ClientState.TiledMinimized) {
|
||||
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
|
||||
}
|
||||
}
|
||||
|
||||
public tileClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
public tileClient(client: ClientWrapper, grid: Grid) {
|
||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||
return;
|
||||
}
|
||||
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), FocusPassing.Type.None);
|
||||
}
|
||||
|
||||
public untileClient(kwinClient: KwinClient) {
|
||||
public floatClient(client: ClientWrapper) {
|
||||
if (client.stateManager.getState() instanceof ClientState.Floating) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
|
||||
}
|
||||
|
||||
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
|
||||
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);
|
||||
this.tileClient(client, grid);
|
||||
}
|
||||
|
||||
public floatKwinClient(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
this.floatClient(client);
|
||||
}
|
||||
|
||||
public pinClient(kwinClient: KwinClient) {
|
||||
@@ -118,7 +130,12 @@ class ClientManager {
|
||||
if (client === undefined) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
|
||||
if (client.getMaximizedMode() !== MaximizedMode.Unmaximized) {
|
||||
// the client is not really kwin-tiled, just maximized
|
||||
kwinClient.tile = null;
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), FocusPassing.Type.None);
|
||||
this.pinManager.addClient(kwinClient);
|
||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
@@ -131,7 +148,7 @@ class ClientManager {
|
||||
return;
|
||||
}
|
||||
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), FocusPassing.Type.None);
|
||||
this.pinManager.removeClient(kwinClient);
|
||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
@@ -147,10 +164,13 @@ class ClientManager {
|
||||
const clientState = client.stateManager.getState();
|
||||
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
|
||||
Clients.makeTileable(kwinClient);
|
||||
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
|
||||
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop === undefined) {
|
||||
return;
|
||||
}
|
||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), FocusPassing.Type.None);
|
||||
} else if (clientState instanceof ClientState.Tiled) {
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
|
||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,27 +180,27 @@ class ClientManager {
|
||||
|
||||
public onClientFocused(kwinClient: KwinClient) {
|
||||
this.lastFocusedClient = kwinClient;
|
||||
const window = this.findTiledWindow(kwinClient, true);
|
||||
const window = this.findTiledWindow(kwinClient);
|
||||
if (window !== null) {
|
||||
window.onFocused();
|
||||
}
|
||||
}
|
||||
|
||||
public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
|
||||
public findTiledWindow(kwinClient: KwinClient) {
|
||||
const client = this.clientMap.get(kwinClient);
|
||||
if (client === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.findTiledWindowOfClient(client, followTransient);
|
||||
return this.findTiledWindowOfClient(client);
|
||||
}
|
||||
|
||||
private findTiledWindowOfClient(client: ClientWrapper, followTransient: boolean): Window|null {
|
||||
private findTiledWindowOfClient(client: ClientWrapper): 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 if (client.transientFor !== null) {
|
||||
return this.findTiledWindowOfClient(client.transientFor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -188,7 +208,7 @@ class ClientManager {
|
||||
|
||||
private removeAllClients() {
|
||||
for (const kwinClient of Array.from(this.clientMap.keys())) {
|
||||
this.removeClient(kwinClient, false);
|
||||
this.removeClient(kwinClient, FocusPassing.Type.None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +218,7 @@ class ClientManager {
|
||||
}
|
||||
|
||||
namespace ClientManager {
|
||||
export type Config = {
|
||||
keepAbove: boolean,
|
||||
export interface Config {
|
||||
floatingKeepAbove: boolean;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
class ClientWrapper {
|
||||
public readonly kwinClient: KwinClient;
|
||||
public readonly stateManager: ClientState.Manager;
|
||||
public transientFor: ClientWrapper | null;
|
||||
private readonly transients: ClientWrapper[];
|
||||
private readonly rulesSignalManager: SignalManager | null;
|
||||
private readonly signalManager: SignalManager;
|
||||
public preferredWidth: number;
|
||||
private maximizedMode: MaximizedMode | undefined;
|
||||
private readonly manipulatingGeometry: Doer;
|
||||
private lastPlacement: QmlRect | null; // workaround for issue #19
|
||||
|
||||
constructor(
|
||||
kwinClient: KwinClient,
|
||||
public readonly kwinClient: KwinClient,
|
||||
constructInitialState: (client: ClientWrapper) => ClientState.State,
|
||||
transientFor: ClientWrapper | null,
|
||||
rulesSignalManager: SignalManager | null,
|
||||
public transientFor: ClientWrapper | null,
|
||||
private readonly rulesSignalManager: SignalManager | null,
|
||||
) {
|
||||
this.kwinClient = kwinClient;
|
||||
this.transientFor = transientFor;
|
||||
@@ -20,6 +19,7 @@ class ClientWrapper {
|
||||
if (transientFor !== null) {
|
||||
transientFor.addTransient(this);
|
||||
}
|
||||
this.signalManager = ClientWrapper.initSignalManager(this);
|
||||
this.rulesSignalManager = rulesSignalManager;
|
||||
this.preferredWidth = kwinClient.frameGeometry.width;
|
||||
this.manipulatingGeometry = new Doer();
|
||||
@@ -35,12 +35,18 @@ class ClientWrapper {
|
||||
}
|
||||
this.lastPlacement = Qt.rect(x, y, width, height);
|
||||
this.kwinClient.frameGeometry = this.lastPlacement;
|
||||
if (this.kwinClient.frameGeometry !== this.lastPlacement) {
|
||||
// frameGeometry assignment failed. This sometimes happens on Wayland
|
||||
// when a window is off-screen, effectively making it stuck there.
|
||||
this.kwinClient.frameGeometry.x = x; // This makes it unstuck.
|
||||
this.kwinClient.frameGeometry = this.lastPlacement;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private moveTransient(dx: number, dy: number, desktopNumber: number) {
|
||||
private moveTransient(dx: number, dy: number, kwinDesktops: KwinDesktop[]) {
|
||||
if (this.stateManager.getState() instanceof ClientState.Floating) {
|
||||
if (this.kwinClient.desktop === desktopNumber) {
|
||||
if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
|
||||
const frame = this.kwinClient.frameGeometry;
|
||||
this.kwinClient.frameGeometry = Qt.rect(
|
||||
frame.x + dx,
|
||||
@@ -51,45 +57,60 @@ class ClientWrapper {
|
||||
}
|
||||
|
||||
for (const transient of this.transients) {
|
||||
transient.moveTransient(dx, dy, desktopNumber);
|
||||
transient.moveTransient(dx, dy, kwinDesktops);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public moveTransients(dx: number, dy: number) {
|
||||
for (const transient of this.transients) {
|
||||
transient.moveTransient(dx, dy, this.kwinClient.desktop);
|
||||
transient.moveTransient(dx, dy, this.kwinClient.desktops);
|
||||
}
|
||||
}
|
||||
|
||||
public focus() {
|
||||
workspace.activeClient = this.kwinClient;
|
||||
Workspace.activeWindow = this.kwinClient;
|
||||
}
|
||||
|
||||
public isFocused() {
|
||||
return workspace.activeClient === this.kwinClient;
|
||||
return Workspace.activeWindow === this.kwinClient;
|
||||
}
|
||||
|
||||
public setMaximize(horizontally: boolean, vertically: boolean) {
|
||||
if (!this.kwinClient.maximizable) {
|
||||
this.maximizedMode = MaximizedMode.Unmaximized;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.maximizedMode === undefined) {
|
||||
if (horizontally && vertically) {
|
||||
this.maximizedMode = MaximizedMode.Maximized;
|
||||
} else if (horizontally) {
|
||||
this.maximizedMode = MaximizedMode.Horizontally;
|
||||
} else if (vertically) {
|
||||
this.maximizedMode = MaximizedMode.Vertically;
|
||||
} else {
|
||||
this.maximizedMode = MaximizedMode.Unmaximized;
|
||||
}
|
||||
}
|
||||
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.setMaximize(vertically, horizontally);
|
||||
});
|
||||
}
|
||||
|
||||
public setFullScreen(fullScreen: boolean) {
|
||||
if (!this.kwinClient.fullScreenable) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.fullScreen = fullScreen;
|
||||
});
|
||||
}
|
||||
|
||||
public setShade(shade: boolean) {
|
||||
this.manipulatingGeometry.do(() => {
|
||||
this.kwinClient.shade = shade;
|
||||
});
|
||||
}
|
||||
|
||||
public isShaded() {
|
||||
return this.kwinClient.shade;
|
||||
public getMaximizedMode() {
|
||||
return this.maximizedMode;
|
||||
}
|
||||
|
||||
public isManipulatingGeometry(newGeometry: QmlRect | null) {
|
||||
@@ -118,7 +139,7 @@ class ClientWrapper {
|
||||
}
|
||||
|
||||
public ensureVisible(screenSize: QmlRect) {
|
||||
if (this.kwinClient.desktop !== workspace.currentDesktop) {
|
||||
if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
|
||||
return;
|
||||
}
|
||||
const frame = this.kwinClient.frameGeometry;
|
||||
@@ -129,8 +150,9 @@ class ClientWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
this.stateManager.destroy(passFocus);
|
||||
this.signalManager.destroy();
|
||||
if (this.rulesSignalManager !== null) {
|
||||
this.rulesSignalManager.destroy();
|
||||
}
|
||||
@@ -141,4 +163,17 @@ class ClientWrapper {
|
||||
transient.transientFor = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static initSignalManager(client: ClientWrapper) {
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(client.kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
|
||||
if (maximizedMode !== MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
|
||||
client.kwinClient.tile = null;
|
||||
}
|
||||
client.maximizedMode = maximizedMode;
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
61
src/lib/world/Clients.ts
Normal file
61
src/lib/world/Clients.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
namespace Clients {
|
||||
const prohibitedClasses = [
|
||||
"ksmserver-logout-greeter",
|
||||
"xwaylandvideobridge",
|
||||
];
|
||||
|
||||
export function canTileEver(kwinClient: KwinClient) {
|
||||
const shapeable = (kwinClient.moveable && kwinClient.resizeable) || kwinClient.fullScreen; // full-screen windows may become shapeable after exiting full-screen mode
|
||||
return shapeable &&
|
||||
!kwinClient.popupWindow &&
|
||||
!prohibitedClasses.includes(kwinClient.resourceClass);
|
||||
}
|
||||
|
||||
export function canTileNow(kwinClient: KwinClient) {
|
||||
return canTileEver(kwinClient) &&
|
||||
!kwinClient.minimized &&
|
||||
kwinClient.desktops.length === 1 &&
|
||||
kwinClient.activities.length === 1;
|
||||
}
|
||||
|
||||
export function makeTileable(kwinClient: KwinClient) {
|
||||
if (kwinClient.minimized) {
|
||||
kwinClient.minimized = false;
|
||||
}
|
||||
if (kwinClient.desktops.length !== 1) {
|
||||
kwinClient.desktops = [Workspace.currentDesktop];
|
||||
}
|
||||
if (kwinClient.activities.length !== 1) {
|
||||
kwinClient.activities = [Workspace.currentActivity];
|
||||
}
|
||||
}
|
||||
|
||||
export function getKwinDesktopApprox(kwinClient: KwinClient) {
|
||||
switch (kwinClient.desktops.length) {
|
||||
case 0:
|
||||
return Workspace.currentDesktop;
|
||||
case 1:
|
||||
return kwinClient.desktops[0];
|
||||
default:
|
||||
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
|
||||
return Workspace.currentDesktop;
|
||||
} else {
|
||||
return kwinClient.desktops[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isFullScreenGeometry(kwinClient: KwinClient) {
|
||||
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
|
||||
return kwinClient.clientGeometry.width >= fullScreenArea.width &&
|
||||
kwinClient.clientGeometry.height >= fullScreenArea.height;
|
||||
}
|
||||
|
||||
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
|
||||
return kwinClient.desktops.length === 0 || kwinClient.desktops.includes(kwinDesktop);
|
||||
}
|
||||
|
||||
export function isOnOneOfVirtualDesktops(kwinClient: KwinClient, kwinDesktops: KwinDesktop[]) {
|
||||
return kwinClient.desktops.length === 0 || kwinClient.desktops.some(d => kwinDesktops.includes(d));
|
||||
}
|
||||
}
|
||||
145
src/lib/world/DesktopManager.ts
Normal file
145
src/lib/world/DesktopManager.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
class DesktopManager {
|
||||
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
|
||||
private selectedScreen: Output;
|
||||
private kwinActivities: Set<string>;
|
||||
private kwinDesktops: Set<KwinDesktop>;
|
||||
|
||||
constructor(
|
||||
private readonly pinManager: PinManager,
|
||||
private readonly config: Desktop.Config,
|
||||
private readonly layoutConfig: LayoutConfig,
|
||||
private readonly focusPasser: FocusPassing.Passer,
|
||||
private readonly desktopFilter: DesktopFilter,
|
||||
) {
|
||||
this.pinManager = pinManager;
|
||||
this.config = config;
|
||||
this.layoutConfig = layoutConfig;
|
||||
this.desktops = new Map();
|
||||
this.selectedScreen = Workspace.activeScreen;
|
||||
this.kwinActivities = new Set(Workspace.activities);
|
||||
this.kwinDesktops = new Set(Workspace.desktops);
|
||||
}
|
||||
|
||||
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
|
||||
if (!this.desktopFilter.shouldWorkOnDesktop(kwinDesktop)) {
|
||||
return undefined;
|
||||
}
|
||||
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
|
||||
const desktop = this.desktops.get(desktopKey);
|
||||
if (desktop !== undefined) {
|
||||
return desktop;
|
||||
} else {
|
||||
return this.addDesktop(activity, kwinDesktop);
|
||||
}
|
||||
}
|
||||
|
||||
public getCurrentDesktop() {
|
||||
return this.getDesktop(Workspace.currentActivity, Workspace.currentDesktop);
|
||||
}
|
||||
|
||||
public getDesktopInCurrentActivity(kwinDesktop: KwinDesktop) {
|
||||
return this.getDesktop(Workspace.currentActivity, kwinDesktop);
|
||||
}
|
||||
|
||||
public getDesktopForClient(kwinClient: KwinClient) {
|
||||
if (kwinClient.activities.length !== 1 || kwinClient.desktops.length !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
return this.getDesktop(kwinClient.activities[0], kwinClient.desktops[0]);
|
||||
}
|
||||
|
||||
private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
|
||||
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
|
||||
const desktop = new Desktop(
|
||||
kwinDesktop,
|
||||
this.pinManager,
|
||||
this.config,
|
||||
() => this.selectedScreen,
|
||||
this.layoutConfig,
|
||||
this.focusPasser,
|
||||
);
|
||||
this.desktops.set(desktopKey, desktop);
|
||||
return desktop;
|
||||
}
|
||||
|
||||
private static getDesktopKey(activity: string, kwinDesktop: KwinDesktop) {
|
||||
return activity + "|" + kwinDesktop.id;
|
||||
}
|
||||
|
||||
public updateActivities() {
|
||||
const newActivities = new Set(Workspace.activities);
|
||||
for (const activity of this.kwinActivities) {
|
||||
if (!newActivities.has(activity)) {
|
||||
this.removeActivity(activity);
|
||||
}
|
||||
}
|
||||
this.kwinActivities = newActivities;
|
||||
}
|
||||
|
||||
public updateDesktops() {
|
||||
const newDesktops = new Set(Workspace.desktops);
|
||||
for (const desktop of this.kwinDesktops) {
|
||||
if (!newDesktops.has(desktop)) {
|
||||
this.removeKwinDesktop(desktop);
|
||||
}
|
||||
}
|
||||
this.kwinDesktops = newDesktops;
|
||||
}
|
||||
|
||||
public selectScreen(screen: Output) {
|
||||
this.selectedScreen = screen;
|
||||
}
|
||||
|
||||
private removeActivity(activity: string) {
|
||||
for (const kwinDesktop of this.kwinDesktops) {
|
||||
this.destroyDesktop(activity, kwinDesktop);
|
||||
}
|
||||
}
|
||||
|
||||
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
|
||||
for (const activity of this.kwinActivities) {
|
||||
this.destroyDesktop(activity, kwinDesktop);
|
||||
}
|
||||
}
|
||||
|
||||
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
|
||||
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
|
||||
const desktop = this.desktops.get(desktopKey);
|
||||
if (desktop !== undefined) {
|
||||
desktop.destroy();
|
||||
this.desktops.delete(desktopKey);
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
for (const desktop of this.desktops.values()) {
|
||||
desktop.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
public *getAllDesktops() {
|
||||
for (const desktop of this.desktops.values()) {
|
||||
yield desktop;
|
||||
}
|
||||
}
|
||||
|
||||
public getDesktopsForClient(kwinClient: KwinClient) {
|
||||
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
|
||||
return desktops;
|
||||
}
|
||||
|
||||
// empty array means all
|
||||
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
|
||||
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
|
||||
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
|
||||
for (const matchedActivity of matchedActivities) {
|
||||
for (const matchedDesktop of matchedDesktops) {
|
||||
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
|
||||
const desktop = this.desktops.get(desktopKey);
|
||||
if (desktop !== undefined) {
|
||||
yield desktop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/lib/world/FocusPassing.ts
Normal file
51
src/lib/world/FocusPassing.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace FocusPassing {
|
||||
export const enum Type {
|
||||
None,
|
||||
Immediate,
|
||||
OnUnfocus,
|
||||
}
|
||||
|
||||
export class Passer {
|
||||
private currentRequest: Request | null = null;
|
||||
|
||||
public request(target: KwinClient) {
|
||||
this.currentRequest = new Request(target, Date.now());
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this.currentRequest = null;
|
||||
}
|
||||
|
||||
public clearIfDifferent(kwinClient: KwinClient) {
|
||||
if (this.currentRequest !== null && this.currentRequest.target !== kwinClient) {
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public activate() {
|
||||
if (this.currentRequest === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentRequest.isExpired()) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Workspace.activeWindow = this.currentRequest.target;
|
||||
}
|
||||
}
|
||||
|
||||
class Request {
|
||||
private static readonly validMs = 200;
|
||||
|
||||
constructor(
|
||||
public readonly target: KwinClient,
|
||||
private readonly time: number,
|
||||
) {}
|
||||
|
||||
public isExpired() {
|
||||
return Date.now() - this.time > Request.validMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,11 @@ class PinManager {
|
||||
this.pinnedClients.delete(kwinClient);
|
||||
}
|
||||
|
||||
public getAvailableSpace(desktopNumber: number, screen: QmlRect) {
|
||||
public getAvailableSpace(kwinDesktop: KwinDesktop, 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)) {
|
||||
if (!Clients.isOnVirtualDesktop(client, kwinDesktop) || client.minimized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
180
src/lib/world/World.ts
Normal file
180
src/lib/world/World.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
class World {
|
||||
private readonly desktopManager: DesktopManager;
|
||||
private readonly clientManager: ClientManager;
|
||||
private readonly pinManager: PinManager;
|
||||
private readonly workspaceSignalManager: SignalManager;
|
||||
private readonly shortcutActions: ShortcutAction[];
|
||||
private readonly screenResizedDelayer: Delayer;
|
||||
private readonly cursorFollowsFocus: boolean;
|
||||
|
||||
constructor(config: Config) {
|
||||
const focusPasser = new FocusPassing.Passer();
|
||||
this.workspaceSignalManager = initWorkspaceSignalHandlers(this, focusPasser);
|
||||
this.cursorFollowsFocus = config.cursorFollowsFocus;
|
||||
|
||||
let presetWidths = {
|
||||
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
|
||||
prev: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
|
||||
getWidths: (minWidth: number, maxWidth: number): number[] => [],
|
||||
};
|
||||
try {
|
||||
presetWidths = new PresetWidths(config.presetWidths, config.gapsInnerHorizontal);
|
||||
} catch (error: any) {
|
||||
notificationInvalidPresetWidths.sendEvent();
|
||||
log("failed to parse presetWidths:", error);
|
||||
}
|
||||
|
||||
this.shortcutActions = registerKeyBindings(this, {
|
||||
manualScrollStep: config.manualScrollStep,
|
||||
presetWidths: presetWidths,
|
||||
columnResizer: config.scrollingCentered ? new RawResizer(presetWidths) : new ContextualResizer(presetWidths),
|
||||
});
|
||||
|
||||
this.screenResizedDelayer = new Delayer(1000, () => {
|
||||
// this delay ensures that docks are taken into account by `Workspace.clientArea`
|
||||
for (const desktop of this.desktopManager.getAllDesktops()) {
|
||||
desktop.onLayoutChanged();
|
||||
}
|
||||
this.update();
|
||||
});
|
||||
|
||||
this.pinManager = new PinManager();
|
||||
|
||||
const layoutConfig = {
|
||||
gapsInnerHorizontal: config.gapsInnerHorizontal,
|
||||
gapsInnerVertical: config.gapsInnerVertical,
|
||||
stackOffsetX: config.stackOffsetX,
|
||||
stackOffsetY: config.stackOffsetY,
|
||||
offScreenOpacity: config.offScreenOpacity / 100.0,
|
||||
stackColumnsByDefault: config.stackColumnsByDefault,
|
||||
resizeNeighborColumn: config.resizeNeighborColumn,
|
||||
reMaximize: config.reMaximize,
|
||||
skipSwitcher: config.skipSwitcher,
|
||||
tiledKeepBelow: config.tiledKeepBelow,
|
||||
maximizedKeepAbove: config.floatingKeepAbove,
|
||||
untileOnDrag: config.untileOnDrag,
|
||||
};
|
||||
|
||||
this.desktopManager = new DesktopManager(
|
||||
this.pinManager,
|
||||
{
|
||||
marginTop: config.gapsOuterTop,
|
||||
marginBottom: config.gapsOuterBottom,
|
||||
marginLeft: config.gapsOuterLeft,
|
||||
marginRight: config.gapsOuterRight,
|
||||
scroller: World.createScroller(config),
|
||||
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
|
||||
gestureScroll: config.gestureScroll,
|
||||
gestureScrollInvert: config.gestureScrollInvert,
|
||||
gestureScrollStep: config.gestureScrollStep,
|
||||
},
|
||||
layoutConfig,
|
||||
focusPasser,
|
||||
new DesktopFilter(config.tiledDesktops),
|
||||
);
|
||||
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
|
||||
this.addExistingClients();
|
||||
this.update();
|
||||
}
|
||||
|
||||
private static createScroller(config: Config) {
|
||||
if (config.scrollingLazy) {
|
||||
return new LazyScroller();
|
||||
} else if (config.scrollingCentered) {
|
||||
return new CenteredScroller();
|
||||
} else if (config.scrollingGrouped) {
|
||||
return new GroupedScroller();
|
||||
} else {
|
||||
log("No scrolling mode selected, using default");
|
||||
return new LazyScroller();
|
||||
}
|
||||
}
|
||||
|
||||
private addExistingClients() {
|
||||
for (const kwinClient of Workspace.windows) {
|
||||
this.clientManager.addClient(kwinClient);
|
||||
}
|
||||
}
|
||||
|
||||
private update() {
|
||||
const currentDesktop = this.desktopManager.getCurrentDesktop();
|
||||
if (currentDesktop !== undefined) {
|
||||
currentDesktop.arrange();
|
||||
this.moveCursorToFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private moveCursorToFocus() {
|
||||
if (this.cursorFollowsFocus && Workspace.activeWindow !== null) {
|
||||
// Only move cursor for tiled windows
|
||||
const tiledWindow = this.clientManager.findTiledWindow(Workspace.activeWindow);
|
||||
if (tiledWindow === null) {
|
||||
return;
|
||||
}
|
||||
const cursorAlreadyInFocus = rectContainsPoint(Workspace.activeWindow.frameGeometry, Workspace.cursorPos);
|
||||
if (cursorAlreadyInFocus) {
|
||||
return;
|
||||
}
|
||||
moveCursorToFocus.call();
|
||||
}
|
||||
}
|
||||
|
||||
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
|
||||
f(this.clientManager, this.desktopManager);
|
||||
this.update();
|
||||
}
|
||||
|
||||
public doIfTiled(
|
||||
kwinClient: KwinClient,
|
||||
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
|
||||
) {
|
||||
const window = this.clientManager.findTiledWindow(kwinClient);
|
||||
if (window === null) {
|
||||
return;
|
||||
}
|
||||
const column = window.column;
|
||||
const grid = column.grid;
|
||||
f(this.clientManager, this.desktopManager, window, column, grid);
|
||||
this.update();
|
||||
}
|
||||
|
||||
public doIfTiledFocused(
|
||||
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
|
||||
) {
|
||||
if (Workspace.activeWindow === null) {
|
||||
return;
|
||||
}
|
||||
this.doIfTiled(Workspace.activeWindow, f);
|
||||
}
|
||||
|
||||
public gestureScroll(amount: number) {
|
||||
this.do((clientManager, desktopManager) => {
|
||||
const currentDesktop = desktopManager.getCurrentDesktop();
|
||||
if (currentDesktop !== undefined) {
|
||||
currentDesktop.gestureScroll(amount);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public gestureScrollFinish() {
|
||||
this.do((clientManager, desktopManager) => {
|
||||
const currentDesktop = desktopManager.getCurrentDesktop();
|
||||
if (currentDesktop !== undefined) {
|
||||
currentDesktop.gestureScrollFinish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.workspaceSignalManager.destroy();
|
||||
for (const shortcutAction of this.shortcutActions) {
|
||||
shortcutAction.destroy();
|
||||
}
|
||||
this.clientManager.destroy();
|
||||
this.desktopManager.destroy();
|
||||
}
|
||||
|
||||
public onScreenResized() {
|
||||
this.screenResizedDelayer.run();
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,14 @@ namespace ClientState {
|
||||
world.onScreenResized();
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
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) => {
|
||||
manager.connect(kwinClient.frameGeometryChanged, () => {
|
||||
world.onScreenResized();
|
||||
});
|
||||
return manager;
|
||||
@@ -7,7 +7,7 @@ namespace ClientState {
|
||||
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
|
||||
this.client = client;
|
||||
this.config = config;
|
||||
if (config.keepAbove) {
|
||||
if (config.floatingKeepAbove) {
|
||||
client.kwinClient.keepAbove = true;
|
||||
}
|
||||
if (limitHeight && client.kwinClient.tile === null) {
|
||||
@@ -16,15 +16,17 @@ namespace ClientState {
|
||||
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
this.signalManager.destroy();
|
||||
if (this.config.keepAbove) {
|
||||
this.client.kwinClient.keepAbove = false;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move to `Tiled.restoreClientAfterTiling`
|
||||
private static limitHeight(client: ClientWrapper) {
|
||||
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, client.kwinClient.screen, client.kwinClient.desktop);
|
||||
const placementArea = Workspace.clientArea(
|
||||
ClientAreaOption.PlacementArea,
|
||||
client.kwinClient.output,
|
||||
Clients.getKwinDesktopApprox(client.kwinClient),
|
||||
);
|
||||
const clientRect = client.kwinClient.frameGeometry;
|
||||
const width = client.preferredWidth;
|
||||
client.place(
|
||||
@@ -46,7 +48,7 @@ namespace ClientState {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, () => {
|
||||
// on Wayland, this fires after `tileChanged`
|
||||
if (kwinClient.tile !== null) {
|
||||
@@ -54,7 +56,7 @@ namespace ClientState {
|
||||
clientManager.pinClient(kwinClient);
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace ClientState {
|
||||
this.state = initialState;
|
||||
}
|
||||
|
||||
public setState(constructNewState: () => State, passFocus: boolean) {
|
||||
public setState(constructNewState: () => State, passFocus: FocusPassing.Type) {
|
||||
this.state.destroy(passFocus);
|
||||
this.state = constructNewState();
|
||||
}
|
||||
@@ -15,12 +15,12 @@ namespace ClientState {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
this.state.destroy(passFocus);
|
||||
}
|
||||
}
|
||||
|
||||
export type State = {
|
||||
destroy(passFocus: boolean): void,
|
||||
};
|
||||
export interface State {
|
||||
destroy(passFocus: FocusPassing.Type): void;
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,14 @@ namespace ClientState {
|
||||
this.pinManager = pinManager;
|
||||
this.desktopManager = desktopManager;
|
||||
this.config = config;
|
||||
if (config.keepAbove) {
|
||||
if (config.floatingKeepAbove) {
|
||||
kwinClient.keepAbove = true;
|
||||
}
|
||||
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
|
||||
}
|
||||
|
||||
public destroy(passFocus: boolean) {
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
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();
|
||||
@@ -30,8 +27,8 @@ namespace ClientState {
|
||||
|
||||
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
|
||||
const manager = new SignalManager();
|
||||
let oldDesktopNumber = kwinClient.desktop;
|
||||
let oldActivities = kwinClient.activities;
|
||||
let oldDesktops = kwinClient.desktops;
|
||||
|
||||
manager.connect(kwinClient.tileChanged, () => {
|
||||
if (kwinClient.tile === null) {
|
||||
@@ -41,7 +38,7 @@ namespace ClientState {
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => {
|
||||
manager.connect(kwinClient.frameGeometryChanged, () => {
|
||||
if (kwinClient.tile === null) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.unpinClient(kwinClient);
|
||||
@@ -53,28 +50,35 @@ namespace ClientState {
|
||||
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.desktopChanged, () => {
|
||||
const changedDesktops = oldDesktopNumber === -1 || kwinClient.desktop === -1 ?
|
||||
[] :
|
||||
[oldDesktopNumber, kwinClient.desktop];
|
||||
manager.connect(kwinClient.minimizedChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
for (const desktop of desktopManager.getDesktops(changedDesktops, kwinClient.activities)) {
|
||||
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
});
|
||||
oldDesktopNumber = kwinClient.desktop;
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.activitiesChanged, (kwinClient: KwinClient) => {
|
||||
const desktops = kwinClient.desktop === -1 ? [] : [kwinClient.desktop];
|
||||
manager.connect(kwinClient.desktopsChanged, () => {
|
||||
const changedDesktops = oldDesktops.length === 0 || kwinClient.desktops.length === 0 ?
|
||||
[] :
|
||||
union(oldDesktops, kwinClient.desktops);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
});
|
||||
oldDesktops = kwinClient.desktops;
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.activitiesChanged, () => {
|
||||
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)) {
|
||||
for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
|
||||
desktop.onPinsChanged();
|
||||
}
|
||||
});
|
||||
261
src/lib/world/clientState/Tiled.ts
Normal file
261
src/lib/world/clientState/Tiled.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
namespace ClientState {
|
||||
export class Tiled implements State {
|
||||
public readonly window: Window;
|
||||
private readonly defaultState: Tiled.WindowState;
|
||||
private readonly signalManager: SignalManager;
|
||||
private static readonly maxExternalFrameGeometryChangedIntervalMs = 1000;
|
||||
|
||||
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, grid.config);
|
||||
}
|
||||
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
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, config: LayoutConfig) {
|
||||
const client = window.client;
|
||||
const kwinClient = client.kwinClient;
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(kwinClient.desktopsChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(kwinClient);
|
||||
if (desktop === undefined) {
|
||||
// windows on multiple desktops are not supported
|
||||
clientManager.floatClient(client);
|
||||
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.floatClient(client);
|
||||
return;
|
||||
}
|
||||
Tiled.moveWindowToGrid(window, desktop.grid);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.minimizedChanged, () => {
|
||||
console.assert(kwinClient.minimized);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.minimizeClient(kwinClient);
|
||||
});
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
|
||||
world.do(() => {
|
||||
window.onMaximizedChanged(maximizedMode);
|
||||
});
|
||||
});
|
||||
|
||||
let moving = false;
|
||||
let resizing = false;
|
||||
let resizeStartWidth = 0;
|
||||
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
|
||||
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
|
||||
if (kwinClient.move) {
|
||||
if (config.untileOnDrag) {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
clientManager.floatClient(client);
|
||||
});
|
||||
} else {
|
||||
moving = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (kwinClient.resize) {
|
||||
resizing = true;
|
||||
resizeStartWidth = window.column.getWidth();
|
||||
if (config.resizeNeighborColumn) {
|
||||
const resizeNeighborColumn = Tiled.getResizeNeighborColumn(window);
|
||||
if (resizeNeighborColumn !== null) {
|
||||
resizeNeighbor = {
|
||||
column: resizeNeighborColumn,
|
||||
startWidth: resizeNeighborColumn.getWidth(),
|
||||
};
|
||||
}
|
||||
}
|
||||
window.column.grid.onUserResizeStarted();
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
|
||||
if (moving) {
|
||||
moving = false;
|
||||
world.do(() => window.column.grid.desktop.onLayoutChanged()); // move the dragged window back to its position
|
||||
}
|
||||
if (resizing) {
|
||||
resizing = false;
|
||||
resizeNeighbor = undefined;
|
||||
window.column.grid.onUserResizeFinished();
|
||||
}
|
||||
});
|
||||
|
||||
const externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
|
||||
manager.connect(kwinClient.frameGeometryChanged, (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) {
|
||||
// TODO: instead of passing dx and dy, remember relative (to the parent) x and y for each
|
||||
// transient window and use them for `moveTransients` and `ensureTransientsVisible`
|
||||
client.moveTransients(dx, dy);
|
||||
}
|
||||
|
||||
if (kwinClient.resize) {
|
||||
world.do(() => {
|
||||
if (newGeometry.width !== oldGeometry.width) {
|
||||
window.column.onUserResizeWidth(
|
||||
resizeStartWidth,
|
||||
newGeometry.width - resizeStartWidth,
|
||||
newGeometry.left !== oldGeometry.left,
|
||||
resizeNeighbor,
|
||||
);
|
||||
}
|
||||
if (newGeometry.height !== oldGeometry.height) {
|
||||
window.column.adjustWindowHeight(
|
||||
window,
|
||||
newGeometry.height - oldGeometry.height,
|
||||
newGeometry.y !== oldGeometry.y,
|
||||
);
|
||||
}
|
||||
});
|
||||
} else if (
|
||||
!window.column.grid.isUserResizing() &&
|
||||
!client.isManipulatingGeometry(newGeometry) &&
|
||||
client.getMaximizedMode() === MaximizedMode.Unmaximized &&
|
||||
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
|
||||
) {
|
||||
if (externalFrameGeometryChangedRateLimiter.acquire()) {
|
||||
world.do(() => window.onFrameGeometryChanged());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
manager.connect(kwinClient.fullScreenChanged, () => {
|
||||
world.do((clientManager, desktopManager) => {
|
||||
// some clients only turn out to be untileable after exiting full-screen mode
|
||||
if (!Clients.canTileEver(kwinClient)) {
|
||||
clientManager.floatClient(client);
|
||||
return;
|
||||
}
|
||||
|
||||
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 getResizeNeighborColumn(window: Window) {
|
||||
const kwinClient = window.client.kwinClient;
|
||||
const column = window.column;
|
||||
if (Workspace.cursorPos.x > kwinClient.clientGeometry.right) {
|
||||
return column.grid.getRightColumn(column);
|
||||
} else if (Workspace.cursorPos.x < kwinClient.clientGeometry.left) {
|
||||
return column.grid.getLeftColumn(column);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
const passFocus = window.isFocused() ? FocusPassing.Type.OnUnfocus : FocusPassing.Type.None;
|
||||
window.moveToColumn(newColumn, true, passFocus);
|
||||
}
|
||||
|
||||
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
|
||||
if (config.skipSwitcher) {
|
||||
client.kwinClient.skipSwitcher = true;
|
||||
}
|
||||
|
||||
if (client.kwinClient.fullScreen) {
|
||||
if (config.maximizedKeepAbove) {
|
||||
client.kwinClient.keepAbove = true;
|
||||
}
|
||||
} else {
|
||||
if (config.tiledKeepBelow) {
|
||||
client.kwinClient.keepBelow = true;
|
||||
}
|
||||
client.kwinClient.keepAbove = 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;
|
||||
}
|
||||
if (config.offScreenOpacity < 1.0) {
|
||||
client.kwinClient.opacity = 1.0;
|
||||
}
|
||||
client.setFullScreen(false);
|
||||
if (client.kwinClient.tile === null) {
|
||||
client.setMaximize(false, false);
|
||||
}
|
||||
client.ensureVisible(screenSize);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Tiled {
|
||||
export interface WindowState {
|
||||
skipSwitcher: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/lib/world/clientState/TiledMinimized.ts
Normal file
31
src/lib/world/clientState/TiledMinimized.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace ClientState {
|
||||
export class TiledMinimized implements State {
|
||||
private readonly signalManager: SignalManager;
|
||||
|
||||
constructor(world: World, client: ClientWrapper) {
|
||||
this.signalManager = TiledMinimized.initSignalManager(world, client);
|
||||
}
|
||||
|
||||
public destroy(passFocus: FocusPassing.Type) {
|
||||
this.signalManager.destroy();
|
||||
}
|
||||
|
||||
private static initSignalManager(world: World, client: ClientWrapper) {
|
||||
const manager = new SignalManager();
|
||||
|
||||
manager.connect(client.kwinClient.minimizedChanged, () => {
|
||||
console.assert(!client.kwinClient.minimized);
|
||||
world.do((clientManager, desktopManager) => {
|
||||
const desktop = desktopManager.getDesktopForClient(client.kwinClient);
|
||||
if (desktop !== undefined) {
|
||||
clientManager.tileClient(client, desktop.grid);
|
||||
} else {
|
||||
clientManager.floatClient(client);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
function init() {
|
||||
const config = loadConfig();
|
||||
const world = new World(config);
|
||||
registerKeyBindings(world, config);
|
||||
return world;
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
function init() {
|
||||
return new World(loadConfig());
|
||||
}
|
||||
|
||||
function loadConfig(): Config {
|
||||
const config: any = {};
|
||||
for (const entry of configDef) {
|
||||
8
src/main/tsconfig.json
Normal file
8
src/main/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"../extern/**/*",
|
||||
"../lib/**/*",
|
||||
"./**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
class ClientMatcher {
|
||||
private readonly rules: Map<string, RegExp>;
|
||||
|
||||
constructor(rules: Map<string, RegExp>) {
|
||||
this.rules = rules;
|
||||
}
|
||||
|
||||
public matches(kwinClient: KwinClient) {
|
||||
const rule = this.rules.get(kwinClient.resourceClass);
|
||||
if (rule === undefined) {
|
||||
return false;
|
||||
}
|
||||
return rule.test(kwinClient.caption);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
type WindowRule = {
|
||||
class: string,
|
||||
caption: string,
|
||||
tile: boolean,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user