133 Commits
v0.6 ... v0.9.2

Author SHA1 Message Date
Peter Fajdiga
e776df509b bump version to 0.9.2 2024-04-27 22:54:53 +02:00
Peter Fajdiga
63de2d4cae WindowRuleEnforcer: prevent tiling windows with pid = -1 (fixes #46) 2024-04-27 17:36:17 +02:00
Peter Fajdiga
85d361f16f Makefile: remove logs target 2024-04-27 12:41:49 +02:00
Peter Fajdiga
f3b75807be Makefile: merge config into build 2024-04-27 12:38:42 +02:00
Peter Fajdiga
88bce26456 Makefile: run tests on every build 2024-04-27 12:38:42 +02:00
Peter Fajdiga
3a75ddab0f WindowRuleEnforcer: fix rule string generation 2024-04-27 12:38:42 +02:00
Peter Fajdiga
ee0aa93308 tests: add WindowRuleEnforcer test cases 2024-04-27 12:38:42 +02:00
Peter Fajdiga
33ca138420 move test files to src/tests 2024-04-27 12:38:42 +02:00
Peter Fajdiga
79f4aaeef8 Revert "tsconfig: exclude test files"
This reverts commit ee14509228.
2024-04-27 12:38:42 +02:00
Peter Fajdiga
0aca6e1146 create unit tests 2024-04-27 12:38:40 +02:00
Peter Fajdiga
28e54434aa run-ts.sh: only delete tmp file if successful 2024-04-26 18:26:35 +02:00
Peter Fajdiga
ee14509228 tsconfig: exclude test files 2024-04-20 19:01:24 +02:00
Peter Fajdiga
1596edc43f run-ts.sh: print build errors 2024-04-20 19:00:03 +02:00
Peter Fajdiga
b897ab5b9f extern: rename .d.ts to .ts 2024-04-20 17:41:56 +02:00
Peter Fajdiga
5e6dad8459 extern: move all declarations to global.d.ts 2024-04-20 17:40:17 +02:00
Peter Fajdiga
beeba74442 extern: split type definitions and global constant declarations 2024-04-20 17:31:10 +02:00
Peter Fajdiga
3e14440180 create a typescript project for each generator script 2024-04-20 17:30:27 +02:00
Peter Fajdiga
36836ad258 split lib and main 2024-04-20 17:30:27 +02:00
Peter Fajdiga
bebc009cc6 split tsconfig 2024-04-20 17:30:27 +02:00
Peter Fajdiga
a4ba8516dc move code to src/main 2024-04-20 17:30:27 +02:00
Peter Fajdiga
675a70d907 tsconfig: clean up 2024-04-20 17:30:27 +02:00
Peter Fajdiga
3ba66f1c89 World: extract function createScroller 2024-04-20 17:30:01 +02:00
Peter Fajdiga
4556198b2e extern: define function return types 2024-04-20 15:28:06 +02:00
Peter Fajdiga
7b8de5955d extern: extract Notification into notification.d.ts 2024-04-19 17:37:44 +02:00
Peter Fajdiga
68a687b7d4 qt.d.ts: add message parameter to console.assert 2024-04-18 21:22:53 +02:00
Peter Fajdiga
20a3ece4b5 bump version to 0.9.1 2024-04-06 08:36:51 +02:00
Peter Fajdiga
3cad8102ee qt.d.ts: remove QByteArray type 2024-04-05 14:49:36 +02:00
Peter Fajdiga
7fd45eed8f kwin.d.ts: mark cursorPos and minSize as immutable 2024-04-05 14:48:56 +02:00
Peter Fajdiga
7299341608 Tiled: use clientGeometry to determine border resize 2024-04-05 14:46:19 +02:00
Peter Fajdiga
842ec1ac63 WindowRuleEnforcer: fix bug in joinRegexes 2024-04-05 13:58:50 +02:00
Peter Fajdiga
0523465b84 WindowRuleEnforcer: remove debug logs 2024-04-01 19:26:50 +02:00
Peter Fajdiga
c7cfa261b9 bump version to 0.9 2024-03-30 12:46:59 +01:00
Peter Fajdiga
56955e4df3 src/config: don't tile kded windows 2024-03-30 12:45:42 +01:00
Peter Fajdiga
bb308cfbfb config: merge X11 and Wayland class regexes in window rules 2024-03-30 12:45:40 +01:00
Peter Fajdiga
6c00245943 config: escape . in window rules 2024-03-30 12:45:38 +01:00
Peter Fajdiga
2efdbe5a7b support regex for class selector in window rules (resolves #41) 2024-03-30 12:45:36 +01:00
Peter Fajdiga
092cbf3ff1 kwin.d.ts: remove unused signal maximizedChanged 2024-03-30 12:45:33 +01:00
Peter Fajdiga
f9ae299ce8 Tiled: restore opacity after un-tiling 2024-03-30 12:45:28 +01:00
Peter Fajdiga
695f5edf6a config: add option to disable layering (resolves #41) 2024-03-30 12:45:25 +01:00
Peter Fajdiga
9b80b535a1 readme: update QML dependencies 2024-03-19 10:58:51 +01:00
Peter Fajdiga
752df86db5 bump version to 0.8.1 2024-03-19 10:46:33 +01:00
Peter Fajdiga
f05eefe19b config: add plasmashell to window rules (fixes #38) 2024-03-19 10:17:37 +01:00
Peter Fajdiga
f550285778 bump version to 0.8 2024-03-18 19:56:14 +01:00
Peter Fajdiga
5247a6a0d3 don't tile immovable windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
2b114a63dc refactor TiledMinimized unminimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
63e4015f3a don't tile popup windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
02db31266b ClientWrapper: don't try to maximize/fullscreenify unsupporting windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
67d4d89700 World: remove workaround for Qt5 bug 2024-03-18 19:31:22 +01:00
Peter Fajdiga
755cf90b1a DesktopManager: destroy removed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
e6a01217a5 DesktopManager.getDesktopsForClient: call getDesktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
21d7bbd6c4 DesktopManager: don't yield previously unconstructed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
605215acdc workspace.ts: use Clients.makeTileable 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4b6808dba1 ClientWrapper: set maximizedMode in setMaximize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
f9749c6f56 Tiled: use interactiveMoveResizeStarted and interactiveMoveResizeFinished 2024-03-18 19:31:22 +01:00
Peter Fajdiga
9b40b2f777 Tiled: use cursorPos to distinguish between border and single-column resize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
33470b4d7b move kwinClient.tile = null to ClientWrapper 2024-03-18 19:31:22 +01:00
Peter Fajdiga
8947719621 ClientWrapper: store maximized state 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4bf4f8e8a1 Tiled: use maximizedAboutToChange instead of maximizedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
080de7cf97 kwin.d.ts: add signal maximizedAboutToChange 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c29902dc15 Pinned: change condition for un-tile on maximixedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1736b0a398 kwin.d.ts: update client signals 2024-03-18 19:31:22 +01:00
Peter Fajdiga
a1c44647ca TiledMinimized: support un-minimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
0ea75d6348 confirm all desktops == empty array 2024-03-18 19:31:22 +01:00
Peter Fajdiga
12901e45ce src/keyBindings: add TODO 2024-03-18 19:31:22 +01:00
Peter Fajdiga
29b4ccd1dd port key bindings to the kwin6 ShortcutHandler system 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7b547bc5b8 pass desktop to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
78a127111b pass output to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
333b7601b2 refactor desktops (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1927ae445d kwin.d.ts: add KwinDesktop (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1f563dae01 kwin.d.ts: update KwinClient properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
6b82eedbfe kwin.d.ts: move removed Workspace signals to KwinClient (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
b479735130 kwin.d.ts: update Workspace properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c8f022d66f kwin.d.ts: rename Workspace 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7f71750a8e use QtQuick 6.0 2024-03-18 19:31:22 +01:00
Peter Fajdiga
13ebf24732 Makefile: fix uninstall recipe 2024-03-18 19:31:22 +01:00
Peter Fajdiga
ec6b3247b7 Makefile: use kpackagetool6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
50681d3a07 update package structure for kde 6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
af930a9b2f Tiled: check if user is resizing any window in the grid 2024-03-18 19:28:58 +01:00
Peter Fajdiga
489a1447e7 ClientWrapper: create a workaround for the problem with stuck off-screen windows on Wayland 2024-03-18 19:28:58 +01:00
Peter Fajdiga
b984f025ec Column: avoid setting preferredWidth of a window when another window's height in the same column is being resized by the user 2024-03-10 21:29:47 +01:00
Peter Fajdiga
4e1204f1bd DesktopManager: refactor getDesktopForClient 2024-03-10 21:02:24 +01:00
Peter Fajdiga
bbcf51783d World: fix grammar in comment 2024-03-10 15:44:17 +01:00
Peter Fajdiga
019da3766e kwin.d.ts: use unknown for Tile 2024-03-10 15:44:17 +01:00
Peter Fajdiga
1535c994b8 use === everywhere 2024-03-10 15:44:16 +01:00
Peter Fajdiga
296d0deca9 remove void keyword in QSignal params 2024-03-09 21:55:15 +01:00
Peter Fajdiga
17e7d5b46e kwin.d.ts: remove unneeded QSignal params 2024-03-09 21:50:27 +01:00
Peter Fajdiga
840a50d14d move ClientAreaOption to kwin.d.ts 2024-03-09 21:44:23 +01:00
Peter Fajdiga
4f99c4dd45 Actions: remove superfluous return values 2024-03-09 19:53:10 +01:00
Peter Fajdiga
030eddaf34 Floating: add missing ; 2024-03-09 19:34:54 +01:00
Peter Fajdiga
7246a7660e kwin.d.ts: remove superfluous comments 2024-03-09 19:19:39 +01:00
Peter Fajdiga
687256d1dd src/config: unjsonify 2024-03-09 19:19:38 +01:00
Peter Fajdiga
12bb7506cc src/keyBindings: unjsonify 2024-03-09 19:19:13 +01:00
Peter Fajdiga
1808ee0025 mark all signal properties as readonly 2024-03-09 19:07:12 +01:00
Peter Fajdiga
3021f61933 metadata.json: change description 2024-03-01 15:54:06 +01:00
Peter Fajdiga
e908138478 fix bug where resizing used manualScrollStep setting instead of manualResizeStep setting 2024-02-22 20:24:12 +01:00
Peter Fajdiga
99ad115370 rename scrollers 2024-02-18 22:11:34 +01:00
Peter Fajdiga
c5a4238f5f move scroller src files to src/behavior/scroller 2024-02-18 22:07:28 +01:00
Peter Fajdiga
0670d9c265 separate clampScrollX logic into different Clamper implementations 2024-02-18 22:06:14 +01:00
Peter Fajdiga
845874b0d0 separate increaseColumnWidth and columnWidthDecrease logic into different ColumnResizer implementations 2024-02-18 22:06:13 +01:00
Peter Fajdiga
a422a077f6 Makefile: add prerequisites to package target 2024-02-18 21:28:17 +01:00
Peter Fajdiga
2fe1be99cb Desktop.scrollCenterVisible: stop prioritizing visible columns (this is now done by ColumnRange.addNeighbors) 2024-02-18 20:21:34 +01:00
Peter Fajdiga
1a449c238d ColumnRange: prioritize nearer columns 2024-02-18 20:15:58 +01:00
Peter Fajdiga
9bda7d1a09 bump version to 0.7.1 2024-02-12 21:27:34 +01:00
Peter Fajdiga
2ce72bcee8 qt.d.ts: add console.trace 2024-02-12 21:17:37 +01:00
Peter Fajdiga
ff3f6c5d6b Desktop: fix ColumnRange when a column's width is still 0 2024-02-12 21:10:12 +01:00
Peter Fajdiga
3ab230b498 Makefile: append version number to package file name 2024-02-11 20:37:28 +01:00
Peter Fajdiga
ba9f362a1c .gitignore: ignore suffixed packages 2024-02-11 20:37:27 +01:00
Peter Fajdiga
ad6c3f1cae bump version to 0.7 2024-02-11 20:37:26 +01:00
Peter Fajdiga
ba4dd2a9c1 config.ui: relabel scrollingGrouped button 2024-02-11 20:37:25 +01:00
Peter Fajdiga
bb61853009 config.ui: reorder tabs 2024-02-11 20:37:22 +01:00
Peter Fajdiga
0cfd9b9e36 Desktop: scrollCenterRange: add parameter prioritiseVisible 2024-01-22 08:55:05 +01:00
Peter Fajdiga
43c4f7ef9a Actions: columnWidthIncrease: add steps for fully visible screen-edge columns 2024-01-22 08:55:05 +01:00
Peter Fajdiga
9cb3f33ecb Actions: extract function findNextStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
31b9e61ae3 config.ui: add manualResizeStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
668e6696ab Actions: column width increase/decrease: replace screen-relative steps with column-relative steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
e63959cfbf Actions: getWidthSteps: ignore screen-relative steps too close to existing steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
ef2650beb8 Actions: improve snapping in columnWidthDecrease 2024-01-22 08:55:05 +01:00
Peter Fajdiga
750c47c040 implement resize steps (resolves #25) 2024-01-22 08:55:05 +01:00
Peter Fajdiga
88ca0d02e1 generators/config/kcfg.ts: escape xml characters in default config values 2024-01-21 19:40:12 +01:00
Peter Fajdiga
aba786b754 Desktop: rename scrollIntoView 2024-01-21 18:39:53 +01:00
Peter Fajdiga
47aa625c99 Actions: column width increase/decrease: use getCurrentVisibleRange 2024-01-21 18:39:25 +01:00
Peter Fajdiga
03c7cc6503 Desktop.scrollToRange: simplify 2024-01-21 18:37:34 +01:00
Peter Fajdiga
9e9ff2b74f remove overscroll feature (resolves #23) 2024-01-21 18:26:18 +01:00
Peter Fajdiga
5674624e6f Desktop.equalizeVisibleColumnsWidths: simplify scroll at the end 2024-01-21 18:17:29 +01:00
Peter Fajdiga
44dd88ef7c Desktop.equalizeVisibleColumnsWidths: handle columns with limited min width 2024-01-21 18:17:29 +01:00
Peter Fajdiga
f800d6ecf0 Desktop: rewrite ColumnRange.addNeighbors 2024-01-21 18:17:29 +01:00
Peter Fajdiga
3477e17bb3 Desktop: scrollCenterRange: replace parameter requireVisible with condition 2024-01-21 18:17:29 +01:00
Peter Fajdiga
755c781646 rename parameters of doIfTiled passed functions 2024-01-21 18:17:29 +01:00
Peter Fajdiga
926345ba31 move column width increase/decrease code to Actions.ts 2024-01-21 18:17:29 +01:00
Peter Fajdiga
a2295ede43 Desktop: add method scrollCenterVisible (moved from ScrollerGrouped) 2024-01-21 18:17:29 +01:00
Peter Fajdiga
ca80a7ca28 Grid.onColumnWidthChanged: fix autoAdjustScroll call 2024-01-14 15:40:24 +01:00
Peter Fajdiga
64474b1677 readme: mention Niri 2024-01-14 09:55:57 +01:00
Peter Fajdiga
eca63cbc16 readme: update key bindings 2023-12-30 17:26:59 +01:00
83 changed files with 1813 additions and 1681 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/package/contents/code/main.js /package/contents/code/main.js
/package/contents/config/main.xml /package/contents/config/main.xml
/karousel.tar.gz /karousel*.tar.gz
run-ts-tmp.js
/.idea /.idea

View File

@@ -1,31 +1,29 @@
.PHONY: * .PHONY: *
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
config: build: tests
tsc -p ./src/main --outFile ./package/contents/code/main.js
mkdir -p ./package/contents/config 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: tests:
tsc --outFile ./package/contents/code/main.js ./run-ts.sh ./src/tests
install: build config install: build
kpackagetool5 --type=KWin/Script -i ./package || kpackagetool5 --type=KWin/Script -u ./package kpackagetool6 --type=KWin/Script -i ./package || kpackagetool6 --type=KWin/Script -u ./package
uninstall: uninstall:
kpackagetool5 --type=KWin/Script -r ./package kpackagetool6 --type=KWin/Script -r karousel
package: package: build
tar -czf ./karousel.tar.gz ./package tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package
logs:
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
docs-key-bindings-bbcode: 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: docs-key-bindings-table:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsTable.ts --outFile /dev/stdout | node - @./run-ts.sh ./src/generators/docs/keyBindingsTable
docs-key-bindings-fmt: 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

View File

@@ -13,12 +13,13 @@ unprompted reflow of window content.
Windows are automatically centered when possible. And when running out of width, windows can be Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally. 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). [Cardboard](https://gitlab.com/cardboardwm/cardboard).
## Dependencies ## Dependencies
Karousel requires the following QML modules: Karousel requires the following QML modules:
- QtQuick 2.15 - QtQuick 6.0
- org.kde.kwin 3.0 - org.kde.kwin 3.0
- org.kde.notification 1.0 - org.kde.notification 1.0
@@ -52,6 +53,7 @@ Here's the default ones:
| Meta+Ctrl+Shift+End | Move column to end | | Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width | | Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width | | Meta+Ctrl+- | Decrease column width |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) | | Meta+Alt+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+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right | | Meta+Alt+D | Scroll one column to the right |

View File

@@ -11,6 +11,143 @@
<layout class="QVBoxLayout" name="layout_main"> <layout class="QVBoxLayout" name="layout_main">
<item> <item>
<widget class="QTabWidget" name="tabContainer"> <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_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_skipSwitcher">
<property name="text">
<string>Tiled windows skip switcher</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Scrolling mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_scrollingLazy">
<property name="text">
<string>Only scroll as necessary</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingCentered">
<property name="text">
<string>Center focused column</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingGrouped">
<property name="text">
<string>Center visible columns</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Layering mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
<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"> <widget class="QWidget" name="tab_parameters">
<attribute name="title"> <attribute name="title">
<string>Parameters</string> <string>Parameters</string>
@@ -143,14 +280,14 @@
</item> </item>
<item row="6" column="0"> <item row="6" column="0">
<widget class="QLabel" name="label_overscroll"> <widget class="QLabel" name="label_manualScrollStep">
<property name="text"> <property name="text">
<string>Overscroll amount:</string> <string>Manual scroll step size:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="1"> <item row="6" column="1">
<widget class="QSpinBox" name="kcfg_overscroll"> <widget class="QSpinBox" name="kcfg_manualScrollStep">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
</property> </property>
@@ -164,14 +301,14 @@
</item> </item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QLabel" name="label_manualScrollStep"> <widget class="QLabel" name="label_manualResizeStep">
<property name="text"> <property name="text">
<string>Manual scroll step size:</string> <string>Manual resize step size:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="1"> <item row="7" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep"> <widget class="QSpinBox" name="kcfg_manualResizeStep">
<property name="suffix"> <property name="suffix">
<string> px</string> <string> px</string>
</property> </property>
@@ -207,136 +344,6 @@
</layout> </layout>
</widget> </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>
<widget class="QWidget" name="tab_windowRules"> <widget class="QWidget" name="tab_windowRules">
<attribute name="title"> <attribute name="title">
<string>Window Rules</string> <string>Window Rules</string>

View File

@@ -1,7 +1,7 @@
import QtQuick 2.15 import QtQuick 6.0
import org.kde.kwin 3.0 import org.kde.kwin 3.0
import org.kde.notification 1.0 import org.kde.notification 1.0
import "./main.js" as Karousel import "../code/main.js" as Karousel
Item { Item {
id: qmlBase id: qmlBase

View File

@@ -1,20 +1,21 @@
{ {
"KPackageStructure": "KWin/Script",
"KPlugin": { "KPlugin": {
"Name": "Karousel", "Name": "Karousel",
"Description": "Manual columnar tiling extension for KWin", "Description": "Scrollable tiling extension for KWin",
"Icon": "preferences-system-windows", "Icon": "preferences-system-windows",
"Authors": [{ "Authors": [{
"Email": "peter.fajdiga@gmail.com", "Email": "peter.fajdiga@gmail.com",
"Name": "Peter Fajdiga" "Name": "Peter Fajdiga"
}], }],
"Id": "karousel", "Id": "karousel",
"ServiceTypes": ["KWin/Script"], "Version": "0.9.2",
"Version": "0.6",
"License": "GPLv3", "License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel", "Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues" "BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
}, },
"X-Plasma-API": "declarativescript", "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" "X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted"
} }

2
run-ts.sh Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
tsc -p "$1" --outFile ./run-ts-tmp.js && node ./run-ts-tmp.js && rm ./run-ts-tmp.js

View File

@@ -1,10 +0,0 @@
enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}

View File

@@ -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
}
];

View File

@@ -1,6 +0,0 @@
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;
type Notification = {
sendEvent(): void;
};

77
src/extern/kwin.d.ts vendored
View File

@@ -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;
}

View File

@@ -5,9 +5,20 @@ console.log(`<?xml version="1.0" encoding="UTF-8"?>
for (const entry of configDef) { for (const entry of configDef) {
console.log(` <entry name="${entry.name}" type="${entry.type}"> console.log(` <entry name="${entry.name}" type="${entry.type}">
<default>${entry.default}</default> <default>${escapeXml(entry.default)}</default>
</entry>`); </entry>`);
} }
console.log(` </group> console.log(` </group>
</kcfg>`); </kcfg>`);
function escapeXml(input: any) {
if (typeof input === "string") {
return input
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
return input;
}
}

View File

@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["../../lib/**/*", "./**/*"]
}

View File

@@ -1,27 +1,10 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
action: string;
}
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: string;
}
function formatComment(comment: string | undefined) { function formatComment(comment: string | undefined) {
return comment === undefined ? "" : ` (${comment})`; return comment === undefined ? "" : ` (${comment})`;
} }
function printCols(...columns: (string[] | string)[]) { function printCols(...columns: (string[] | string)[]) {
const nCols = columns.length; const nCols = columns.length;
if (nCols == 0) { if (nCols === 0) {
return; return;
} }
@@ -30,7 +13,7 @@ function printCols(...columns: (string[] | string)[]) {
).map( ).map(
(column: string[] | string) => column.length (column: string[] | string) => column.length
)); ));
if (nRows == Infinity) { if (nRows === Infinity) {
// we only have single string columns // we only have single string columns
nRows = 1; nRows = 1;
} }

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -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",
},
];

View File

@@ -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]);
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,47 +1,47 @@
namespace Actions { namespace Actions {
export function init(world: World, config: Config) { export function getAction(world: World, config: Config, name: string) {
return { switch (name) {
focusLeft: () => { case "focus-left": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column); const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) { if (prevColumn === null) {
return; return;
} }
prevColumn.focus(); prevColumn.focus();
}); });
}, };
focusRight: () => { case "focus-right": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column); const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) { if (nextColumn === null) {
return; return;
} }
nextColumn.focus(); nextColumn.focus();
}); });
}, };
focusUp: () => { case "focus-up": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window); const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) { if (prevWindow === null) {
return; return;
} }
prevWindow.focus(); prevWindow.focus();
}); });
}, };
focusDown: () => { case "focus-down": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window); const nextWindow = column.getNextWindow(window);
if (nextWindow === null) { if (nextWindow === null) {
return; return;
} }
nextWindow.focus(); nextWindow.focus();
}); });
}, };
focusStart: () => { case "focus-start": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn(); const firstColumn = grid.getFirstColumn();
@@ -50,9 +50,9 @@ namespace Actions {
} }
firstColumn.focus(); firstColumn.focus();
}); });
}, };
focusEnd: () => { case "focus-end": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn(); const lastColumn = grid.getLastColumn();
@@ -61,10 +61,10 @@ namespace Actions {
} }
lastColumn.focus(); lastColumn.focus();
}); });
}, };
windowMoveLeft: () => { case "window-move-left": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) { if (column.getWindowCount() === 1) {
// move from own column into existing column // move from own column into existing column
const prevColumn = grid.getPrevColumn(column); const prevColumn = grid.getPrevColumn(column);
@@ -79,10 +79,10 @@ namespace Actions {
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
} }
}); });
}, };
windowMoveRight: () => { case "window-move-right": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) { if (column.getWindowCount() === 1) {
// move from own column into existing column // move from own column into existing column
const nextColumn = grid.getNextColumn(column); const nextColumn = grid.getNextColumn(column);
@@ -97,100 +97,100 @@ namespace Actions {
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
} }
}); });
}, };
windowMoveUp: () => { case "window-move-up": return () => {
// TODO (optimization): only arrange moved windows // TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window); column.moveWindowUp(window);
}); });
}, };
windowMoveDown: () => { case "window-move-down": return () => {
// TODO (optimization): only arrange moved windows // TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window); column.moveWindowDown(window);
}); });
}, };
windowMoveStart: () => { case "window-move-start": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null); const newColumn = new Column(grid, null);
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
}); });
}, };
windowMoveEnd: () => { case "window-move-end": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn()); const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
}); });
}, };
windowToggleFloating: () => { case "window-toggle-floating": return () => {
const kwinClient = workspace.activeClient; const kwinClient = Workspace.activeWindow;
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient); clientManager.toggleFloatingClient(kwinClient);
}); });
}, };
columnMoveLeft: () => { case "column-move-left": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column); grid.moveColumnLeft(column);
}); });
}, };
columnMoveRight: () => { case "column-move-right": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column); grid.moveColumnRight(column);
}); });
}, };
columnMoveStart: () => { case "column-move-start": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(null); column.moveAfter(null);
}); });
}, };
columnMoveEnd: () => { case "column-move-end": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn()); column.moveAfter(grid.getLastColumn());
}); });
}, };
columnToggleStacked: () => { case "column-toggle-stacked": return () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked(); column.toggleStacked();
}); });
}, };
columnWidthIncrease: () => { case "column-width-increase": return () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
grid.increaseColumnWidth(column); config.columnResizer.increaseWidth(column, config.manualResizeStep);
}); });
}, };
columnWidthDecrease: () => { case "column-width-decrease": return () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
grid.decreaseColumnWidth(column); config.columnResizer.decreaseWidth(column, config.manualResizeStep);
}); });
}, };
columnsWidthEqualize: () => { case "columns-width-equalize": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths(); desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
}); });
}, };
gridScrollLeft: () => { case "grid-scroll-left": return () => {
gridScroll(world, -config.manualScrollStep); gridScroll(world, -config.manualScrollStep);
}, };
gridScrollRight: () => { case "grid-scroll-right": return () => {
gridScroll(world, config.manualScrollStep); gridScroll(world, config.manualScrollStep);
}, };
gridScrollStart: () => { case "grid-scroll-start": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn(); const firstColumn = grid.getFirstColumn();
@@ -199,9 +199,9 @@ namespace Actions {
} }
grid.desktop.scrollToColumn(firstColumn); grid.desktop.scrollToColumn(firstColumn);
}); });
}, };
gridScrollEnd: () => { case "grid-scroll-end": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn(); const lastColumn = grid.getLastColumn();
@@ -210,15 +210,15 @@ namespace Actions {
} }
grid.desktop.scrollToColumn(lastColumn); grid.desktop.scrollToColumn(lastColumn);
}); });
}, };
gridScrollFocused: () => { case "grid-scroll-focused": return () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column); grid.desktop.scrollCenterRange(column);
}) })
}, };
gridScrollLeftColumn: () => { case "grid-scroll-left-column": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true); const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
@@ -233,9 +233,9 @@ namespace Actions {
grid.desktop.scrollToColumn(prevColumn); grid.desktop.scrollToColumn(prevColumn);
}); });
}, };
gridScrollRightColumn: () => { case "grid-scroll-right-column": return () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true); const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
@@ -250,39 +250,41 @@ namespace Actions {
grid.desktop.scrollToColumn(nextColumn); grid.desktop.scrollToColumn(nextColumn);
}); });
}, };
};
default: throw new Error("unknown action: " + name);
}
} }
export function initNum(world: World) { export function getNumAction(world: World, name: string) {
return { switch (name) {
focusColumn: (columnIndex: number) => { case "focus-": return (columnIndex: number) => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid; const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex); const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) { if (targetColumn === null) {
return null; return;
} }
targetColumn.focus(); targetColumn.focus();
}); });
}, };
windowMoveToColumn: (columnIndex: number) => { case "window-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex); const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) { if (targetColumn === null) {
return null; return;
} }
window.moveToColumn(targetColumn); window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll(); grid.desktop.autoAdjustScroll();
}); });
}, };
columnMoveToColumn: (columnIndex: number) => { case "column-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex); const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) { if (targetColumn === null || targetColumn === column) {
return null; return;
} }
if (targetColumn.isAfter(column)) { if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn); column.moveAfter(targetColumn);
@@ -290,30 +292,38 @@ namespace Actions {
column.moveAfter(grid.getPrevColumn(targetColumn)); column.moveAfter(grid.getPrevColumn(targetColumn));
} }
}); });
}, };
columnMoveToDesktop: (desktopIndex: number) => { case "column-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1; const kwinDesktop = Workspace.desktops[desktopIndex];
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid; if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) { if (newGrid === null || newGrid === oldGrid) {
return; return;
} }
column.moveToGrid(newGrid, newGrid.getLastColumn()); column.moveToGrid(newGrid, newGrid.getLastColumn());
}); });
}, };
tailMoveToDesktop: (desktopIndex: number) => { case "tail-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => { world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1; const kwinDesktop = Workspace.desktops[desktopIndex];
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid; if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) { if (newGrid === null || newGrid === oldGrid) {
return; return;
} }
oldGrid.evacuateTail(newGrid, column); oldGrid.evacuateTail(newGrid, column);
}); });
}, };
};
default: throw new Error("unknown num action: " + name);
}
} }
function gridScroll(world: World, amount: number) { function gridScroll(world: World, amount: number) {
@@ -325,5 +335,12 @@ namespace Actions {
export type Config = { export type Config = {
manualScrollStep: number, manualScrollStep: number,
manualResizeStep: number,
columnResizer: ColumnResizer,
}; };
export type ColumnResizer = {
increaseWidth(column: Column, step: number): void,
decreaseWidth(column: Column, step: number): void,
}
} }

View File

@@ -0,0 +1,96 @@
class ContextualResizer {
public increaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
return;
}
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() + step,
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
],
width => width - column.getWidth(),
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
public decreaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
const visibleColumnsWidth = rightVisibleColumn.getRight() - leftVisibleColumn.getLeft();
const unusedWidth = visibleRange.getWidth() - visibleColumnsWidth;
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() - step,
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
],
width => column.getWidth() - width,
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
let bestScore = Infinity;
let bestStep = undefined;
for (const step of steps) {
const score = evaluate(step);
if (score > 0 && score < bestScore) {
bestScore = score;
bestStep = step;
}
}
return bestStep;
}
}

View File

@@ -0,0 +1,9 @@
class RawResizer {
public increaseWidth(column: Column, step: number) {
column.adjustWidth(step, true);
}
public decreaseWidth(column: Column, step: number) {
column.adjustWidth(-step, true);
}
}

View File

@@ -1,13 +1,5 @@
class ScrollerCentered { class CenterClamper {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterRange(column);
}
public clampScrollX(desktop: Desktop, x: number) { public clampScrollX(desktop: Desktop, x: number) {
return ScrollerCentered.clampScrollX(desktop, x);
}
public static clampScrollX(desktop: Desktop, x: number) {
const firstColumn = desktop.grid.getFirstColumn(); const firstColumn = desktop.grid.getFirstColumn();
if (firstColumn === null) { if (firstColumn === null) {
return 0; return 0;

View File

@@ -1,8 +1,4 @@
class ScrollerLazy { class EdgeClamper {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollToRange(column);
}
public clampScrollX(desktop: Desktop, x: number) { public clampScrollX(desktop: Desktop, x: number) {
let minScroll = 0; let minScroll = 0;
let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width; let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;

View File

@@ -0,0 +1,5 @@
class CenteredScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterRange(column);
}
}

View File

@@ -0,0 +1,5 @@
class GroupedScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterVisible(column);
}
}

View File

@@ -0,0 +1,5 @@
class LazyScroller {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollIntoView(column);
}
}

View File

@@ -5,8 +5,8 @@ type Config = {
gapsOuterRight: number, gapsOuterRight: number,
gapsInnerHorizontal: number, gapsInnerHorizontal: number,
gapsInnerVertical: number, gapsInnerVertical: number,
overscroll: number,
manualScrollStep: number, manualScrollStep: number,
manualResizeStep: number,
offScreenOpacity: number, offScreenOpacity: number,
untileOnDrag: boolean, untileOnDrag: boolean,
stackColumnsByDefault: boolean, stackColumnsByDefault: boolean,

View File

@@ -0,0 +1,167 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"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": "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: "manualScrollStep",
type: "UInt",
default: 200,
},
{
name: "manualResizeStep",
type: "UInt",
default: 600,
},
{
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: "noLayering",
type: "Bool",
default: false,
},
{
name: "windowRules",
type: "String",
default: defaultWindowRules,
}
];

6
src/lib/extern/global.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare const console: Console;
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;

103
src/lib/extern/kwin.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
type KWin = {
readConfig(key: string, defaultValue: any): any;
};
type 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;
readonly currentDesktopChanged: QSignal<[]>
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: 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,
}
type Tile = unknown;
type Output = unknown;
interface KwinClient {
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient;
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;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile;
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 {
readonly id: string;
}
type ShortcutHandler = {
readonly activated: QSignal<[]>;
destroy(): void;
};

3
src/lib/extern/notification.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
type Notification = {
sendEvent(): void;
};

View File

@@ -1,16 +1,20 @@
declare const console: { type Console = {
log(...args: any[]); log(...args: any[]): void;
assert(boolean); trace(): void;
}; assert(assertion: boolean, message?: string): void;
}
declare const Qt: { type Qt = {
rect(x: number, y: number, width: number, height: number): QmlRect; rect(x: number, y: number, width: number, height: number): QmlRect;
createQmlObject(qml: string, parent: QmlObject); createQmlObject(qml: string, parent: QmlObject): QmlObject;
}; }
type QmlObject = unknown; type QmlObject = unknown;
type QByteArray = string; type QmlPoint = {
x: number;
y: number;
}
type QmlRect = { type QmlRect = {
x: number; x: number;
@@ -35,7 +39,7 @@ type QSignal<T extends unknown[]> = {
type QmlTimer = { type QmlTimer = {
interval: number; interval: number;
triggered: QSignal<[void]>; readonly triggered: QSignal<[]>;
restart(): void; restart(): void;
destroy(): void; destroy(): void;
}; };

View File

@@ -0,0 +1,185 @@
const keyBindings: KeyBinding[] = [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
},
{
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",
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
},
];
const numKeyBindings: NumKeyBinding[] = [
{
name: "focus-",
description: "Move focus to column ",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
},
{
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,
},
{
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,
},
{
name: "column-move-to-desktop-",
description: "Move column to desktop ",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
},
{
name: "tail-move-to-desktop-",
description: "Move this and all following columns to desktop ",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
},
];

View File

@@ -0,0 +1,67 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(world: World, config: Actions.Config, shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(Actions.getAction(world, config, keyBinding.name)),
));
}
function registerNumKeyBindings(world: World, 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 :
"";
const action = Actions.getNumAction(world, numKeyBinding.name);
shortcutActions.push(new ShortcutAction(
{
name: numKeyBinding.name + numKey,
description: numKeyBinding.description + numKey,
defaultKeySequence: keySequence,
},
catchWrap(() => action(i)),
));
}
}
// TODO: refactor
function registerKeyBindings(world: World, config: Actions.Config) {
const shortcutActions: ShortcutAction[] = [];
for (const keyBinding of keyBindings) {
registerKeyBinding(world, config, shortcutActions, keyBinding);
}
for (const numKeyBinding of numKeyBindings) {
registerNumKeyBindings(world, shortcutActions, numKeyBinding);
}
return shortcutActions;
}

View File

@@ -25,7 +25,7 @@ class Column {
this.grid = targetGrid; this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn); targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber; window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
} }
} }
} }
@@ -92,16 +92,17 @@ class Column {
public setWidth(width: number, setPreferred: boolean) { public setWidth(width: number, setPreferred: boolean) {
width = clamp(width, this.getMinWidth(), this.getMaxWidth()); width = clamp(width, this.getMinWidth(), this.getMaxWidth());
const oldWidth = this.width; if (width === this.width) {
return;
}
this.width = width; this.width = width;
if (setPreferred) { if (setPreferred) {
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.preferredWidth = width; window.client.preferredWidth = width;
} }
} }
if (width !== oldWidth) { this.grid.onColumnWidthChanged(this);
this.grid.onColumnWidthChanged(this, oldWidth, width);
}
} }
public adjustWidth(widthDelta: number, setPreferred: boolean) { public adjustWidth(widthDelta: number, setPreferred: boolean) {

View File

@@ -1,6 +1,6 @@
class Desktop { class Desktop {
public readonly grid: Grid; public readonly grid: Grid;
public readonly desktopNumber: number; public readonly kwinDesktop: KwinDesktop;
private readonly pinManager: PinManager; private readonly pinManager: PinManager;
private readonly config: Desktop.Config; private readonly config: Desktop.Config;
private scrollX: number; private scrollX: number;
@@ -10,26 +10,26 @@ class Desktop {
public clientArea: QmlRect; public clientArea: QmlRect;
public tilingArea: QmlRect; public tilingArea: QmlRect;
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) { constructor(kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager; this.pinManager = pinManager;
this.config = config; this.config = config;
this.scrollX = 0; this.scrollX = 0;
this.dirty = true; this.dirty = true;
this.dirtyScroll = true; this.dirtyScroll = true;
this.dirtyPins = true; this.dirtyPins = true;
this.desktopNumber = desktopNumber; this.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig); this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber); this.clientArea = Desktop.getClientArea(kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config); this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
} }
private updateArea() { private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber); const newClientArea = Desktop.getClientArea(this.kwinDesktop);
if (newClientArea === this.clientArea && !this.dirtyPins) { if (newClientArea === this.clientArea && !this.dirtyPins) {
return; return;
} }
this.clientArea = newClientArea; this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config); this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
this.dirty = true; this.dirty = true;
this.dirtyScroll = true; this.dirtyScroll = true;
this.dirtyPins = false; this.dirtyPins = false;
@@ -37,12 +37,12 @@ class Desktop {
this.autoAdjustScroll(); this.autoAdjustScroll();
} }
private static getClientArea(desktopNumber: number) { private static getClientArea(kwinDesktop: KwinDesktop) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber); return Workspace.clientArea(ClientAreaOption.PlacementArea, Workspace.activeScreen, kwinDesktop);
} }
private static getTilingArea(clientArea: QmlRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) { private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea); const availableSpace = pinManager.getAvailableSpace(kwinDesktop, clientArea);
const top = availableSpace.top + config.marginTop; const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom; const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft; const left = availableSpace.left + config.marginLeft;
@@ -55,10 +55,9 @@ class Desktop {
) )
} }
// calculates a Range that scrolls the contained Range into view public scrollIntoView(range: Desktop.Range) {
public calculateVisibleRange(containedRange: Desktop.Range) { const left = range.getLeft();
const left = containedRange.getLeft(); const right = range.getRight();
const right = containedRange.getRight();
const initialVisibleRange = this.getCurrentVisibleRange(); const initialVisibleRange = this.getCurrentVisibleRange();
let targetScrollX: number; let targetScrollX: number;
@@ -67,26 +66,10 @@ class Desktop {
} else if (right > initialVisibleRange.getRight()) { } else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width; targetScrollX = right - this.tilingArea.width;
} else { } else {
return this.getVisibleRange(this.clampScrollX(this.scrollX)); targetScrollX = initialVisibleRange.getLeft();
} }
const overscroll = this.getTargetOverscroll(targetScrollX, left < initialVisibleRange.getLeft()); this.setScroll(targetScrollX, false);
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) { public scrollCenterRange(range: Desktop.Range) {
@@ -95,6 +78,13 @@ class Desktop {
this.adjustScroll(Math.round(windowCenter - screenCenter), false); this.adjustScroll(Math.round(windowCenter - screenCenter), false);
} }
public scrollCenterVisible(focusedColumn: Column) {
const columnRange = new Desktop.ColumnRange(focusedColumn);
const visibleRange = this.getCurrentVisibleRange();
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal);
this.scrollCenterRange(columnRange);
}
public autoAdjustScroll() { public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn(); const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null || focusedColumn.grid !== this.grid) { if (focusedColumn === null || focusedColumn.grid !== this.grid) {
@@ -119,7 +109,7 @@ class Desktop {
} }
private clampScrollX(x: number) { private clampScrollX(x: number) {
return this.config.scroller.clampScrollX(this, x); return this.config.clamper.clampScrollX(this, x);
} }
public setScroll(x: number, force: boolean) { public setScroll(x: number, force: boolean) {
@@ -141,18 +131,32 @@ class Desktop {
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal; let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length; let remainingColumns = visibleColumns.length;
for (const column of visibleColumns) {
const columnWidth = Math.round(remainingWidth / remainingColumns); const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
column.setWidth(columnWidth, true); for (const minWidth of minWidths) {
remainingWidth -= columnWidth; if (minWidth > remainingWidth / remainingColumns) {
remainingColumns--; remainingWidth -= minWidth;
remainingColumns--;
}
} }
const targetVisibleRange = Desktop.RangeImpl.fromRanges( const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0], visibleColumns[0],
visibleColumns[visibleColumns.length - 1], visibleColumns[visibleColumns.length - 1],
); ));
this.setScroll(this.calculateVisibleRange(targetVisibleRange).getLeft(), false);
} }
public arrange() { public arrange() {
@@ -187,8 +191,8 @@ namespace Desktop {
marginBottom: number, marginBottom: number,
marginLeft: number, marginLeft: number,
marginRight: number, marginRight: number,
overscroll: number,
scroller: Desktop.Scroller, scroller: Desktop.Scroller,
clamper: Desktop.Clamper,
}; };
export type Range = { export type Range = {
@@ -225,8 +229,83 @@ namespace Desktop {
} }
} }
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
function canFit(column: Column) {
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth();
}
function isUsable(column: Column|null) {
return column !== null && canFit(column);
}
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
}
if (!isUsable(rightColumn)) {
rightColumn = null;
}
}
checkColumns();
const visibleCenter = visibleRange.getLeft() + visibleRange.getWidth() / 2;
while (leftColumn !== null || rightColumn !== null) {
const leftToCenter = leftColumn === null ? Infinity : Math.abs(leftColumn.getLeft() - visibleCenter);
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getPrevColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getNextColumn(rightColumn!);
}
checkColumns();
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
export type Scroller = { export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void; scrollToColumn(desktop: Desktop, column: Column): void;
}
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number; clampScrollX(desktop: Desktop, x: number): number;
} }
} }

View File

@@ -43,6 +43,10 @@ class Grid {
return this.width; return this.width;
} }
public isUserResizing() {
return this.userResize;
}
public getPrevColumn(column: Column) { public getPrevColumn(column: Column) {
return this.columns.getPrev(column); return this.columns.getPrev(column);
} }
@@ -134,93 +138,6 @@ class Grid {
return width; 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) { public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize); column.arrange(x, visibleRange, this.userResize);
@@ -272,13 +189,13 @@ class Grid {
this.desktop.autoAdjustScroll(); this.desktop.autoAdjustScroll();
} }
public onColumnWidthChanged(column: Column, oldWidth: number, width: number) { public onColumnWidthChanged(column: Column) {
const nextColumn = this.columns.getNext(column); const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn); this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) { if (!this.userResize) {
this.desktop.autoAdjustScroll(); this.desktop.autoAdjustScroll();
} }
this.desktop.onLayoutChanged();
} }
public onColumnFocused(column: Column) { public onColumnFocused(column: Column) {

View File

@@ -10,8 +10,7 @@ class Window {
this.height = client.kwinClient.frameGeometry.height; this.height = client.kwinClient.frameGeometry.height;
this.focusedState = { this.focusedState = {
fullScreen: false, fullScreen: false,
maximizedHorizontally: false, maximizedMode: MaximizedMode.Unmaximized,
maximizedVertically: false,
}; };
this.skipArrange = false; this.skipArrange = false;
this.column = column; this.column = column;
@@ -37,8 +36,11 @@ class Window {
if (this.column.grid.config.reMaximize && this.isFocused()) { if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement // do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all) // (otherwise placement may not happen at all)
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) { if (this.focusedState.maximizedMode > MaximizedMode.Unmaximized) {
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally); 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; maximized = true;
} }
if (this.focusedState.fullScreen) { if (this.focusedState.fullScreen) {
@@ -76,8 +78,8 @@ class Window {
this.column.grid.desktop.onLayoutChanged(); this.column.grid.desktop.onLayoutChanged();
} }
public onMaximizedChanged(horizontally: boolean, vertically: boolean) { public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = horizontally || vertically; const maximized = maximizedMode > MaximizedMode.Unmaximized;
this.skipArrange = maximized; this.skipArrange = maximized;
if (this.column.grid.config.tiledKeepBelow) { if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized; this.client.kwinClient.keepBelow = !maximized;
@@ -86,8 +88,7 @@ class Window {
this.client.kwinClient.keepAbove = maximized; this.client.kwinClient.keepAbove = maximized;
} }
if (this.isFocused()) { if (this.isFocused()) {
this.focusedState.maximizedHorizontally = horizontally; this.focusedState.maximizedMode = maximizedMode;
this.focusedState.maximizedVertically = vertically;
} }
this.column.grid.desktop.onLayoutChanged(); this.column.grid.desktop.onLayoutChanged();
} }
@@ -145,7 +146,6 @@ class Window {
namespace Window { namespace Window {
export type State = { export type State = {
fullScreen: boolean, fullScreen: boolean,
maximizedHorizontally: boolean, maximizedMode: MaximizedMode,
maximizedVertically: boolean,
} }
} }

View 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;
}
}

View File

@@ -0,0 +1,5 @@
type WindowRule = {
class: string | undefined,
caption: string | undefined,
tile: boolean,
};

View 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 Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!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 = 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 + ")";
}
}

View File

@@ -17,8 +17,8 @@ class Delayer {
} }
function initQmlTimer() { function initQmlTimer() {
return Qt.createQmlObject( return <QmlTimer>Qt.createQmlObject(
`import QtQuick 2.15 `import QtQuick 6.0
Timer {}`, Timer {}`,
qmlBase qmlBase
); );

View File

@@ -0,0 +1,25 @@
class ShortcutAction {
private readonly shortcutHandler: ShortcutHandler;
constructor(keyBinding: KeyBinding, f: () => void) {
this.shortcutHandler = ShortcutAction.initShortcutHandler(keyBinding);
this.shortcutHandler.activated.connect(f);
}
public destroy() {
this.shortcutHandler.destroy();
}
private static initShortcutHandler(keyBinding: KeyBinding) {
return <ShortcutHandler>Qt.createQmlObject(
`import QtQuick 6.0
import org.kde.kwin 3.0
ShortcutHandler {
name: "karousel-${keyBinding.name}";
text: "Karousel: ${keyBinding.description}";
sequence: "${keyBinding.defaultKeySequence}";
}`,
qmlBase,
);
}
}

54
src/lib/workspace.ts Normal file
View File

@@ -0,0 +1,54 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
Clients.makeTileable(kwinClient);
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
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.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
})
});
manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
})
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -27,13 +27,13 @@ class ClientManager {
public addClient(kwinClient: KwinClient) { public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient)); console.assert(!this.hasClient(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
let constructState: (client: ClientWrapper) => ClientState.State; let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) { if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient); constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) { } else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid; constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, grid);
} else { } else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false); constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
} }
@@ -76,41 +76,41 @@ class ClientManager {
return; return;
} }
if (client.stateManager.getState() instanceof ClientState.Tiled) { if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.TiledMinimized(), kwinClient === this.lastFocusedClient); client.stateManager.setState(
() => new ClientState.TiledMinimized(this.world, client),
kwinClient === this.lastFocusedClient,
);
} }
} }
public unminimizeClient(kwinClient: KwinClient) { public tileClient(client: ClientWrapper, grid: Grid) {
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;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) { if (client.stateManager.getState() instanceof ClientState.Tiled) {
return; 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), false);
} }
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), false);
}
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
const client = this.clientMap.get(kwinClient); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
return; return;
} }
if (client.stateManager.getState() instanceof ClientState.Tiled) { this.tileClient(client, grid);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false); }
public floatKwinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
} }
this.floatClient(client);
} }
public pinClient(kwinClient: KwinClient) { public pinClient(kwinClient: KwinClient) {
@@ -147,8 +147,11 @@ class ClientManager {
const clientState = client.stateManager.getState(); const clientState = client.stateManager.getState();
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) { if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient); Clients.makeTileable(kwinClient);
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid; const desktop = this.desktopManager.getDesktopForClient(kwinClient);
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false); if (desktop === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
} else if (clientState instanceof ClientState.Tiled) { } 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), false);
} }

View File

@@ -3,8 +3,10 @@ class ClientWrapper {
public readonly stateManager: ClientState.Manager; public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null; public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[]; private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null; private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number; public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer; private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19 private lastPlacement: QmlRect | null; // workaround for issue #19
@@ -20,6 +22,7 @@ class ClientWrapper {
if (transientFor !== null) { if (transientFor !== null) {
transientFor.addTransient(this); transientFor.addTransient(this);
} }
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager; this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width; this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer(); this.manipulatingGeometry = new Doer();
@@ -35,12 +38,18 @@ class ClientWrapper {
} }
this.lastPlacement = Qt.rect(x, y, width, height); this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement; 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.stateManager.getState() instanceof ClientState.Floating) {
if (this.kwinClient.desktop === desktopNumber) { if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect( this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx, frame.x + dx,
@@ -51,32 +60,52 @@ class ClientWrapper {
} }
for (const transient of this.transients) { for (const transient of this.transients) {
transient.moveTransient(dx, dy, desktopNumber); transient.moveTransient(dx, dy, kwinDesktops);
} }
} }
} }
public moveTransients(dx: number, dy: number) { public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) { for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktop); transient.moveTransient(dx, dy, this.kwinClient.desktops);
} }
} }
public focus() { public focus() {
workspace.activeClient = this.kwinClient; Workspace.activeWindow = this.kwinClient;
} }
public isFocused() { public isFocused() {
return workspace.activeClient === this.kwinClient; return Workspace.activeWindow === this.kwinClient;
} }
public setMaximize(horizontally: boolean, vertically: boolean) { public setMaximize(horizontally: boolean, vertically: boolean) {
if (!this.kwinClient.maximizable) {
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.manipulatingGeometry.do(() => {
this.kwinClient.setMaximize(vertically, horizontally); this.kwinClient.setMaximize(vertically, horizontally);
}); });
} }
public setFullScreen(fullScreen: boolean) { public setFullScreen(fullScreen: boolean) {
if (!this.kwinClient.fullScreenable) {
return;
}
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen; this.kwinClient.fullScreen = fullScreen;
}); });
@@ -92,6 +121,10 @@ class ClientWrapper {
return this.kwinClient.shade; return this.kwinClient.shade;
} }
public getMaximizedMode() {
return this.maximizedMode;
}
public isManipulatingGeometry(newGeometry: QmlRect | null) { public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) { if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true; return true;
@@ -118,7 +151,7 @@ class ClientWrapper {
} }
public ensureVisible(screenSize: QmlRect) { public ensureVisible(screenSize: QmlRect) {
if (this.kwinClient.desktop !== workspace.currentDesktop) { if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return; return;
} }
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
@@ -131,6 +164,7 @@ class ClientWrapper {
public destroy(passFocus: boolean) { public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus); this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) { if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy(); this.rulesSignalManager.destroy();
} }
@@ -141,4 +175,17 @@ class ClientWrapper {
transient.transientFor = null; 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;
}
} }

49
src/lib/world/Clients.ts Normal file
View File

@@ -0,0 +1,49 @@
namespace Clients {
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable && kwinClient.resizeable && !kwinClient.popupWindow;
}
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.frameGeometry === fullScreenArea;
}
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));
}
}

View File

@@ -0,0 +1,127 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string, currentDesktop: KwinDesktop) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktops = new Map();
this.kwinActivities = new Set(Workspace.activities);
this.kwinDesktops = new Set(Workspace.desktops);
this.addDesktop(currentActivity, currentDesktop);
}
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
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.layoutConfig);
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;
}
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;
}
}
}
}
}

View File

@@ -13,11 +13,11 @@ class PinManager {
this.pinnedClients.delete(kwinClient); 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); const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
let lots = [baseLot]; let lots = [baseLot];
for (const client of this.pinnedClients) { for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, desktopNumber)) { if (!Clients.isOnVirtualDesktop(client, kwinDesktop)) {
continue; continue;
} }

View File

@@ -4,16 +4,21 @@ class World {
public readonly clientManager: ClientManager; public readonly clientManager: ClientManager;
private readonly pinManager: PinManager; private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager; private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer; private readonly screenResizedDelayer: Delayer;
constructor(config: Config) { constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag; this.untileOnDrag = config.untileOnDrag;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this); this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
this.shortcutActions = registerKeyBindings(this, {
manualScrollStep: config.manualScrollStep,
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
});
this.screenResizedDelayer = new Delayer(1000, () => { this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks get taken into account by `workspace.clientArea` // this delay ensures that docks are taken into account by `Workspace.clientArea`
const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine for (const desktop of this.desktopManager.getAllDesktops()) {
for (const desktop of desktopManager.desktops()) {
desktop.onLayoutChanged(); desktop.onLayoutChanged();
} }
this.update(); this.update();
@@ -40,32 +45,39 @@ class World {
marginBottom: config.gapsOuterBottom, marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft, marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight, marginRight: config.gapsOuterRight,
overscroll: config.overscroll, scroller: World.createScroller(config),
scroller: config.scrollingLazy ? new ScrollerLazy() : clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
config.scrollingCentered ? new ScrollerCentered() :
config.scrollingGrouped ? new ScrollerGrouped(layoutConfig) :
console.assert(false),
}, },
layoutConfig, layoutConfig,
workspace.currentActivity, Workspace.currentActivity,
Workspace.currentDesktop,
); );
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager); this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients(); this.addExistingClients();
this.update(); 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() { private addExistingClients() {
const kwinClients = workspace.clientList(); const kwinClients = Workspace.windows;
for (let i = 0; i < kwinClients.length; i++) { for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i]; const kwinClient = kwinClients[i];
this.clientManager.addClient(kwinClient); this.clientManager.addClient(kwinClient);
} }
} }
public updateDesktops() {
this.desktopManager.update();
}
private update() { private update() {
this.desktopManager.getCurrentDesktop().arrange(); this.desktopManager.getCurrentDesktop().arrange();
} }
@@ -94,11 +106,14 @@ class World {
followTransient: boolean, followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void, f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) { ) {
this.doIfTiled(workspace.activeClient, followTransient, f); this.doIfTiled(Workspace.activeWindow, followTransient, f);
} }
public destroy() { public destroy() {
this.workspaceSignalManager.destroy(); this.workspaceSignalManager.destroy();
for (const shortcutAction of this.shortcutActions) {
shortcutAction.destroy();
}
this.clientManager.destroy(); this.clientManager.destroy();
this.desktopManager.destroy(); this.desktopManager.destroy();
} }

View File

@@ -16,7 +16,7 @@ namespace ClientState {
private static initSignalManager(world: World, kwinClient: KwinClient) { private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => { manager.connect(kwinClient.frameGeometryChanged, () => {
world.onScreenResized(); world.onScreenResized();
}); });
return manager; return manager;

View File

@@ -24,7 +24,11 @@ namespace ClientState {
} }
private static limitHeight(client: ClientWrapper) { 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 clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth; const width = client.preferredWidth;
client.place( client.place(
@@ -54,7 +58,7 @@ namespace ClientState {
clientManager.pinClient(kwinClient); clientManager.pinClient(kwinClient);
}); });
} }
}) });
return manager; return manager;
} }

View File

@@ -30,8 +30,8 @@ namespace ClientState {
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) { private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
const manager = new SignalManager(); const manager = new SignalManager();
let oldDesktopNumber = kwinClient.desktop;
let oldActivities = kwinClient.activities; let oldActivities = kwinClient.activities;
let oldDesktops = kwinClient.desktops;
manager.connect(kwinClient.tileChanged, () => { manager.connect(kwinClient.tileChanged, () => {
if (kwinClient.tile === null) { if (kwinClient.tile === null) {
@@ -41,7 +41,7 @@ namespace ClientState {
} }
}); });
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => { manager.connect(kwinClient.frameGeometryChanged, () => {
if (kwinClient.tile === null) { if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient); clientManager.unpinClient(kwinClient);
@@ -56,25 +56,24 @@ namespace ClientState {
}) })
}); });
manager.connect(kwinClient.desktopChanged, () => { manager.connect(kwinClient.desktopsChanged, () => {
const changedDesktops = oldDesktopNumber === -1 || kwinClient.desktop === -1 ? const changedDesktops = oldDesktops.length === 0 || kwinClient.desktops.length === 0 ?
[] : [] :
[oldDesktopNumber, kwinClient.desktop]; union(oldDesktops, kwinClient.desktops);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedDesktops, kwinClient.activities)) { for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
desktop.onPinsChanged(); desktop.onPinsChanged();
} }
}); });
oldDesktopNumber = kwinClient.desktop; oldDesktops = kwinClient.desktops;
}); });
manager.connect(kwinClient.activitiesChanged, (kwinClient: KwinClient) => { manager.connect(kwinClient.activitiesChanged, () => {
const desktops = kwinClient.desktop === -1 ? [] : [kwinClient.desktop];
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ? const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
[] : [] :
union(oldActivities, kwinClient.activities); union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(desktops, changedActivities)) { for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
desktop.onPinsChanged(); desktop.onPinsChanged();
} }
}); });

View File

@@ -31,59 +31,71 @@ namespace ClientState {
const kwinClient = client.kwinClient; const kwinClient = client.kwinClient;
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => { manager.connect(kwinClient.desktopsChanged, () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (kwinClient.desktop === -1) { const desktop = desktopManager.getDesktopForClient(kwinClient);
// windows on all desktops are not supported if (desktop === undefined) {
clientManager.untileClient(kwinClient); // windows on multiple desktops are not supported
clientManager.floatKwinClient(kwinClient);
return; return;
} }
Tiled.moveWindowToCorrectGrid(desktopManager, window); Tiled.moveWindowToGrid(window, desktop.grid);
}); });
}); });
manager.connect(kwinClient.activitiesChanged, () => { manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (kwinClient.activities.length !== 1) { const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported // windows on multiple activities are not supported
clientManager.untileClient(kwinClient); clientManager.floatKwinClient(kwinClient);
return; return;
} }
Tiled.moveWindowToCorrectGrid(desktopManager, window); Tiled.moveWindowToGrid(window, desktop.grid);
}); });
}) })
let lastResize = false; manager.connect(kwinClient.minimizedChanged, () => {
manager.connect(kwinClient.moveResizedChanged, () => { console.assert(kwinClient.minimized);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (kwinClient.move) { clientManager.minimizeClient(kwinClient);
if (world.untileOnDrag) {
clientManager.untileClient(kwinClient);
}
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
}); });
}); });
let cursorChangedAfterResizeStart = false; manager.connect(kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
manager.connect(kwinClient.moveResizeCursorChanged, () => { world.do(() => {
cursorChangedAfterResizeStart = true; window.onMaximizedChanged(maximizedMode);
}); });
manager.connect(kwinClient.clientStartUserMovedResized, () => {
cursorChangedAfterResizeStart = false;
}); });
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => { let resizing = false;
let resizingBorder = false;
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
if (kwinClient.move) {
if (world.untileOnDrag) {
world.do((clientManager, desktopManager) => {
clientManager.floatKwinClient(kwinClient);
});
}
return;
}
if (kwinClient.resize) {
resizing = true;
resizingBorder = Workspace.cursorPos.x > kwinClient.clientGeometry.right ||
Workspace.cursorPos.x < kwinClient.clientGeometry.left;
window.column.grid.onUserResizeStarted();
}
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (resizing) {
resizing = false;
window.column.grid.onUserResizeFinished();
}
});
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged` // on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) { if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
@@ -104,10 +116,11 @@ namespace ClientState {
} }
if (kwinClient.resize) { if (kwinClient.resize) {
world.do(() => window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart)); world.do(() => window.onUserResize(oldGeometry, resizingBorder));
} else if ( } else if (
!window.column.grid.isUserResizing() &&
!client.isManipulatingGeometry(newGeometry) && !client.isManipulatingGeometry(newGeometry) &&
!Clients.isMaximizedGeometry(kwinClient) && client.getMaximizedMode() === MaximizedMode.Unmaximized &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point !Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) { ) {
world.do(() => window.onFrameGeometryChanged()); world.do(() => window.onFrameGeometryChanged());
@@ -130,17 +143,13 @@ namespace ClientState {
return manager; return manager;
} }
private static moveWindowToCorrectGrid(desktopManager: DesktopManager, window: Window) { private static moveWindowToGrid(window: Window, grid: Grid) {
const kwinClient = window.client.kwinClient; if (grid === window.column.grid) {
// window already on the given grid
const oldGrid = window.column.grid;
const newGrid = desktopManager.getDesktopForClient(kwinClient).grid;
if (oldGrid === newGrid) {
// window already on the correct grid
return; return;
} }
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn()); const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
} }
@@ -165,6 +174,9 @@ namespace ClientState {
if (config.tiledKeepBelow) { if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = false; client.kwinClient.keepBelow = false;
} }
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false); client.setShade(false);
client.setFullScreen(false); client.setFullScreen(false);
if (client.kwinClient.tile === null) { if (client.kwinClient.tile === null) {

View 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: boolean) {
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;
}
}
}

View File

@@ -1,6 +0,0 @@
function init() {
const config = loadConfig();
const world = new World(config);
registerKeyBindings(world, config);
return world;
}

3
src/main/main.ts Normal file
View File

@@ -0,0 +1,3 @@
function init() {
return new World(loadConfig());
}

4
src/main/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["../lib/**/*", "./**/*"]
}

View File

@@ -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);
}
}

View File

@@ -1,5 +0,0 @@
type WindowRule = {
class: string,
caption: string,
tile: boolean,
};

View File

@@ -1,89 +0,0 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: Set<string>;
constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = WindowRuleEnforcer.createWindowRuleMaps(windowRules);
this.preferFloating = new ClientMatcher(mapFloat);
this.preferTiling = new ClientMatcher(mapTile);
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]);
}
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.has(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
if (shouldTile) {
clientManager.tileClient(kwinClient);
} else {
clientManager.untileClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleMaps(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>();
const mapTile = new Map<string, string[]>();
for (const windowRule of windowRules) {
const map = windowRule.tile ? mapTile : mapFloat;
let captions = map.get(windowRule.class);
if (captions === undefined) {
captions = [];
map.set(windowRule.class, captions);
}
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption);
}
}
return [
WindowRuleEnforcer.createWindowRuleRegexMap(mapFloat),
WindowRuleEnforcer.createWindowRuleRegexMap(mapTile),
];
}
private static createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
const regexMap = new Map<string, RegExp>;
for (const [k, v] of windowRuleMap) {
regexMap.set(k, WindowRuleEnforcer.joinRegexes(v));
}
return regexMap;
}
private static joinRegexes(regexes: string[]) {
if (regexes.length == 0) {
return new RegExp("");
}
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 + ")";
}
}

View File

@@ -0,0 +1,36 @@
{
const testCases = [
{tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true},
{tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "ksmserver-logout-greeter", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "org.kde.plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: true, resourceClass: "plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: false, resourceClass: "org.kde.kfind", caption: "something", shouldTile: true},
{tiledByDefault: false, resourceClass: "kfind", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "zoom", shouldTile: false},
];
const enforcer = new WindowRuleEnforcer(JSON.parse(defaultWindowRules));
for (const testCase of testCases) {
const kwinClient: any = createKwinClient(testCase.tiledByDefault, testCase.resourceClass, testCase.caption);
assert(enforcer.shouldTile(kwinClient) === testCase.shouldTile, "failed case: " + JSON.stringify(testCase));
}
function createKwinClient(normalWindow: boolean, resoureClass: string, caption: string) {
return {
normalWindow: normalWindow,
transient: false,
managed: true,
pid: 100,
moveable: true,
resizeable: true,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
resourceClass: resoureClass,
caption: caption,
}
}
}

17
src/tests/tests.ts Normal file
View File

@@ -0,0 +1,17 @@
declare const process: {
exit(code?: number): void,
};
function assert(assertion: boolean, message?: string) {
if (assertion) {
return;
}
if (message != undefined) {
console.assert(assertion, message);
} else {
console.assert(assertion);
}
console.trace();
process.exit(1);
}

4
src/tests/tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["../lib/**/*", "./**/*"]
}

12
src/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020"],
"module": "none",
"allowJs": false,
"esModuleInterop": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

View File

@@ -1,72 +0,0 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(workspace.clientRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(workspace.clientMinimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: KwinClient, horizontally: boolean, vertically: boolean) => {
if ((horizontally || vertically) && kwinClient.tile !== null) {
kwinClient.tile = null;
}
world.doIfTiled(kwinClient, false, (world, desktopManager, window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
});
});
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
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.numberDesktopsChanged, (oldNumberOfVirtualDesktops: number) => {
world.updateDesktops();
});
manager.connect(workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -1,35 +0,0 @@
namespace Clients {
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.resizeable;
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
}
export function isMaximizedGeometry(kwinClient: KwinClient) {
const maximizeArea = workspace.clientArea(ClientAreaOption.MaximizeArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === maximizeArea;
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, desktopNumber: number) {
return kwinClient.desktop === desktopNumber || kwinClient.desktop === -1;
}
}

View File

@@ -1,147 +0,0 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktopsPerActivity: Map<string, Desktop[]>;
private nVirtualDesktops: number;
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktopsPerActivity = new Map();
this.nVirtualDesktops = 0;
this.update()
this.addActivity(currentActivity);
}
public update() {
this.setNVirtualDesktops(workspace.desktops);
}
public getDesktop(activity: string, desktopNumber: number) {
const desktopIndex = desktopNumber - 1;
if (desktopIndex >= this.nVirtualDesktops || desktopIndex < 0) {
throw new Error("invalid desktop number: " + String(desktopNumber));
}
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
return this.desktopsPerActivity.get(activity)![desktopIndex];
}
public getCurrentDesktop() {
return this.getDesktop(workspace.currentActivity, workspace.currentDesktop);
}
public getDesktopInCurrentActivity(desktopNumber: number) {
return this.getDesktop(workspace.currentActivity, desktopNumber);
}
public getDesktopForClient(kwinClient: KwinClient) {
console.assert(kwinClient.activities.length === 1 && kwinClient.desktop > 0);
return this.getDesktop(kwinClient.activities[0], kwinClient.desktop);
}
private setNVirtualDesktops(nVirtualDesktops: number) {
if (nVirtualDesktops > this.nVirtualDesktops) {
this.addDesktopsToActivities(nVirtualDesktops - this.nVirtualDesktops);
} else if (nVirtualDesktops < this.nVirtualDesktops) {
this.removeDesktopsFromActivities(this.nVirtualDesktops - nVirtualDesktops);
}
this.nVirtualDesktops = nVirtualDesktops;
}
private addDesktopsToActivities(n: number) {
for (const desktops of this.desktopsPerActivity.values()) {
this.addDesktops(desktops, n);
}
}
private addDesktops(desktops: Desktop[], n: number) {
const nStart = desktops.length;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
desktops.push(new Desktop(desktopNumber, this.pinManager, this.config, this.layoutConfig));
}
}
private removeDesktopsFromActivities(n: number) {
const lastRemainingDesktopIndex = this.nVirtualDesktops - n - 1;
for (const desktops of this.desktopsPerActivity.values()) {
const targetDesktop = desktops[lastRemainingDesktopIndex];
for (let i = 0; i < n; i++) {
const removedDesktop = desktops.pop()!;
removedDesktop.grid.evacuate(targetDesktop.grid);
}
}
}
private addActivity(activity: string) {
const desktops: Desktop[] = [];
this.addDesktops(desktops, this.nVirtualDesktops);
this.desktopsPerActivity.set(activity, desktops);
}
private removeActivity(activity: string) {
const removedDesktops = this.desktopsPerActivity.get(activity)!;
this.desktopsPerActivity.delete(activity);
const targetActivityDesktops = this.desktopsPerActivity.values().next().value;
for (let i = 0; i < removedDesktops.length; i++) {
removedDesktops[i].grid.evacuate(targetActivityDesktops[i]);
}
}
public destroy() {
for (const desktop of this.desktops()) {
desktop.destroy();
}
}
public *desktops() {
for (const desktops of this.desktopsPerActivity.values()) {
for (const desktop of desktops) {
yield desktop;
}
}
}
public *getDesktopsForClient(kwinClient: KwinClient) {
const activities = kwinClient.activities.length > 0 ? kwinClient.activities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (kwinClient.desktop === -1) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
const desktopIndex = kwinClient.desktop - 1;
yield activityDesktops[desktopIndex];
}
}
}
// empty array means all
public *getDesktops(desktopNumbers: number[], inputActivities: string[]) {
const activities = inputActivities.length > 0 ? inputActivities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (desktopNumbers.length === 0) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
for (const desktopNumber of desktopNumbers) {
const desktopIndex = desktopNumber - 1;
yield activityDesktops[desktopIndex];
}
}
}
}
}

View File

@@ -1,5 +0,0 @@
namespace ClientState {
export class TiledMinimized implements State {
public destroy(passFocus: boolean) {}
}
}

View File

@@ -1,108 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"lib": [
"es2020",
], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "none", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": false, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./ts_built.js", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": false, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"include": [
"src/**/*"
]
}