557 Commits
v0.2 ... v0.11

Author SHA1 Message Date
Peter Fajdiga
47f4bbd9b6 bump version to 0.11 2025-01-18 13:24:43 +01:00
Peter Fajdiga
2d4ad73d16 tests: always use MockQSignal<[]> for signals without parameters 2025-01-15 20:59:43 +01:00
Peter Fajdiga
bb4e4f8ebd tests: add more events and assertions to passFocus 2025-01-15 20:56:16 +01:00
Peter Fajdiga
0742975334 MockWorkspace: unset activeWindow when the active windows is removed 2025-01-15 19:13:22 +01:00
Peter Fajdiga
64457429d0 pass focus when moving a window to another desktop 2025-01-15 19:01:24 +01:00
Peter Fajdiga
02154f2f5e MockKwinClient: unset activeWindow when the active windows moves to an inactive desktop 2025-01-15 14:22:45 +01:00
Peter Fajdiga
0a2bb4f65d MockKwinClient: fire desktopsChanged signal 2025-01-15 14:08:24 +01:00
Peter Fajdiga
6f207e59c4 MockKwinClient: make underscored properties private 2025-01-15 11:56:13 +01:00
Peter Fajdiga
4c987b6c5b pass focus when moving a column to another desktop 2025-01-15 11:48:26 +01:00
Peter Fajdiga
bca0158df9 tests: give mock clients numbered captions 2025-01-15 11:47:26 +01:00
Peter Fajdiga
9feeb0f23e prevent unnecessary scrolling on focus change (fixes #67) 2025-01-15 10:40:46 +01:00
Peter Fajdiga
0241846ea5 MockKwinClient: only fire maximizedAboutToChange if there's an actual change 2025-01-14 19:34:14 +01:00
Peter Fajdiga
3bf3f16f49 MockKwinClient: initialize windowed to true 2025-01-14 19:31:27 +01:00
Peter Fajdiga
782a6db56d readme: prepend "Then" 2025-01-09 17:51:25 +01:00
Peter Fajdiga
93b6850ffd readme: mention org.kde.notification in Installation 2025-01-09 17:46:25 +01:00
Peter Fajdiga
5f0c637d1a Actions.gridScroll: simplify 2024-12-23 20:03:57 +01:00
Himadri Bhattacharjee
8829d0b291 add ability to cycle preset widths in reverse (#75) (resolves #74)
* feat: add ability to cycle preset widths in reverse

* tests: add tests for cycling widths in reverse
2024-12-22 16:57:54 +01:00
Peter Fajdiga
d37b4bc5d1 tests: add test case for no layering 2024-11-08 16:05:18 +01:00
Peter Fajdiga
ead29e5e69 rename file 3-feature_request.md 2024-11-08 10:15:26 +01:00
Peter Fajdiga
ff75d931f6 Update issue templates 2024-11-08 10:13:51 +01:00
Peter Fajdiga
d00d514d30 stop setting keepAbove in destroy methods of Floating and Pinned 2024-11-05 21:48:35 +01:00
Peter Fajdiga
3b919909dc prevent unpinned windows from retaining keepAbove 2024-11-05 21:46:26 +01:00
Peter Fajdiga
0004b6f921 fillSpace: rename to fenceposts 2024-11-05 19:37:46 +01:00
Peter Fajdiga
24265c56f9 tests: lazyScroller: add steps 2024-10-27 23:37:19 +01:00
Peter Fajdiga
dcbc0a474d Range: reorder code 2024-10-27 23:32:39 +01:00
Peter Fajdiga
88f170f5c1 Range.Basic: unexport 2024-10-27 23:31:31 +01:00
Peter Fajdiga
78ab48ee09 Range.Basic: define properties in constructor 2024-10-27 23:27:14 +01:00
Peter Fajdiga
b2d81796f8 replace Range.Basic.fromRanges with Range.fromRanges 2024-10-27 23:20:17 +01:00
Peter Fajdiga
7d27331ce5 replace Range.Basic.contains with Range.contains 2024-10-27 23:18:21 +01:00
Peter Fajdiga
55e1037a7b extract Range into Range.ts 2024-10-27 23:14:13 +01:00
Peter Fajdiga
7820c7d00e replace Column.isVisible with RangeImpl.contains 2024-10-27 23:08:52 +01:00
Peter Fajdiga
3d8ca0bc14 Grid: remove function 2024-10-27 22:46:45 +01:00
Peter Fajdiga
eaf68b87f9 tests: create LazyScroller test 2024-10-27 22:29:00 +01:00
Peter Fajdiga
b2dfad6042 tests: Assert.grid: add centered parameter 2024-10-27 21:13:14 +01:00
Peter Fajdiga
054808cb38 tests: Assert.centeredGrid: add width parameter 2024-10-27 20:29:11 +01:00
Peter Fajdiga
97059fa4f7 readme: add installation instructions 2024-10-27 16:20:07 +01:00
Peter Fajdiga
5e7959c7f4 readme: update key bindings 2024-10-27 14:36:55 +01:00
Peter Fajdiga
a9c3aa7eae bump version to 0.10 2024-10-27 14:06:20 +01:00
Peter Fajdiga
3aec148f16 Actions.columnsWidthEqualize: use fillSpace 2024-10-26 21:19:17 +02:00
Peter Fajdiga
c60cfeb521 Actions.squeezeColumns: use fillSpace 2024-10-26 20:17:49 +02:00
Peter Fajdiga
0bbf82d84d fillSpace: handle unresizable columns 2024-10-26 19:31:17 +02:00
Peter Fajdiga
44c5c43c25 fillSpace: distribute remainder 2024-10-26 18:51:01 +02:00
Peter Fajdiga
98b2d8b882 tests: fillSpace: add test cases 2024-10-26 18:50:55 +02:00
Peter Fajdiga
4e78d27620 fillSpace: reduce function nesting 2024-10-26 18:48:17 +02:00
Peter Fajdiga
32700d1193 fillSpace: handle empty input 2024-10-25 21:52:54 +02:00
Peter Fajdiga
817351ec44 fillSpace: simplify loop 2024-10-25 21:46:26 +02:00
Peter Fajdiga
c2dd832e5c move functions from math.ts to collections.ts 2024-10-25 21:39:33 +02:00
Peter Fajdiga
2f78f9afb1 fillSpace: reimplement 2024-10-25 21:39:05 +02:00
Peter Fajdiga
2cc716f59e fillSpace: improve divisor selection 2024-10-25 17:08:40 +02:00
Peter Fajdiga
685323546a fillSpace: rename variable to size 2024-10-25 12:38:11 +02:00
Peter Fajdiga
c8e40185dc fillSpace: extract helper function findMeanSpaceFiller 2024-10-25 12:37:03 +02:00
Peter Fajdiga
0b0cb77e9e tests: Assert.equal: use === 2024-10-25 12:33:10 +02:00
Peter Fajdiga
ce4f810372 fillSpace: return array of values 2024-10-25 12:32:27 +02:00
Peter Fajdiga
3ccc6dd870 move fillSpace into its own file 2024-10-25 12:15:35 +02:00
Peter Fajdiga
92e1c2ffd4 tests: rename file to fillSpace.ts 2024-10-25 12:10:46 +02:00
Peter Fajdiga
3a1c911974 math: rename findMeanInt to fillSpace 2024-10-25 12:10:21 +02:00
Peter Fajdiga
e197c3200e tests: findMean: add test case 2024-10-25 12:03:02 +02:00
Peter Fajdiga
4fea81423a implement helper function findMeanInt 2024-10-20 23:44:41 +02:00
Peter Fajdiga
b459ba6d4e tests: Assert: ignore runLog if undefined 2024-10-20 23:43:45 +02:00
Peter Fajdiga
a16e2edb1e increase externalFrameGeometryChangedRateLimiter.n to 4 (for IntelliJ project selection window) 2024-10-19 19:27:15 +02:00
Peter Fajdiga
fb1047c8ba Destkop.scrollCenterRange: force scroll (resolves #35) 2024-10-19 17:16:14 +02:00
Peter Fajdiga
66bc14287d Makefile: enable skipping tests 2024-10-19 15:48:17 +02:00
Peter Fajdiga
86ab4a20f4 tests: Assert.grid: log failing window and column 2024-10-19 15:25:51 +02:00
Peter Fajdiga
f467d5a523 add Steam Big Picture mode to default window rules 2024-10-14 22:36:18 +02:00
Peter Fajdiga
082d4571d8 remove manualResizeStep 2024-10-14 20:35:11 +02:00
Peter Fajdiga
aa061e586e ContextualResizer: remove hard-coded max-width option 2024-10-14 20:31:56 +02:00
Peter Fajdiga
ff5121f1c4 RawResizer: use preset widths 2024-10-14 19:53:11 +02:00
Peter Fajdiga
984edbec90 move resize step search function to math.ts 2024-10-14 19:31:38 +02:00
Peter Fajdiga
ea27ce4a03 ContextualResizer: use preset widths 2024-10-14 01:10:16 +02:00
Peter Fajdiga
ae0a4e142a create PresetWidths stub 2024-10-14 01:05:57 +02:00
Peter Fajdiga
09b7b44e2c Revert "ContextualResizer: add step for min width"
This reverts commit 430f731804.
2024-10-14 00:47:23 +02:00
Peter Fajdiga
fb88300dbe remove Window.onUserResize 2024-10-13 20:47:17 +02:00
Peter Fajdiga
9dafb92f47 move user width resize logic to Column.onUserResizeWidth 2024-10-13 20:04:27 +02:00
Peter Fajdiga
92144e8e83 prevent widening left neighbor on edge resize with min-width 2024-10-13 19:41:37 +02:00
Peter Fajdiga
9b54dd543e Column: increase minWidth to 40 2024-10-13 13:37:25 +02:00
Peter Fajdiga
5145b04cb7 prevent pushing columns with min-width on edge resize 2024-10-13 13:33:02 +02:00
Peter Fajdiga
4372e13869 tests: userResize: add ineffectual resizes 2024-10-13 12:54:58 +02:00
Peter Fajdiga
7694bb1f8d tests: userResize: break some resizes into multiple steps 2024-10-13 12:14:49 +02:00
Peter Fajdiga
3615952bd7 tests: MockWorkspace.resizeWindow: set frameGeometry while keeping the same QmlRect instance 2024-10-13 12:09:29 +02:00
Peter Fajdiga
cba6ecbb3d tests: MockWorkspace.resizeWindow: support passing an array of changes 2024-10-13 01:44:10 +02:00
Peter Fajdiga
db55af462b tests: add user resize tests 2024-10-13 01:38:29 +02:00
Peter Fajdiga
32f384df75 tests: rewrite test "columns squeeze side (just scroll)" 2024-10-12 21:33:51 +02:00
Peter Fajdiga
25ac6d4ce8 tests: rename file to columnsSqueezeSide.ts 2024-10-12 21:12:42 +02:00
Peter Fajdiga
194f774519 Actions: columns-squeeze-*: squeeze all visible columns 2024-10-12 16:38:20 +02:00
Peter Fajdiga
430f731804 ContextualResizer: add step for min width 2024-10-12 14:05:32 +02:00
Peter Fajdiga
2cdf308a72 key bindings: shorten descriptions for column-shrink-left and column-shrink-right 2024-10-07 00:09:15 +02:00
Peter Fajdiga
acf4c5c6ae Actions: add actions columnShrinkLeft and columnShrinkRight 2024-10-07 00:08:39 +02:00
Peter Fajdiga
703ed2eb40 tests: move runLog to global 2024-10-06 22:31:36 +02:00
Peter Fajdiga
ec5e7002dc tests: Assert: extract helper function appendMessage 2024-10-05 20:44:03 +02:00
Peter Fajdiga
fe92b1aa13 tests: create and use helper functions for client creation 2024-10-05 20:43:57 +02:00
Peter Fajdiga
d812fa706f tests: rename function to Assert.assert 2024-10-05 17:17:44 +02:00
Peter Fajdiga
0bb9ae9e8d tests: eliminate waiting 2024-10-05 17:08:42 +02:00
Peter Fajdiga
315ea6b560 Tiled: allow two (instead of one) external frame changes per second 2024-10-05 15:45:28 +02:00
Peter Fajdiga
a4236534fc tests: externalResize: also test concession expiry 2024-10-05 14:36:49 +02:00
Peter Fajdiga
e95378c0dd tests: add external resize test 2024-10-05 14:14:55 +02:00
Peter Fajdiga
884dc9977d tests: MockKwinClient: only fire frameGeometryChanged and fullScreenChanged signals on actual changes 2024-10-05 13:43:32 +02:00
Peter Fajdiga
f3cf45e5f3 tests: add global variables and 2024-10-04 23:22:35 +02:00
Peter Fajdiga
5e7f191e95 config: reorder default presetWidths consistently with its behavior 2024-10-04 18:47:18 +02:00
Peter Fajdiga
ec14d5295f move preset width cycling logic into class 2024-10-04 18:44:19 +02:00
Peter Fajdiga
116372c954 PresetWidths: order widths from small to large 2024-10-04 18:23:48 +02:00
Peter Fajdiga
7290a0741d tests: presetWidths: add tests for filling the screen with columns 2024-10-04 15:08:15 +02:00
Peter Fajdiga
5bbd0da172 tests: Assert: rename function to truth 2024-10-04 14:35:00 +02:00
Peter Fajdiga
fb415042d8 tests: Assert: use parameter destructuring for optional parameters 2024-10-04 14:34:44 +02:00
Peter Fajdiga
ff5ba6b455 tests: rename file Assert.ts 2024-10-04 14:34:35 +02:00
Peter Fajdiga
ef0e840812 tests: assert: create namespace 2024-10-04 14:34:05 +02:00
Peter Fajdiga
b0cb9eaba0 tests: assert: extract helper function buildMessage 2024-10-04 14:30:17 +02:00
Peter Fajdiga
0eb624d6ff tests: assertGrid: correct comment 2024-10-04 14:30:17 +02:00
Peter Fajdiga
6e53bbad2f Actions: implement cyclePresetWidths (resolves #57) 2024-10-04 14:29:45 +02:00
Peter Fajdiga
218ab65d61 PresetWidths: clamp, sort, and deduplicate widths 2024-10-03 00:47:17 +02:00
Peter Fajdiga
e91c9c825e move presetWidths logic into new class PresetWidths 2024-10-03 00:43:36 +02:00
Peter Fajdiga
c2931c9fbe tests: parsePresetWidths: add test cases with non-positive widths 2024-10-03 00:05:16 +02:00
Peter Fajdiga
1c61f22353 pass spacing to parsePresetWidths 2024-10-03 00:03:18 +02:00
Peter Fajdiga
3ff7688e42 implement parsing of presetWidths 2024-10-02 23:36:58 +02:00
Peter Fajdiga
f5ca5e71c0 config: add presetWidths 2024-10-02 22:05:17 +02:00
Peter Fajdiga
3f222f0ef8 tests: extract assertGrid 2024-10-02 19:35:34 +02:00
Peter Fajdiga
9a08841bb8 ignore minimized pinned clients 2024-09-29 23:16:09 +02:00
Peter Fajdiga
2081b9997c use branded types for external types 2024-09-29 20:48:47 +02:00
Peter Fajdiga
93fe2a3cfd hard-code tiling prevention for ksmserver-logout-greeter and xwaylandvideobridge (fixes #61) 2024-09-29 18:27:12 +02:00
Peter Fajdiga
9cee3f2808 tests: WindowRuleEnforcer: reformat code 2024-09-29 18:21:06 +02:00
Peter Fajdiga
16618e6ad3 tests: WindowRuleEnforcer: add more test cases 2024-09-29 18:01:23 +02:00
Peter Fajdiga
90c27bc58b defaultWindowRules: reduce repetition by using regex 2024-09-29 17:53:38 +02:00
Peter Fajdiga
79055dcdf3 tests: layout: add more test steps, include a 3-window column 2024-09-25 00:28:46 +02:00
Peter Fajdiga
1e328c7b57 tests: layout: refactor code 2024-09-25 00:20:58 +02:00
Peter Fajdiga
2be6a04fc4 tests: layout: add helper function assertGrid 2024-09-24 23:51:52 +02:00
Peter Fajdiga
9b21a720be add key bindings for moving window to next/previous position (resolves #58) 2024-09-24 23:36:33 +02:00
Peter Fajdiga
fcfed28a0a docs: output unassigned key bindings as well 2024-09-24 23:36:33 +02:00
Peter Fajdiga
7dce802b70 docs: convert key bindings to new type DocsKeyBinding 2024-09-24 23:36:33 +02:00
Peter Fajdiga
94393393f5 add key bindings for next and previous window in grid (resolves #58) 2024-09-24 23:36:24 +02:00
Peter Fajdiga
1ba128af05 tests: assert: add missing semicolon 2024-09-24 19:50:17 +02:00
Peter Fajdiga
8094b64ce7 tests: rename layout.ts 2024-09-24 01:32:03 +02:00
Peter Fajdiga
335418e84b tests: add test for moving windows 2024-09-24 01:27:49 +02:00
Peter Fajdiga
c675fc63d1 tests: add test for focus shortcuts 2024-09-24 00:52:05 +02:00
Peter Fajdiga
9d81262b18 tests: maximization: rename clients 2024-09-24 00:29:42 +02:00
Peter Fajdiga
5d36f02631 tests: implement keyboard shortcuts mocks 2024-09-23 23:45:31 +02:00
Peter Fajdiga
977facbed7 tests: add re-maximize tests 2024-09-20 23:55:52 +02:00
Peter Fajdiga
278a2d9c7b prevent unmaximize after opening a transient window 2024-09-20 23:52:18 +02:00
Peter Fajdiga
8076635719 MockKwinClient: reduce indentation 2024-09-20 23:49:17 +02:00
Peter Fajdiga
c8f0ed87a4 MockKwinClient: fix set fullScreen 2024-09-20 22:27:37 +02:00
Peter Fajdiga
b3f581c386 tests: create TestRunner 2024-09-20 21:46:14 +02:00
Peter Fajdiga
279333dd1d tests: print selected branches on fail 2024-09-20 21:27:26 +02:00
Peter Fajdiga
dac1d488b7 tests: introduce randomness in mocks 2024-09-20 16:26:38 +02:00
Peter Fajdiga
df587fc37b MockKwinClient: introduce hasBorder 2024-09-20 16:02:02 +02:00
Peter Fajdiga
987a4c852f prevent setting column width on exiting full screen 2024-09-20 16:01:08 +02:00
Peter Fajdiga
ebff074a4c maximization test: correct expectations for tiled window 2024-09-20 12:24:48 +02:00
Peter Fajdiga
ef4c509e75 kwin.ts: allow null in activeWindow 2024-09-20 12:19:38 +02:00
Peter Fajdiga
d0c1438724 Desktop: use rectEquals 2024-09-20 12:02:38 +02:00
Peter Fajdiga
d926be2e12 assert.ts: skip own stack frames 2024-09-20 11:53:03 +02:00
Peter Fajdiga
b18516646d assert.ts: add functions for comparing QmlRect 2024-09-20 11:42:12 +02:00
Peter Fajdiga
b0405858c5 MockKwinClient: fix initial properties and signal binding 2024-09-20 11:41:38 +02:00
Peter Fajdiga
0cb3513532 tests: use rectEquals 2024-09-16 00:43:58 +02:00
Peter Fajdiga
0b2d876074 Clients: use safer QmlRect comparison 2024-09-16 00:40:59 +02:00
Peter Fajdiga
cb1b70d4f0 tests: handle setMaximize 2024-09-16 00:09:19 +02:00
Peter Fajdiga
da6b983e4a tests: fix MockWorkspace 2024-09-15 23:36:23 +02:00
Peter Fajdiga
6181057fa8 tests: extract 2024-09-15 23:27:44 +02:00
Peter Fajdiga
abc8671c6f tests/utils/mocks: rename files 2024-09-15 23:24:47 +02:00
Peter Fajdiga
36bf942266 tests/utils/mocks: remove Mocks namespace 2024-09-15 23:23:21 +02:00
Peter Fajdiga
d239ac24b3 tests: setters for full screen and frame geometry 2024-09-15 21:22:19 +02:00
Peter Fajdiga
79571146be Mocks.Workspace: implement createWindow 2024-09-14 23:43:00 +02:00
Peter Fajdiga
7273196e0c setup flow tests 2024-09-14 23:11:21 +02:00
Peter Fajdiga
212ade5ab6 extract global.d.ts into its own Typescript project 2024-09-14 23:11:14 +02:00
Peter Fajdiga
239f8d95cb KwinClient: allow null for transientFor 2024-09-14 23:09:35 +02:00
Peter Fajdiga
e430a20638 qt.ts: make QmlRect edge fields readonly 2024-09-13 21:22:33 +02:00
Peter Fajdiga
0beaebd874 tests: move assert.ts 2024-09-13 20:32:24 +02:00
Peter Fajdiga
6100090db3 tests: extract process definition into its own file 2024-09-13 20:32:01 +02:00
Peter Fajdiga
8d07958962 run-ts.sh: keep tmp js file 2024-09-10 23:55:11 +02:00
Peter Fajdiga
80bafee5ed ClientWrapper: define properties in constructor 2024-09-08 09:29:49 +02:00
Peter Fajdiga
38e72a9504 ClientManager, DesktopManager: define properties in constructor 2024-09-08 09:20:10 +02:00
Peter Fajdiga
d0c4cee63d move to src/main 2024-09-08 09:06:46 +02:00
Peter Fajdiga
494192179b run-ts.sh: refactor 2024-09-08 00:41:07 +02:00
Peter Fajdiga
0dcba7cbc0 move unit tests to tests/units 2024-09-07 22:18:11 +02:00
Peter Fajdiga
8312364202 Makefile: package: rename package dir to karousel 2024-09-07 20:27:30 +02:00
Peter Fajdiga
6d8dfad4e7 keyBindings/definition.ts: use macros in strings 2024-09-07 20:27:26 +02:00
Peter Fajdiga
0d970a8bec Function.partial: allow multiple head parameters 2024-09-07 20:27:01 +02:00
Peter Fajdiga
12e8d71ef3 Actions: replace composeNum with Function.partial 2024-09-07 20:27:01 +02:00
Peter Fajdiga
407b24df08 Actions: fix binding of this 2024-09-07 20:27:01 +02:00
Peter Fajdiga
4ecced369a Actions: shorten parameter names 2024-09-07 20:27:01 +02:00
Peter Fajdiga
e3979d94f7 move composeNum into getNumKeyBindings 2024-09-07 20:27:01 +02:00
Peter Fajdiga
b6a5080d5d docs: update key bindings usage 2024-09-07 20:27:01 +02:00
Peter Fajdiga
e974f0ebbd move Actions.ts to keyBindings dir 2024-09-07 20:27:01 +02:00
Peter Fajdiga
23e13436d8 merge types.ts into Actions.ts 2024-09-07 20:27:01 +02:00
Peter Fajdiga
aaef587c00 Actions: reorder code 2024-09-07 20:27:01 +02:00
Peter Fajdiga
8f4c20138e merge NumActions into Actions 2024-09-07 20:27:01 +02:00
Peter Fajdiga
a754da9b78 Actions: rename methods to use camelCase 2024-09-07 20:27:01 +02:00
Peter Fajdiga
b94d3e2304 call World methods outside of NumActions 2024-09-07 20:27:01 +02:00
Peter Fajdiga
11e1458180 call World methods outside of Actions 2024-09-07 20:27:01 +02:00
Peter Fajdiga
e7e68628dd remove Actions.Getter 2024-09-07 20:27:01 +02:00
Peter Fajdiga
edac1a679c use directional names 2024-09-07 20:27:01 +02:00
Peter Fajdiga
f711248619 Makefile: reorder lines 2024-09-07 20:27:01 +02:00
Peter Fajdiga
502f4bee26 Makefile: use long switches for kpackagetool6 2024-09-07 20:27:01 +02:00
Peter Fajdiga
35d2096811 remove parameter followTransient everywhere 2024-09-07 20:27:01 +02:00
Peter Fajdiga
f682c160db add TODOs 2024-09-07 20:27:01 +02:00
Peter Fajdiga
ef9cb01755 Tiled: call floatClient instead of floatKwinClient 2024-09-07 20:27:01 +02:00
Peter Fajdiga
d51b9caec5 Actions: enable followTransient for all actions 2024-09-07 20:27:01 +02:00
Peter Fajdiga
62bc96f205 move untileOnDrag from World to LayoutConfig 2024-09-07 20:27:01 +02:00
Peter Fajdiga
35802eead7 consistently use semicolons with type aliases 2024-09-07 20:27:01 +02:00
Peter Fajdiga
ba886fe5f6 ClientManager.Config: rename floatingKeepAbove 2024-09-07 20:27:01 +02:00
Peter Fajdiga
30e4e3e273 World: make clientManager private 2024-09-07 20:27:01 +02:00
Peter Fajdiga
6bcb30b0bd refactor actions from switch cases to class methods 2024-09-07 20:27:01 +02:00
Peter Fajdiga
747a90f5d7 extract Actions.NumActions 2024-09-07 20:27:01 +02:00
Peter Fajdiga
da899171c4 introduce Actions.Getter 2024-09-07 20:27:01 +02:00
Peter Fajdiga
109238e645 create actions dir 2024-09-07 20:27:01 +02:00
Peter Fajdiga
7d1ebcf126 Actions: turn into a class 2024-09-07 20:27:01 +02:00
Peter Fajdiga
07ce0cceb5 issue templates: rename Generic bug report 2024-09-07 10:31:12 +02:00
Peter Fajdiga
c7aaa66c5c issue templates: rename General bug report 2024-09-07 10:06:40 +02:00
Peter Fajdiga
5c77f4f276 issue templates: reorder templates 2024-09-07 10:05:01 +02:00
Peter Fajdiga
80608e721f issue templates: add compatibility issue template 2024-09-07 09:47:42 +02:00
Peter Fajdiga
54b07ebe56 Tiled: limit reactions to external frameGeometry changes (fixes #56) 2024-08-15 14:37:57 +02:00
Peter Fajdiga
aeba236720 ClientManager: don't tile fullscreen-shaped windows 2024-08-15 14:37:57 +02:00
Peter Fajdiga
1984442ed3 ClientManager: don't tile windows that start in fullscreen 2024-08-15 14:37:57 +02:00
Peter Fajdiga
2a4c5eac3b ClientManager.addClient: make client tilable here 2024-08-15 14:37:57 +02:00
Peter Fajdiga
266beb85ae WindowRuleEnforcer.shouldTile: remove Clients.canTileNow call 2024-08-15 14:37:57 +02:00
Peter Fajdiga
5877e466da issue templates: add [Bug] title prefix 2024-07-19 15:50:18 +02:00
Peter Fajdiga
9a60f94eda Update issue templates 2024-07-19 15:46:21 +02:00
Peter Fajdiga
0f7092a5b6 rename keyBindingsMarkdown 2024-07-10 09:57:28 +02:00
Peter Fajdiga
0b3608a530 readme: update key bindings 2024-07-10 09:52:47 +02:00
Peter Fajdiga
aaeac977e6 readme: shorten description 2024-07-09 22:42:46 +02:00
Peter Fajdiga
adc78f11bc bump version to 0.9.4 2024-07-09 21:27:54 +02:00
Peter Fajdiga
f1206b18b1 switch screen on screensChanged 2024-07-09 09:50:16 +02:00
Peter Fajdiga
c6066c354d add action screen-switch (fixes #37) 2024-07-09 09:34:50 +02:00
Peter Fajdiga
914202f091 Desktop: use constructor assignment 2024-07-09 09:05:30 +02:00
Peter Fajdiga
b85c86e7db Column: rename isToTheLeftOf and isToTheRightOf 2024-07-08 12:25:55 +02:00
Peter Fajdiga
fdb4b88333 Column: remove moveAfter 2024-07-08 12:25:50 +02:00
Peter Fajdiga
4e3d924366 Grid: rename moveColumn 2024-07-08 12:25:24 +02:00
Peter Fajdiga
3d3e8cff17 config: adjust default gaps 2024-07-07 23:24:23 +02:00
Peter Fajdiga
a79229da75 bump version to 0.9.3 2024-07-07 12:46:55 +02:00
Peter Fajdiga
53d04c1d33 config: don't tile xwaylandvideobridge (fixes #54) 2024-07-07 12:17:21 +02:00
Peter Fajdiga
a18ff61d9e prevent untiling maximized windows (fixes #51) 2024-07-05 16:12:23 +02:00
Peter Fajdiga
99ffad9223 use inqequality operator with MaximizedMode 2024-07-05 16:00:06 +02:00
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
Peter Fajdiga
3a8baf4cd7 bump version to 0.6 2023-12-30 17:18:44 +01:00
Peter Fajdiga
dc14171ae7 config: add Wayland window classes to window rules (resolves #24) 2023-12-27 19:45:07 +01:00
Peter Fajdiga
6dcf8979c2 config: add yakuake to window rules 2023-12-27 19:41:30 +01:00
Peter Fajdiga
fe5661c07f Grid: auto-scroll after removing a non-focused column 2023-12-24 14:58:44 +01:00
Peter Fajdiga
90b783b34b Grid: remove unused methods getLeftOffScreenColumn and getRightOffScreenColumn 2023-12-24 12:52:10 +01:00
Peter Fajdiga
fb40bd9592 Grid.decreaseColumnWidth: prevent scrolling away from focused column when reaching minimum width 2023-12-24 12:50:52 +01:00
Peter Fajdiga
768d95450d refactor arrange functions 2023-12-24 09:29:31 +01:00
Peter Fajdiga
e95a0e44c9 prevent translucent windows during resizing 2023-12-24 09:21:49 +01:00
Peter Fajdiga
c1b8d05919 defaultWindowRules: update Zoom entry 2023-12-24 09:21:44 +01:00
Peter Fajdiga
e98ce18105 add opacity settings for obscured windows 2023-12-16 12:10:26 +01:00
Peter Fajdiga
25a9efc8e4 config.ui: split up parameters and behavior tabs 2023-12-16 11:39:42 +01:00
Peter Fajdiga
db48644944 package/contents/ui: add footer spacer to take up excess space 2023-12-16 11:39:42 +01:00
Peter Fajdiga
950e0de076 Desktop: prevent scrolling when unnecessary (add dirtyScroll variable) 2023-12-16 10:07:02 +01:00
Peter Fajdiga
05ffe0895e Revert "Desktop.scrollToCenterRange: force scroll"
This reverts commit dda63d68cde58c7f4a7162b11a2fd614365d36ff.
2023-12-16 09:33:26 +01:00
Peter Fajdiga
61db5ca69f use different implementations of clampScrollX in different scrollers 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f7b5dd0b9c Desktop.equalizeVisibleColumnsWidths: use Desktop.RangeImpl.fromRanges 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f83f60c98f Grid: reimplement increaseColumnWidth and decreaseColumnWidth 2023-12-16 09:33:26 +01:00
Peter Fajdiga
3e8734eefb Desktop.scrollToCenterRange: force scroll 2023-12-16 09:33:26 +01:00
Peter Fajdiga
bed0ea7ed8 Actions: make gridScrollFocused center focused column again 2023-12-16 09:33:26 +01:00
Peter Fajdiga
92c99f0b87 rename focusColumn -> scrollToColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
b2024bc8aa Actions: make scrolling actions use focusColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
58f358313b config.ui: change description for kcfg_scrollingLazy 2023-12-16 09:33:26 +01:00
Peter Fajdiga
352a7061f6 config.ui: shorten description for kcfg_scrollingCentered 2023-12-16 09:33:26 +01:00
Peter Fajdiga
7314c0ee24 add ScrollerGrouped 2023-12-16 09:33:26 +01:00
Peter Fajdiga
c65361853c config.ui: use button groups 2023-12-16 09:32:26 +01:00
Peter Fajdiga
fa53e765b3 qt.d.ts: rename QmlTimer 2023-12-16 09:30:19 +01:00
Peter Fajdiga
4d35681ee2 qt.d.ts: rename QmlSize 2023-12-16 09:30:14 +01:00
Peter Fajdiga
1824bcdf85 fix usages of QmlRect.right and .bottom 2023-12-12 17:33:10 +01:00
Peter Fajdiga
ce1b402bf2 qt.d.ts: add comments to QmlRect fields 2023-12-10 21:08:00 +01:00
Peter Fajdiga
2df6d5d8e6 qt.d.ts: rename QmlRect 2023-12-10 21:06:03 +01:00
Peter Fajdiga
e7d33030ba read and use scrolling configuration 2023-12-05 22:15:49 +01:00
Peter Fajdiga
2bd000f0a6 config.ui: add scrolling configuration 2023-12-05 22:03:22 +01:00
Peter Fajdiga
6313d8f18e implement scrollers 2023-12-05 21:59:01 +01:00
Peter Fajdiga
464ec3bcb1 Grid.increaseColumnWidth: fix scroll adjustment after resize 2023-12-02 20:57:46 +01:00
Peter Fajdiga
fae793cb09 Desktop.equalizeVisibleColumnsWidths: adjust scroll after resizing 2023-12-02 20:50:13 +01:00
Peter Fajdiga
d7346a6fab Desktop.clampScrollX: simplify 2023-12-02 20:38:14 +01:00
Peter Fajdiga
0e5efd2be7 Desktop: receive Range in parameters instead of Column 2023-12-02 17:36:29 +01:00
Peter Fajdiga
a1a315790e turn Desktop.Range into an interface (well, into a type) 2023-12-02 14:12:41 +01:00
Peter Fajdiga
9d62499bf0 Grid.Range: make fields private 2023-12-02 14:09:23 +01:00
Peter Fajdiga
97cf61d1dd Grid: extract method calculateVisibleRange 2023-12-02 14:06:28 +01:00
Peter Fajdiga
5d83c6dd2c rename Grid.ScrollPos to Grid.Range 2023-12-02 13:55:39 +01:00
Peter Fajdiga
8915e8a9da Grid.getVisibleColumnsWidth: use getVisibleColumns 2023-12-02 12:44:57 +01:00
Peter Fajdiga
22e4c47189 add action columns-width-equalize (resolves #22) 2023-12-02 12:44:57 +01:00
Peter Fajdiga
552d2b851f readme: list QML dependencies 2023-11-12 10:02:33 +01:00
Peter Fajdiga
c4ce795359 config.ui: add stacked mode description to tooltip 2023-09-29 10:23:33 +02:00
Peter Fajdiga
1ac1fc3c2b readme: update key bindings 2023-09-29 10:21:47 +02:00
Peter Fajdiga
d0e041d16a keyBindings: add comment for column-toggle-stacked 2023-09-29 10:21:26 +02:00
Peter Fajdiga
8fc3fc976d Window: WindowState -> Window.State 2023-09-29 08:58:38 +02:00
Peter Fajdiga
e0eeace9dc ClientState.Manager: ; -> , 2023-09-29 08:58:37 +02:00
Peter Fajdiga
84e2a06b35 Tiled: define type WindowState 2023-09-29 08:56:45 +02:00
Peter Fajdiga
3373e02658 add skipSwitcher setting 2023-09-29 08:52:59 +02:00
Peter Fajdiga
43f425c868 config: set overscroll default to 0 2023-09-22 17:45:11 +02:00
Peter Fajdiga
5c5127f7ce bump version to 0.5 2023-09-22 17:42:16 +02:00
Peter Fajdiga
8664f05998 ClientManager: prevent glitchy placement when maximizing a pinned client 2023-09-22 17:36:54 +02:00
Peter Fajdiga
e4f6a32d42 make pinning work with kwin-tiled windows of any frameGeometry 2023-09-22 17:25:32 +02:00
Peter Fajdiga
2882bb8d5d base pinning on KwinClient.tile instead of frameGeometry 2023-09-22 14:40:25 +02:00
Peter Fajdiga
5ef71c92ce Column: prevent stacking columns with unshadeable windows 2023-09-22 11:15:35 +02:00
Peter Fajdiga
de59979a6e Tiled: restructure branches in moveResizedChanged handler 2023-09-22 10:54:12 +02:00
Peter Fajdiga
71d7d60837 ClientManager: use kwinClient parameters 2023-09-20 20:14:30 +02:00
Peter Fajdiga
7f44d23dd0 kwin.d.ts: merge AbstractClient and TopLevel 2023-09-20 20:13:23 +02:00
Peter Fajdiga
aceffae5f9 config.ui: add tooltips 2023-09-20 19:53:33 +02:00
Peter Fajdiga
09e5eaca88 add re-maximize setting 2023-09-19 21:15:01 +02:00
Peter Fajdiga
734dd8a4cc fix re-full-screen bug with Kate on Wayland 2023-09-19 21:05:59 +02:00
Peter Fajdiga
aeabb396f9 Revert "remove re-maximize and re-fullscreen functionality" (fixes #18)
This reverts commit 4f06f17b
2023-09-19 21:04:27 +02:00
Peter Fajdiga
05b9ebc325 handle grid changes during a full-screen window 2023-09-19 18:58:50 +02:00
Peter Fajdiga
4728afb5ea Grid: set preferred width in increaseColumnWidth and decreaseColumnWidth 2023-09-17 19:17:50 +02:00
Peter Fajdiga
653cb20e43 ClientWrapper: remove obsolete TODO 2023-09-17 19:17:50 +02:00
Peter Fajdiga
0cb2b68bf6 implement workaround that fixes #19 2023-09-17 19:17:50 +02:00
Peter Fajdiga
b8b8900754 Tiled: call World.do only if condition satisfied 2023-09-17 08:00:32 +02:00
Peter Fajdiga
14c006b5b1 Window.onFullScreenChanged: remove check for isFocused 2023-09-16 15:34:12 +02:00
Peter Fajdiga
9fe6be9b91 implement pinning (resolves #8) 2023-09-16 14:55:38 +02:00
Peter Fajdiga
97d1592318 add option for setting keepAbove on floating windows (fixes #15) 2023-09-16 14:51:46 +02:00
Peter Fajdiga
cc8cc04b05 WindowRuleEnforcer.shouldTile: don't tile transient windows 2023-09-10 18:18:48 +02:00
Peter Fajdiga
cc74d3610a WindowRuleEnforcer.shouldTile: explode condition 2023-09-10 18:17:25 +02:00
Peter Fajdiga
088725402e WindowRuleEnforcer: refactor function -> private static 2023-09-10 17:31:54 +02:00
Peter Fajdiga
25b7507b30 fix incorrect movement of transient windows (bug introduced in f4e9822f29) 2023-09-09 15:01:59 +02:00
Peter Fajdiga
9b32caafdc config/loader.ts: declare return type of loadConfig 2023-09-09 12:58:56 +02:00
Peter Fajdiga
668d579d63 prevent resizing new floating windows 2023-09-09 11:08:01 +02:00
Peter Fajdiga
f4e9822f29 ClientManager.addClient: avoid constructing state twice 2023-09-09 09:19:37 +02:00
Peter Fajdiga
bdb0a4aeb0 ClientWrapper: refactor constructor parameter initialState -> constructInitialState 2023-09-09 09:11:01 +02:00
Peter Fajdiga
dde1a12fce ClientManager.addClient: use variable kwinClient 2023-09-09 09:01:01 +02:00
Peter Fajdiga
c57c8391fb ClientWrapper: move tiling- and floating-specific functions to ClientState.Floating and ClientState.Tiled 2023-09-08 15:40:16 +02:00
Peter Fajdiga
ec64b47ceb Tiled: add comment regarding kwinClient.fullScreen 2023-09-06 22:53:36 +02:00
Peter Fajdiga
b055345e48 readme: update key bindings 2023-09-04 20:24:17 +02:00
Peter Fajdiga
eb43f45287 bump version to 0.4 2023-09-04 20:20:06 +02:00
Peter Fajdiga
e61d7538b2 re-arrange desktop when switching to it 2023-09-03 19:58:01 +02:00
Peter Fajdiga
a3c0976f55 main.qml: remove start and stop logs 2023-09-03 19:25:10 +02:00
Peter Fajdiga
b372489eb5 ClientState: remove unnecessary ClientState. prefixes 2023-09-03 19:13:33 +02:00
Peter Fajdiga
fa974a68aa kwin.d.ts: add comments for desktop and activities 2023-09-03 19:13:33 +02:00
Peter Fajdiga
20517aed7f Desktop.updateArea: set dirty flag 2023-09-03 14:15:01 +02:00
Peter Fajdiga
b49082d51d DesktopManager.getDesktop: fix desktop number validity check 2023-09-02 12:33:08 +02:00
Peter Fajdiga
08135a4ad4 prevent setting incorrect frameGeometry to windows that went from Karousel-tiled to Kwin-quick-tiled 2023-09-02 10:54:47 +02:00
Peter Fajdiga
681ae38d85 ClientState.Manager.setState: construct the new state after destroying the old one 2023-09-02 10:45:02 +02:00
Peter Fajdiga
9912a8d917 ClientState: split into files 2023-09-02 10:24:07 +02:00
Peter Fajdiga
1bcf768588 unset kwinClient.tile on maximize 2023-09-02 09:42:53 +02:00
Peter Fajdiga
c84fddc618 Tiled: guess maximized or full screen based on frameGeometry 2023-09-02 09:42:52 +02:00
Peter Fajdiga
ad62dafdc7 kwin.d.ts: move desktop property to TopLevel to reflect documentation 2023-09-02 09:14:45 +02:00
Peter Fajdiga
425c5c9e5b remove rectEqual 2023-09-02 09:07:28 +02:00
Peter Fajdiga
4b0f259c6d Window.onFrameGeometryChanged: remove parameter oldGeometry 2023-09-02 09:01:08 +02:00
Peter Fajdiga
a5ecc94479 prepend "Karousel:" to logs 2023-09-01 21:17:11 +02:00
Peter Fajdiga
a3f479e2e6 World: rename desktopManager 2023-09-01 17:24:42 +02:00
Peter Fajdiga
daef95731b add semicolons after type aliases 2023-09-01 17:24:42 +02:00
Peter Fajdiga
3b3bb679de qt.d.ts: define QmlObject as unknown 2023-09-01 17:24:42 +02:00
Peter Fajdiga
566c8fe53d kwin.d.ts: mark readonly properties 2023-09-01 17:24:42 +02:00
Peter Fajdiga
07ce7d4e60 Tiled: rename client 2023-09-01 14:18:40 +02:00
Peter Fajdiga
0dfc29b1eb define enum ClientAreaOption 2023-09-01 14:03:52 +02:00
Peter Fajdiga
4d784c5d01 keyBindings: allow up to 12 numerical key bindings, regardless of defaults 2023-08-30 21:59:44 +02:00
Peter Fajdiga
a4d27a2885 keyBindings: add comments regarding clashes with default KDE shortcuts 2023-08-30 21:46:48 +02:00
Peter Fajdiga
f703f0655a rename directory "clientState" 2023-08-30 21:21:14 +02:00
Peter Fajdiga
963949b039 clientstate.Manager: redefine State type 2023-08-30 21:20:17 +02:00
Peter Fajdiga
22ee707207 Desktop: correctly set clientArea (fixes bug from 0e59f382) 2023-08-30 20:58:00 +02:00
Peter Fajdiga
8d6e4f9bc7 add type parameters to QSignal 2023-08-29 23:39:53 +02:00
Peter Fajdiga
075f6c7e3d qt.d.ts: define void return type for QSignal methods 2023-08-29 23:16:41 +02:00
Peter Fajdiga
5404b61d20 kwin.d.ts: remove X11Client 2023-08-29 22:29:06 +02:00
Peter Fajdiga
76b0016055 create workaround for issue #9 2023-08-29 22:27:36 +02:00
Peter Fajdiga
64cdb90f4a define kwin client types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
80ecc7e6c9 define kwin types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
0e59f382b4 Desktop: refactor clientArea and tilingArea 2023-08-29 22:19:16 +02:00
Peter Fajdiga
6001dd5b02 define qt types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
201dd4463e ClientMatcher: remove String cast 2023-08-29 22:19:16 +02:00
Peter Fajdiga
10718bc2c7 use type instead of interface 2023-08-29 21:03:32 +02:00
Peter Fajdiga
b15bb85037 split up extern.d.ts into multiple files 2023-08-29 21:03:19 +02:00
Peter Fajdiga
4904d075ae show a notification if window rules JSON invalid 2023-08-28 23:53:11 +02:00
Peter Fajdiga
7871bbbe6d capitalize "Window Rules" tab title in config dialog 2023-08-28 23:49:04 +02:00
Peter Fajdiga
d91ea7b412 ClientWrapper: handle Kwin's quick tiles 2023-08-27 09:40:13 +02:00
Peter Fajdiga
37e9b85279 Tiled: untile when tiling using Kwin's tiling system 2023-08-25 17:15:12 +02:00
Peter Fajdiga
0bdb4af0e6 create ClientState namespace 2023-08-25 15:27:16 +02:00
Peter Fajdiga
4f06f17ba7 remove re-maximize and re-fullscreen functionality 2023-08-25 14:22:31 +02:00
Peter Fajdiga
382cbe101b Window: unset fullscreen before unmaximizing 2023-08-25 13:58:43 +02:00
Peter Fajdiga
e580acf979 update column widths after screen size change 2023-08-25 12:46:01 +02:00
Peter Fajdiga
453c4ece2c Grid: rename field desktop 2023-08-25 11:58:27 +02:00
Peter Fajdiga
15b77d0207 re-arrange after window move 2023-08-25 11:02:08 +02:00
Peter Fajdiga
d23c13c344 Desktop: fix unnecessary overscroll removing right margin on window close 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c4307e187f use namespaces instead of modules 2023-08-25 11:01:30 +02:00
Peter Fajdiga
463da59197 add krunner to default window rules 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c5ec40e5ea World.update: make private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
2f4268fc94 mark remaining methods and fields explicitly public or private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
f1911b1247 Desktop: rename ScrollPos 2023-08-25 11:01:30 +02:00
Peter Fajdiga
5b71f1c48f DesktopManager: rename methods 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c7e7b91f3f rename ScrollView to Desktop 2023-08-25 11:01:30 +02:00
Peter Fajdiga
4b3a403559 rename desktopNumber and virtualDesktop 2023-08-25 11:01:30 +02:00
Peter Fajdiga
9477b7e337 refactor arrange 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c87ef982ae ClientWrapper: prevent moving transient windows on other desktops 2023-08-25 11:01:30 +02:00
Peter Fajdiga
de3e78424a ScrollView: ScrollPos -> ScrollView.Pos 2023-08-25 11:01:30 +02:00
Peter Fajdiga
3039033ea9 World.getFocusedWindow: add ability to follow transientFor 2023-08-25 11:01:30 +02:00
Peter Fajdiga
de0f89062a World.doIfTiled: extract helper method findTiledWindow 2023-08-25 11:01:30 +02:00
Peter Fajdiga
0831c1be8b LinkedList: LinkedListNode -> LinkedList.Node 2023-08-25 11:01:30 +02:00
Peter Fajdiga
d6bfe2fd03 mark methods explicitly public or private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
048bf2a51a Grid.moveColumnLeft: call ScrollView.onGridReordered 2023-08-25 11:01:30 +02:00
Peter Fajdiga
a949dca458 readme: update key bindings 2023-08-16 21:40:27 +02:00
Peter Fajdiga
57c4643098 bump version to 0.3.1 2023-08-16 21:25:17 +02:00
Peter Fajdiga
e92563b424 Grid.decreaseColumnWidth: fix scroll left bug 2023-08-16 21:19:54 +02:00
Peter Fajdiga
671326bdd7 Grid: remove unneeded arrange calls 2023-08-16 21:19:54 +02:00
Peter Fajdiga
5e9db7d2cd prevent off-screen transients (fixes #11) 2023-08-16 21:19:53 +02:00
Peter Fajdiga
b447eacdfd World: pass same config object to ScrollViewManager 2023-08-15 17:21:10 +02:00
Peter Fajdiga
94f6e6f33b Revert "remove actions gridScrollLeft and gridScrollRight"
This reverts commit bdf62b65
2023-08-15 17:18:36 +02:00
Peter Fajdiga
85b0221220 rename Actions.ts 2023-08-15 17:08:01 +02:00
Peter Fajdiga
1894b055f7 create Actions module 2023-08-15 17:07:48 +02:00
Peter Fajdiga
05f7550a3b move KwinClient util functions into Clients module 2023-08-15 17:05:32 +02:00
Peter Fajdiga
a04f629de0 Column: make width private 2023-08-13 23:34:37 +02:00
Peter Fajdiga
4bda4d0d7c Window: update skipArrange comment 2023-08-13 20:53:18 +02:00
Peter Fajdiga
8bf076948a readme: key binding configuration 2023-08-09 22:45:45 +02:00
Peter Fajdiga
04bd85a287 readme: update key bindings 2023-07-21 09:07:18 +02:00
Peter Fajdiga
b8cf677084 bump version to 0.3 2023-07-21 09:04:11 +02:00
Peter Fajdiga
e67e6f4d62 World: remove config property 2023-07-15 19:18:34 +02:00
Peter Fajdiga
bdf62b65e4 remove actions gridScrollLeft and gridScrollRight 2023-07-15 19:15:15 +02:00
Peter Fajdiga
3238d70772 extract LayoutConfig 2023-07-15 19:15:14 +02:00
Peter Fajdiga
8382696d01 extract ScrollView.Config 2023-07-15 19:13:11 +02:00
Peter Fajdiga
18470b391f Grid: count horizontal gap as part of the partially visible window 2023-07-15 18:33:05 +02:00
Peter Fajdiga
ed8ec7c794 move Grid.isColumnVisible to Column.isVisible 2023-07-15 12:39:15 +02:00
Peter Fajdiga
fce442c25d rename action columnToggleStacked 2023-07-07 14:40:32 +02:00
Peter Fajdiga
81ef0e0442 remove action columnExpand 2023-07-07 14:37:24 +02:00
Peter Fajdiga
0fbb0fe90e promote ScrollPos to class 2023-07-07 14:04:46 +02:00
Peter Fajdiga
b12b70a294 ScrollView: remove gridToTilingSpace 2023-07-07 13:51:18 +02:00
Peter Fajdiga
0a3ba5c963 ScrollView: refactor scrollCenterColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
fa17b1fcc2 ScrollView: refactor getScrollPosForColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
68d2c5bbd8 fix scrollCenterColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
5c8da41647 add helper function getScrollPosForColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
9e808f99c9 move Column functionality from ScrollView back to Grid 2023-07-07 13:51:18 +02:00
Peter Fajdiga
29fa65613e extract scrolling functionality from Grid to ScrollView 2023-07-07 13:51:16 +02:00
Peter Fajdiga
9c1592b626 add setting untileOnDrag 2023-07-05 12:20:00 +02:00
Peter Fajdiga
dec4281bb9 remove actions expandVisibleColumns and shrinkVisibleColumns 2023-07-05 11:52:11 +02:00
Peter Fajdiga
41facafac7 Grid: prevent overscrolling when it would unnecessarily hide a fully visible window 2023-07-05 11:47:16 +02:00
Peter Fajdiga
0635e20622 Grid: add helper method gridToTilingSpace 2023-07-05 11:29:42 +02:00
Peter Fajdiga
454a14724d Column: add methods getLeft and getRight 2023-07-01 22:53:28 +02:00
Peter Fajdiga
0fff1ce837 update "tilingArea space" comments 2023-07-01 21:41:30 +02:00
Peter Fajdiga
ee8bac5a42 columnWidthIncrease and Decrease 2023-07-01 11:16:38 +02:00
Peter Fajdiga
dba5e07a86 add setting resizeNeighborColumn 2023-06-24 13:09:02 +02:00
Peter Fajdiga
e2a5625d41 resize neighbor column on edge resize 2023-06-24 13:03:22 +02:00
Peter Fajdiga
6e9edad39d Column: respect min width of windows 2023-06-24 11:07:53 +02:00
Peter Fajdiga
0266cde2f1 bump version to 0.2.1 2023-06-23 21:01:24 +02:00
Peter Fajdiga
f0e662de37 shrinkVisibleColumns: prevent expanding 2023-06-23 20:36:12 +02:00
Peter Fajdiga
e5c9b52370 Grid.rescaleVisibleColumns: adjust scroll after rescaling 2023-06-23 20:32:04 +02:00
Peter Fajdiga
83ac2506cf Grid.rescaleVisibleColumns: take overscroll amount into account 2023-06-23 20:32:03 +02:00
Peter Fajdiga
d8eec7a881 generate key binding documentation for bbcode 2023-06-23 20:32:03 +02:00
Peter Fajdiga
cb66a26394 readme: add key bindings 2023-06-23 13:43:49 +02:00
133 changed files with 6859 additions and 2541 deletions

View File

@@ -0,0 +1,19 @@
---
name: Compatibility issue
about: Report an issue with a specific application or window
title: "[Compatibility]"
labels: ''
assignees: ''
---
Karousel version:
Plasma version:
X11 / Wayland:
Window class:
Window caption (title):
Window type:
(Get this info [here](https://github.com/peterfajdiga/karousel/wiki/Getting-window-info))
Description:

14
.github/ISSUE_TEMPLATE/2-bug-report.md vendored Normal file
View File

@@ -0,0 +1,14 @@
---
name: Generic bug report
about: Report a bug
title: "[Bug]"
labels: ''
assignees: ''
---
Karousel version:
Plasma version:
X11 / Wayland:
Description:

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Request a feature
title: "[Feature]"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
These sections are just guidelines, feel free to remove them.

3
.gitignore vendored
View File

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

View File

@@ -1,31 +1,32 @@
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
TESTS := true
.PHONY: *
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern.d.ts
config:
build: tests
tsc -p ./src/main --outFile ./package/contents/code/main.js
mkdir -p ./package/contents/config
tsc ${TSC_SCRIPT_FLAGS} ./src/config/definition.ts ./generators/config/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
build:
tsc --outFile ./package/contents/code/main.js
tests:
ifeq (${TESTS}, true)
./run-ts.sh ./src/tests
endif
install: build config
kpackagetool5 --type=KWin/Script -i ./package || kpackagetool5 --type=KWin/Script -u ./package
install: build
kpackagetool6 --type=KWin/Script --install=./package || kpackagetool6 --type=KWin/Script --upgrade=./package
uninstall:
kpackagetool5 --type=KWin/Script -r ./package
kpackagetool6 --type=KWin/Script --remove=karousel
package:
tar -czf ./karousel.tar.gz ./package
package: build
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package --transform s/package/karousel/
logs:
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
docs-key-bindings-bbcode:
@./run-ts.sh ./src/generators/docs/keyBindingsBbcode
docs-key-bindings-plain:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsPlain.ts --outFile /dev/stdout | node -
docs-key-bindings-table:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsTable.ts --outFile /dev/stdout | node -
docs-key-bindings-markdown:
@./run-ts.sh ./src/generators/docs/keyBindingsMarkdown
docs-key-bindings-fmt:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsFmt.ts --outFile /dev/stdout | node -
@./run-ts.sh ./src/generators/docs/keyBindingsFmt

View File

@@ -1,22 +1,77 @@
# Karousel
KWin tiling script with scrolling. Works especially well with ultrawide screens.
Scrollable tiling Kwin script. Works especially well with ultrawide screens.
Use with [this](https://github.com/peterfajdiga/kwin4_effect_geometry_change) for animations.
https://github.com/peterfajdiga/karousel/assets/22796326/2ab62d18-09c7-45f9-8fda-e5e36b8d7a02
Karousel works differently from most tiling window managers in that it does not maximize the width
of windows, as this can be undesirable with wider screens, where it results in excessively wide
windows that require large return sweeps when reading their content.
Instead, it leaves the width of windows to the user's control. This additionally prevents
unprompted reflow of window content.
A scrollable tiling window manager tiles windows, but it does not maximize their widths. Instead, it leaves the width of windows to the user's control.
Windows are automatically centered when possible. And when running out of width, windows can be scrolled through horizontally.
Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
[Cardboard](https://gitlab.com/cardboardwm/cardboard).
## Dependencies
Karousel requires the following QML modules:
- QtQuick 6.0
- org.kde.kwin 3.0
- org.kde.notification 1.0
## Limitations
- Doesn't support multiple screens
- Doesn't support windows on all desktops
- Doesn't support windows on multiple activities
## Installation
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
Or clone the repo and run `make install` (requires node and tsc).
## Key bindings
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
Here's the default ones:
| Shortcut | Action |
| --- | --- |
| Meta+Space | Toggle floating |
| Meta+A | Move focus left |
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| (unassigned) | Move focus to the next window in grid |
| (unassigned) | Move focus to the previous window in grid |
| Meta+Home | Move focus to start |
| Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
| Meta+Shift+W | Move window up |
| Meta+Shift+S | Move window down |
| (unassigned) | Move window to the next position in grid |
| (unassigned) | Move window to the previous position in grid |
| Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width |
| Meta+R | Cycle through preset column widths |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Ctrl+D | Squeeze right column onto the screen |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right |
| Meta+Alt+PgUp | Scroll left |
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end |
| Meta+Ctrl+Return | Move Karousel grid to the current screen |
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |
| Meta+Ctrl+Shift+F[N] | Move column to desktop N |
| Meta+Ctrl+Shift+Alt+F[N] | Move this and all following columns to desktop N |

View File

@@ -1,14 +0,0 @@
const colLeft = [
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
...numKeyBindings.map((binding: NumKeyBinding) => {
const numPrefix = binding.fKeys ? "F" : "";
return `${binding.defaultModifiers}+${numPrefix}[N]`;
}),
];
const colRight = [
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
];
printCols(colLeft, " ", colRight);

View File

@@ -1,8 +0,0 @@
for (const binding of keyBindings) {
console.log(`${binding.defaultKeySequence} - ${binding.description}${formatComment(binding.comment)}`);
}
for (const binding of numKeyBindings) {
const numPrefix = binding.fKeys ? "F" : "";
console.log(`${binding.defaultModifiers}+${numPrefix}[N] - ${binding.description}N${formatComment(binding.comment)}`);
}

View File

@@ -1,18 +0,0 @@
const colLeft = [
"Shortcut",
"---",
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
...numKeyBindings.map((binding: NumKeyBinding) => {
const numPrefix = binding.fKeys ? "F" : "";
return `${binding.defaultModifiers}+${numPrefix}[N]`;
}),
];
const colRight = [
"Action",
"---",
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
];
printCols("| ", colLeft, " | ", colRight, " |");

View File

@@ -1,19 +0,0 @@
import QtQuick 2.15
import org.kde.kwin 3.0
import "./main.js" as Karousel
Item {
id: qmlBase
property var karouselInstance
Component.onCompleted: {
qmlBase.karouselInstance = Karousel.init();
print("script started");
}
Component.onDestruction: {
qmlBase.karouselInstance.destroy();
print("script stopped");
}
}

View File

@@ -11,19 +11,153 @@
<layout class="QVBoxLayout" name="layout_main">
<item>
<widget class="QTabWidget" name="tabContainer">
<widget class="QWidget" name="tab_general">
<widget class="QWidget" name="tab_behavior">
<attribute name="title">
<string>General</string>
<string>Behavior</string>
</attribute>
<layout class="QGridLayout" name="layout_tab_general" columnstretch="0,1">
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox">
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QCheckBox" name="kcfg_untileOnDrag">
<property name="text">
<string>Un-tile windows by dragging them</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_skipSwitcher">
<property name="text">
<string>Tiled windows skip switcher</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Scrolling mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_scrollingLazy">
<property name="text">
<string>Only scroll as necessary</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingCentered">
<property name="text">
<string>Center focused column</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingGrouped">
<property name="text">
<string>Center visible columns</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Layering mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_noLayering">
<property name="text">
<string>No layering</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_footer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_parameters">
<attribute name="title">
<string>Parameters</string>
</attribute>
<layout class="QFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_gapsOuterTop">
<property name="text">
<string>Top margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
@@ -45,9 +179,6 @@
<property name="text">
<string>Bottom margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
@@ -69,9 +200,6 @@
<property name="text">
<string>Left margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
@@ -93,9 +221,6 @@
<property name="text">
<string>Right margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
@@ -117,9 +242,6 @@
<property name="text">
<string>Horizontal gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
@@ -141,9 +263,6 @@
<property name="text">
<string>Vertical gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="1">
@@ -161,40 +280,13 @@
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_overscroll">
<property name="text">
<string>Overscroll amount:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_overscroll">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_manualScrollStep">
<property name="text">
<string>Manual scroll step size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep">
<property name="suffix">
<string> px</string>
@@ -208,28 +300,52 @@
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<item row="7" column="0">
<widget class="QLabel" name="label_presetWidths">
<property name="text">
<string>Stack columns by default</string>
<string>Preset widths:</string>
</property>
<property name="toolTip">
<string>Widths used for cycling through widths</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="kcfg_presetWidths">
<property name="toolTip">
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<spacer name="bottomSpacer_tab_general">
<property name="orientation">
<enum>Qt::Vertical</enum>
<item row="8" column="0">
<widget class="QLabel" name="label_offScreenOpacity">
<property name="text">
<string>Obscured window opacity:</string>
</property>
</spacer>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
<property name="suffix">
<string> %</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_windowRules">
<attribute name="title">
<string>Window rules</string>
<string>Window Rules</string>
</attribute>
<layout class="QVBoxLayout" name="layout_tab_windowRules">
<layout class="QVBoxLayout">
<item>
<widget class="QPlainTextEdit" name="kcfg_windowRules">
<property name="tabChangesFocus">

View File

@@ -0,0 +1,38 @@
import QtQuick 6.0
import org.kde.kwin 3.0
import org.kde.notification 1.0
import "../code/main.js" as Karousel
Item {
id: qmlBase
property var karouselInstance
Component.onCompleted: {
qmlBase.karouselInstance = Karousel.init();
}
Component.onDestruction: {
qmlBase.karouselInstance.destroy();
}
Notification {
id: notificationInvalidWindowRules
componentName: "plasma_workspace"
eventId: "notification"
title: "Karousel"
text: "Your Window Rules JSON is malformed, please review your Karousel configuration"
flags: Notification.Persistent
urgency: Notification.HighUrgency
}
Notification {
id: notificationInvalidPresetWidths
componentName: "plasma_workspace"
eventId: "notification"
title: "Karousel"
text: "Your preset widths are malformed, please review your Karousel configuration"
flags: Notification.Persistent
urgency: Notification.HighUrgency
}
}

View File

@@ -1,20 +1,21 @@
{
"KPackageStructure": "KWin/Script",
"KPlugin": {
"Name": "Karousel",
"Description": "Manual columnar tiling extension for KWin",
"Description": "Scrollable tiling extension for KWin",
"Icon": "preferences-system-windows",
"Authors": [{
"Email": "peter.fajdiga@gmail.com",
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"ServiceTypes": ["KWin/Script"],
"Version": "0.2",
"Version": "0.11",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
},
"X-Plasma-API": "declarativescript",
"X-Plasma-MainScript": "code/main.qml",
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-MainScript": "ui/main.qml",
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted"
}

8
run-ts.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/bash
set -e
set -o pipefail
JS_FILE='./run-ts-tmp.js'
tsc -p "$1" --outFile "$JS_FILE"
node "$JS_FILE"

View File

@@ -1,354 +0,0 @@
function initActions(world: World) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
},
focusRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
},
focusUp: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
},
focusDown: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
},
focusStart: () => {
const grid = world.getCurrentGrid();
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
grid.arrange();
},
focusEnd: () => {
const grid = world.getCurrentGrid();
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
grid.arrange();
},
windowMoveLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
window.moveToColumn(prevColumn);
grid.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
grid.arrange();
});
},
windowMoveRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
window.moveToColumn(nextColumn);
grid.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
grid.arrange();
});
},
windowMoveUp: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
column.moveWindowUp(window);
grid.arrange(); // TODO (optimization): only arrange moved windows
});
},
windowMoveDown: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
column.moveWindowDown(window);
grid.arrange(); // TODO (optimization): only arrange moved windows
});
},
windowMoveStart: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
grid.arrange();
});
},
windowMoveEnd: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
grid.arrange();
});
},
windowExpand: () => {
world.doIfTiledFocused(false, (window, column, grid) => {
column.toggleStacked();
grid.arrange();
});
},
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
world.toggleFloatingClient(kwinClient);
},
columnMoveLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
grid.moveColumnLeft(column);
grid.arrange();
});
},
columnMoveRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
grid.moveColumnRight(column);
grid.arrange();
});
},
columnMoveStart: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
column.moveAfter(null);
grid.arrange();
});
},
columnMoveEnd: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
column.moveAfter(grid.getLastColumn());
grid.arrange();
});
},
columnExpand: () => {
world.doIfTiledFocused(false, (window, column, grid) => {
column.expand();
grid.arrange();
});
},
expandVisibleColumns: () => {
const grid = world.getCurrentGrid();
grid.rescaleVisibleColumns(true);
grid.arrange();
},
shrinkVisibleColumns: () => {
const grid = world.getCurrentGrid();
grid.rescaleVisibleColumns(false);
grid.arrange();
},
gridScrollLeft: () => {
gridScroll(world, -world.config.manualScrollStep);
},
gridScrollRight: () => {
gridScroll(world, world.config.manualScrollStep);
},
gridScrollStart: () => {
const grid = world.getCurrentGrid();
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.scrollToColumn(firstColumn);
grid.arrange();
},
gridScrollEnd: () => {
const grid = world.getCurrentGrid();
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.scrollToColumn(lastColumn);
grid.arrange();
},
gridScrollFocused: () => {
const focusedWindow = world.getFocusedWindow();
if (focusedWindow === null) {
return;
}
const column = focusedWindow.column;
const grid = column.grid;
grid.scrollCenterColumn(column);
grid.arrange();
},
gridScrollLeftColumn: () => {
const grid = world.getCurrentGrid();
const column = grid.getLeftmostVisibleColumn(true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.scrollToColumn(prevColumn);
grid.arrange();
},
gridScrollRightColumn: () => {
const grid = world.getCurrentGrid();
const column = grid.getRightmostVisibleColumn(true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.scrollToColumn(nextColumn);
grid.arrange();
},
};
}
function initNumActions(world: World) {
return {
focusColumn: (columnIndex: number) => {
const grid = world.getCurrentGrid();
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
targetColumn.focus();
},
windowMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
window.moveToColumn(targetColumn);
grid.autoAdjustScroll();
grid.arrange();
});
},
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return null;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
grid.arrange();
});
},
columnMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = world.getGridInCurrentActivity(desktopNumber);
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
oldGrid.arrange();
newGrid.arrange();
});
},
tailMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = world.getGridInCurrentActivity(desktopNumber);
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
oldGrid.arrange();
newGrid.arrange();
});
},
};
}
function gridScroll(world: World, amount: number) {
const scrollAmount = amount;
const grid = world.getCurrentGrid();
grid.adjustScroll(scrollAmount, false);
grid.arrange();
}
function canTileEver(kwinClient: AbstractClient) {
return kwinClient.resizeable;
}
function canTileNow(kwinClient: AbstractClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
}
function makeTileable(kwinClient: AbstractClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
}

View File

@@ -1,12 +0,0 @@
type Config = {
gapsOuterTop: number,
gapsOuterBottom: number,
gapsOuterLeft: number,
gapsOuterRight: number,
gapsInnerHorizontal: number,
gapsInnerVertical: number,
overscroll: number,
manualScrollStep: number,
stackColumnsByDefault: boolean,
windowRules: string,
}

View File

@@ -1,101 +0,0 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "kcalc",
"tile": false
},
{
"class": "kfind",
"tile": true
},
{
"class": "kruler",
"tile": false
},
{
"class": "zoom",
"caption": "Zoom Cloud Meetings",
"tile": false
},
{
"class": "zoom",
"caption": "zoom",
"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": 18
},
{
"name": "manualScrollStep",
"type": "UInt",
"default": 200
},
{
"name": "stackColumnsByDefault",
"type": "Bool",
"default": false
},
{
"name": "windowRules",
"type": "String",
"default": defaultWindowRules
}
];

13
src/extern.d.ts vendored
View File

@@ -1,13 +0,0 @@
declare const qmlBase;
declare const console;
declare const KWin;
declare const Qt;
declare const workspace;
declare const options;
type AbstractClient = any;
type TopLevel = any;
type X11Client = any;
type QRect = any;
type QSignal = any;
type QQmlTimer = any;

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

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

View File

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

View File

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

View File

@@ -1,27 +1,16 @@
interface KeyBinding {
name: string;
type DocsKeyBinding = {
description: string;
comment?: string;
defaultKeySequence: string;
action: string;
}
keySequence: string;
};
interface NumKeyBinding {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: string;
}
function formatComment(comment: string | undefined) {
return comment === undefined ? "" : ` (${comment})`;
function formatDescription(item: {description: string, comment?: string}) {
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
return `${applyMacro(item.description, "N")}${suffix}`;
}
function printCols(...columns: (string[] | string)[]) {
const nCols = columns.length;
if (nCols == 0) {
if (nCols === 0) {
return;
}
@@ -30,7 +19,7 @@ function printCols(...columns: (string[] | string)[]) {
).map(
(column: string[] | string) => column.length
));
if (nRows == Infinity) {
if (nRows === Infinity) {
// we only have single string columns
nRows = 1;
}
@@ -65,3 +54,15 @@ function printCols(...columns: (string[] | string)[]) {
console.log(line);
}
}
const empty: any = {};
const keyBindings: DocsKeyBinding[] = Array.prototype.concat(
getKeyBindings(empty, empty).map(binding => ({
description: formatDescription(binding),
keySequence: binding.defaultKeySequence || "(unassigned)",
})),
getNumKeyBindings(empty, empty).map(binding => ({
description: formatDescription(binding),
keySequence: `${binding.defaultModifiers}+${binding.fKeys ? "F" : ""}[N]`,
})),
);

View File

@@ -0,0 +1,7 @@
console.log(`[list]`);
for (const binding of keyBindings) {
console.log(` [*] ${binding.keySequence}${binding.description}`);
}
console.log(`[/list]`);

View File

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

View File

@@ -0,0 +1,9 @@
const colLeft = [
...keyBindings.map(binding => binding.keySequence),
];
const colRight = [
...keyBindings.map(binding => binding.description),
];
printCols(colLeft, " ", colRight);

View File

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

View File

@@ -0,0 +1,13 @@
const colLeft = [
"Shortcut",
"---",
...keyBindings.map(binding => binding.keySequence),
];
const colRight = [
"Action",
"---",
...keyBindings.map(binding => binding.description),
];
printCols("| ", colLeft, " | ", colRight, " |");

View File

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

View File

@@ -1,217 +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",
"defaultKeySequence": "Meta+D",
"action": "focusRight",
},
{
"name": "focus-up",
"description": "Move focus up",
"defaultKeySequence": "Meta+W",
"action": "focusUp",
},
{
"name": "focus-down",
"description": "Move focus down",
"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": "window-expand",
"description": "Expand window",
"comment": "Expands focused window vertically; toggles stacked layout for focused column",
"defaultKeySequence": "Meta+X",
"action": "windowExpand",
},
{
"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-expand",
"description": "Expand column",
"comment": "Expands focused column horizontally to fill the screen",
"defaultKeySequence": "Meta+Ctrl+X",
"action": "columnExpand",
},
{
"name": "expand-visible-columns",
"description": "Expand fully visible columns",
"comment": "Expands fully visible columns to fill the screen",
"defaultKeySequence": "Meta+Alt++",
"action": "expandVisibleColumns",
},
{
"name": "shrink-visible-columns",
"description": "Shrink visible columns",
"comment": "Shrinks fully and partially visible columns, making them fully visible and filling the screen",
"defaultKeySequence": "Meta+Alt+-",
"action": "shrinkVisibleColumns",
},
{
"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 ",
"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,62 +0,0 @@
interface KeyBinding {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
action: keyof ReturnType<typeof initActions>;
}
interface NumKeyBinding {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: keyof ReturnType<typeof initNumActions>;
}
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
console.log(error);
console.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 < n; i++) {
const numKey = String(i + 1);
registerKeyBinding(
name + numKey,
description + numKey,
modifiers + "+" + numPrefix + numKey,
() => callback(i),
);
}
}
function registerKeyBindings(world: World) {
const actions = initActions(world);
for (const binding of keyBindings) {
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
}
const numActions = initNumActions(world);
for (const binding of numKeyBindings) {
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
}
}

View File

@@ -1,252 +0,0 @@
class Column {
public grid: Grid;
public gridX: number;
public width: number; // TODO: increase column width to contain transients
private readonly windows: LinkedList<Window>;
private stacked: boolean;
private focusTaker: Window|null;
private widthBeforeExpand: number;
constructor(grid: Grid, prevColumn: Column|null) {
this.gridX = 0;
this.width = 0;
this.windows = new LinkedList();
this.stacked = grid.world.config.stackColumnsByDefault;
this.focusTaker = null;
this.widthBeforeExpand = 0;
this.grid = grid;
this.grid.onColumnAdded(this, prevColumn);
}
moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
if (targetGrid === this.grid) {
this.grid.onColumnMoved(this, prevColumn);
} else {
this.grid.onColumnRemoved(this, false);
this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) {
window.client.kwinClient.desktop = targetGrid.desktop;
}
}
}
moveAfter(prevColumn: Column|null) {
if (prevColumn === this) {
return;
}
this.grid.onColumnMoved(this, prevColumn);
}
isAfter(other: Column) {
return this.gridX > other.gridX;
}
isBefore(other: Column) {
return this.gridX < other.gridX;
}
moveWindowUp(window: Window) {
this.windows.moveBack(window);
}
moveWindowDown(window: Window) {
this.windows.moveForward(window);
}
getWindowCount() {
return this.windows.length();
}
isEmpty() {
return this.getWindowCount() === 0;
}
getPrevWindow(window: Window) {
return this.windows.getPrev(window);
}
getNextWindow(window: Window) {
return this.windows.getNext(window);
}
getWidth() {
return this.width;
}
getMaxWidth() {
return this.grid.tilingArea.width;
}
setWidth(width: number, setPreferred: boolean) {
width = Math.min(width, this.getMaxWidth());
const oldWidth = this.width;
this.width = width;
if (setPreferred) {
for (const window of this.windows.iterator()) {
window.client.preferredWidth = width;
}
}
if (width !== oldWidth) {
this.grid.onColumnWidthChanged(this, oldWidth, width);
}
}
adjustWidth(widthDelta: number, setPreferred: boolean) {
this.setWidth(this.width + widthDelta, setPreferred);
}
expand() {
const maxWidth = this.getMaxWidth();
const isAlreadyExpanded = this.width === maxWidth && this.widthBeforeExpand > 0;
if (isAlreadyExpanded) {
this.setWidth(this.widthBeforeExpand, false);
} else {
this.widthBeforeExpand = this.width;
this.setWidth(maxWidth, false);
}
}
adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
if (otherWindow === null) {
return;
}
window.height += heightDelta;
otherWindow.height -= heightDelta;
}
resizeWindows() {
const nWindows = this.windows.length();
if (nWindows === 0) {
return;
}
if (nWindows === 1) {
this.stacked = this.grid.world.config.stackColumnsByDefault;
}
let remainingPixels = this.grid.tilingArea.height - (nWindows-1) * this.grid.world.config.gapsInnerVertical;
let remainingWindows = nWindows;
for (const window of this.windows.iterator()) {
const windowHeight = Math.round(remainingPixels / remainingWindows);
window.height = windowHeight;
remainingPixels -= windowHeight;
remainingWindows--;
}
// TODO: respect min height
}
getFocusTaker() {
if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) {
return null;
}
return this.focusTaker;
}
focus() {
const window = this.getFocusTaker() ?? this.windows.getFirst();
if (window === null) {
return;
}
window.focus();
}
arrange(x: number) {
if (this.stacked && this.windows.length() >= 2) {
this.arrangeStacked(x);
return;
}
let y = this.grid.tilingArea.y;
for (const window of this.windows.iterator()) {
window.client.setShade(false);
window.arrange(x, y, this.width, window.height);
y += window.height + this.grid.world.config.gapsInnerVertical;
}
}
arrangeStacked(x: number) {
const expandedWindow = this.getFocusTaker();
let collapsedHeight;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.client.setShade(false);
} else {
window.client.setShade(true);
collapsedHeight = window.client.kwinClient.frameGeometry.height;
}
}
const nCollapsed = this.getWindowCount() - 1;
const expandedHeight = this.grid.tilingArea.height - nCollapsed * (collapsedHeight + this.grid.world.config.gapsInnerVertical);
let y = this.grid.tilingArea.y;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.arrange(x, y, this.width, expandedHeight);
y += expandedHeight;
} else {
window.arrange(x, y, this.width, window.height);
y += collapsedHeight;
}
y += this.grid.world.config.gapsInnerVertical;
}
}
toggleStacked() {
if (this.windows.length() < 2) {
return;
}
this.stacked = !this.stacked;
}
onWindowAdded(window: Window) {
this.windows.insertEnd(window);
if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false);
}
// TODO: also change column width if the new window requires it
this.resizeWindows();
if (window.isFocused()) {
this.onWindowFocused(window);
}
}
onWindowRemoved(window: Window, passFocus: boolean) {
const lastWindow = this.windows.length() === 1;
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
this.windows.remove(window);
if (window === this.focusTaker) {
this.focusTaker = windowToFocus;
}
if (lastWindow) {
console.assert(this.isEmpty());
this.destroy(passFocus);
} else {
this.resizeWindows();
if (passFocus && windowToFocus !== null) {
windowToFocus.focus();
}
}
}
onWindowFocused(window: Window) {
this.grid.onColumnFocused(this);
this.focusTaker = window;
}
restoreToTiled() {
const lastFocusedWindow = this.getFocusTaker();
if (lastFocusedWindow !== null) {
lastFocusedWindow.restoreToTiled();
}
}
destroy(passFocus: boolean) {
this.grid.onColumnRemoved(this, passFocus);
}
}

View File

@@ -1,297 +0,0 @@
class Grid {
public readonly world: World;
private readonly columns: LinkedList<Column>;
private lastFocusedColumn: Column|null;
private scrollX: number;
private width: number;
private userResize: boolean; // is any part of the grid being resized by the user
public clientArea: QRect;
public tilingArea: QRect;
public readonly desktop: number;
private readonly userResizeFinishedDelayer: Delayer;
constructor(world: World, desktop: number) {
this.world = world;
this.columns = new LinkedList();
this.lastFocusedColumn = null;
this.scrollX = 0;
this.width = 0;
this.userResize = false;
this.desktop = desktop;
this.updateArea();
this.userResizeFinishedDelayer = new Delayer(50, () => {
// this delay prevents windows' contents from freezing after resizing
this.autoAdjustScroll();
this.arrange();
});
}
updateArea() {
const newClientArea = workspace.clientArea(workspace.PlacementArea, 0, this.desktop);
if (newClientArea === this.clientArea) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Qt.rect(
newClientArea.x + this.world.config.gapsOuterLeft,
newClientArea.y + this.world.config.gapsOuterTop,
newClientArea.width - this.world.config.gapsOuterLeft - this.world.config.gapsOuterRight,
newClientArea.height - this.world.config.gapsOuterTop - this.world.config.gapsOuterBottom,
)
for (const column of this.columns.iterator()) {
column.resizeWindows();
}
this.autoAdjustScroll();
}
moveColumnLeft(column: Column) {
this.columns.moveBack(column);
this.columnsSetX(column);
this.autoAdjustScroll();
}
moveColumnRight(column: Column) {
const nextColumn = this.columns.getNext(column);
if (nextColumn === null) {
return;
}
this.moveColumnLeft(nextColumn);
}
getPrevColumn(column: Column) {
return this.columns.getPrev(column);
}
getNextColumn(column: Column) {
return this.columns.getNext(column);
}
getFirstColumn() {
return this.columns.getFirst();
}
getLastColumn() {
return this.columns.getLast();
}
getColumnAtIndex(i: number) {
return this.columns.getItemAtIndex(i);
}
getLastFocusedColumn() {
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
return null;
}
return this.lastFocusedColumn;
}
getLeftmostVisibleColumn(fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
const left = column.gridX - this.scrollX; // in screen space
const right = left + column.width; // in screen space
const x = fullyVisible ? left : right;
if (x >= 0) {
return column;
}
}
return null;
}
getRightmostVisibleColumn(fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
const left = column.gridX - this.scrollX; // in screen space
const right = left + column.width; // in screen space
const x = fullyVisible ? right : left;
if (x <= this.tilingArea.width) {
last = column;
} else {
break;
}
}
return last;
}
rescaleVisibleColumns(fullyVisible: boolean) {
const startColumn = this.getLeftmostVisibleColumn(fullyVisible);
const endColumn = this.getRightmostVisibleColumn(fullyVisible);
if (startColumn === null || endColumn === null) {
return;
}
const startX = startColumn.gridX;
const endX = endColumn.gridX + endColumn.width;
const width = endX - startX;
const scaleRatio = this.tilingArea.width / width;
let remainingWidth = this.tilingArea.width;
for (const column of this.columns.iteratorFrom(startColumn)) {
if (column !== endColumn) {
const newWidth = Math.round(column.width * scaleRatio);
column.setWidth(newWidth, true);
remainingWidth -= newWidth + this.world.config.gapsInnerHorizontal;
} else {
column.setWidth(remainingWidth, true);
break;
}
}
}
scrollToColumn(column: Column) {
const left = column.gridX - this.scrollX; // in screen space
const right = left + column.width; // in screen space
const remainingSpace = this.tilingArea.width - column.width;
const overScrollX = Math.min(this.world.config.overscroll, Math.round(remainingSpace / 2));
if (left < 0) {
this.adjustScroll(left - overScrollX, false);
} else if (right > this.tilingArea.width) {
this.adjustScroll(right - this.tilingArea.width + overScrollX, false);
} else {
this.removeOverscroll();
}
}
scrollCenterColumn(column: Column) {
const windowCenter = column.gridX + column.width / 2 + this.world.config.gapsInnerHorizontal - this.scrollX; // in screen space
const screenCenter = this.tilingArea.x + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
autoAdjustScroll() {
const focusedWindow = this.world.getFocusedWindow();
if (focusedWindow === null) {
this.removeOverscroll();
return;
}
const column = focusedWindow.column;
if (column.grid !== this) {
return;
}
this.scrollToColumn(column);
}
private setScroll(x: number, force: boolean) {
if (!force) {
let minScroll = 0;
let maxScroll = this.width - this.tilingArea.width;
if (maxScroll < 0) {
const centerScroll = Math.round(maxScroll / 2);
minScroll = centerScroll;
maxScroll = centerScroll;
}
x = clamp(x, minScroll, maxScroll);
}
this.scrollX = x;
}
adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
private removeOverscroll() {
this.setScroll(this.scrollX, false);
}
private columnsSetX(firstMovedColumn: Column|null) {
const lastUnmovedColumn = firstMovedColumn === null ? this.columns.getLast() : this.columns.getPrev(firstMovedColumn);
let x = lastUnmovedColumn === null ? 0 : lastUnmovedColumn.gridX + lastUnmovedColumn.width + this.world.config.gapsInnerHorizontal;
if (firstMovedColumn !== null) {
for (const column of this.columns.iteratorFrom(firstMovedColumn)) {
column.gridX = x;
x += column.width + this.world.config.gapsInnerHorizontal;
}
}
this.width = x - this.world.config.gapsInnerHorizontal;
}
arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
let x = this.tilingArea.x - this.scrollX;
for (const column of this.columns.iterator()) {
column.arrange(x);
x += column.getWidth() + this.world.config.gapsInnerHorizontal;
}
}
onColumnAdded(column: Column, prevColumn: Column|null) {
if (prevColumn === null) {
this.columns.insertStart(column);
} else {
this.columns.insertAfter(column, prevColumn);
}
this.columnsSetX(column);
this.autoAdjustScroll();
}
onColumnRemoved(column: Column, passFocus: boolean) {
const isLastColumn = this.columns.length() === 1;
const nextColumn = this.getNextColumn(column);
const columnToFocus = isLastColumn ? null : this.getPrevColumn(column) ?? nextColumn;
if (column === this.lastFocusedColumn) {
this.lastFocusedColumn = columnToFocus;
}
this.columns.remove(column);
this.columnsSetX(nextColumn);
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} else {
this.removeOverscroll();
}
}
onColumnMoved(column: Column, prevColumn: Column|null) {
const movedLeft = prevColumn === null ? true : column.isAfter(prevColumn);
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
this.columnsSetX(firstMovedColumn);
this.autoAdjustScroll();
}
onColumnWidthChanged(column: Column, oldWidth: number, width: number) {
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
if (!this.userResize) {
this.autoAdjustScroll();
}
}
onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null) {
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;
this.scrollToColumn(column);
}
onUserResizeStarted() {
this.userResize = true;
}
onUserResizeFinished() {
this.userResize = false;
this.userResizeFinishedDelayer.run();
}
evacuateTail(targetGrid: Grid, startColumn: Column) {
for (const column of this.columns.iteratorFrom(startColumn)) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
evacuate(targetGrid: Grid) {
for (const column of this.columns.iterator()) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
destroy() {
this.userResizeFinishedDelayer.destroy();
}
}

View File

@@ -1,115 +0,0 @@
class Window {
public column: Column;
public readonly client: ClientWrapper;
public height: number;
public readonly focusedState: WindowState;
private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedHorizontally: false,
maximizedVertically: false,
};
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this);
}
moveToColumn(targetColumn: Column) {
if (targetColumn === this.column) {
return;
}
this.column.onWindowRemoved(this, false);
this.column = targetColumn;
targetColumn.onWindowAdded(this);
}
arrange(x: number, y: number, width: number, height: number) {
if (this.skipArrange) {
// window is being manually resized, prevent fighting with the user
return;
}
this.client.place(x, y, width, height);
if (this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
this.client.setFullScreen(this.focusedState.fullScreen);
}
}
focus() {
if (this.client.isShaded()) {
// workaround for KWin deactivating clients when unshading immediately after activation
this.client.setShade(false);
}
this.client.focus();
}
isFocused() {
return this.client.isFocused();
}
onFocused() {
this.column.onWindowFocused(this);
}
restoreToTiled() {
if (this.isFocused()) {
return;
}
this.client.setMaximize(false, false);
this.client.setFullScreen(false);
}
onMaximizedChanged(horizontally: boolean, vertically: boolean) {
const maximized = horizontally || vertically;
this.skipArrange = maximized;
this.client.kwinClient.keepBelow = !maximized;
if (this.isFocused()) {
this.focusedState.maximizedHorizontally = horizontally;
this.focusedState.maximizedVertically = vertically;
}
}
onFullScreenChanged(fullScreen: boolean) {
this.skipArrange = fullScreen;
if (this.isFocused()) {
this.client.kwinClient.keepBelow = !fullScreen;
this.focusedState.fullScreen = fullScreen;
}
}
onUserResize(oldGeometry: QRect) {
const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height;
if (widthDelta !== 0) {
this.column.adjustWidth(widthDelta, true);
if (newGeometry.x !== oldGeometry.x) {
this.column.grid.adjustScroll(widthDelta, true);
}
}
if (heightDelta !== 0) {
this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y);
}
}
onProgrammaticResize(oldGeometry: QRect) {
const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true);
}
destroy(passFocus: boolean) {
this.column.onWindowRemoved(this, passFocus);
}
}
type WindowState = {
fullScreen: boolean,
maximizedHorizontally: boolean,
maximizedVertically: boolean,
}

View File

@@ -0,0 +1,62 @@
class PresetWidths {
private readonly presets: ((maxWidth: number) => number)[];
constructor(presetWidths: string, spacing: number) {
this.presets = PresetWidths.parsePresetWidths(presetWidths, spacing);
}
public next(currentWidth: number, minWidth: number, maxWidth: number) {
const widths = this.getWidths(minWidth, maxWidth);
const nextIndex = widths.findIndex(width => width > currentWidth);
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
}
public prev(currentWidth: number, minWidth: number, maxWidth: number) {
const widths = this.getWidths(minWidth, maxWidth).reverse();
const nextIndex = widths.findIndex(width => width < currentWidth);
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
}
public getWidths(minWidth: number, maxWidth: number) {
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
widths.sort((a, b) => a - b);
return uniq(widths);
}
private static parsePresetWidths(presetWidths: string, spacing: number): ((maxWidth: number) => number)[] {
function getRatioFunction(ratio: number) {
return (maxWidth: number) => Math.floor((maxWidth + spacing) * ratio - spacing);
}
return presetWidths.split(",").map((widthStr: string) => {
widthStr = widthStr.trim();
const widthPx = PresetWidths.parseNumberWithSuffix(widthStr, "px");
if (widthPx !== undefined) {
return () => widthPx;
}
const widthPct = PresetWidths.parseNumberWithSuffix(widthStr, "%");
if (widthPct !== undefined) {
return getRatioFunction(widthPct / 100.0);
}
return getRatioFunction(PresetWidths.parseNumberSafe(widthStr));
});
}
private static parseNumberSafe(str: string) {
const num = Number(str);
if (isNaN(num) || num <= 0) {
throw new Error("Invalid number: " + str);
}
return num;
}
private static parseNumberWithSuffix(str: string, suffix: string) {
if (!str.endsWith(suffix)) {
return undefined;
}
return PresetWidths.parseNumberSafe(str.substring(0, str.length-suffix.length).trim());
}
}

View File

@@ -0,0 +1,89 @@
class ContextualResizer {
constructor(
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
) {}
public increaseWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
return;
}
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 = findMinPositive(
[
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => width - column.getWidth(),
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
public decreaseWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
let leftOffScreenColumn = grid.getLeftColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getRightColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
const visibleColumnsWidth = rightVisibleColumn.getRight() - leftVisibleColumn.getLeft();
const unusedWidth = visibleRange.getWidth() - visibleColumnsWidth;
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = findMinPositive(
[
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => column.getWidth() - width,
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
}

View File

@@ -0,0 +1,31 @@
class RawResizer {
constructor(
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
) {}
public increaseWidth(column: Column) {
const newWidth = findMinPositive(
[
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
],
width => width - column.getWidth(),
);
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
}
public decreaseWidth(column: Column) {
const newWidth = findMinPositive(
[
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
],
width => column.getWidth() - width,
);
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
}
}

View File

@@ -0,0 +1,13 @@
class CenterClamper {
public clampScrollX(desktop: Desktop, x: number) {
const firstColumn = desktop.grid.getFirstColumn();
if (firstColumn === null) {
return 0;
}
const lastColumn = desktop.grid.getLastColumn()!;
let minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
let maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
return clamp(x, minScroll, maxScroll);
}
}

View File

@@ -0,0 +1,10 @@
class EdgeClamper {
public clampScrollX(desktop: Desktop, x: number) {
let minScroll = 0;
let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
if (maxScroll < 0) {
return Math.round(maxScroll / 2);
}
return clamp(x, minScroll, maxScroll);
}
}

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

22
src/lib/config/config.ts Normal file
View File

@@ -0,0 +1,22 @@
type Config = {
gapsOuterTop: number;
gapsOuterBottom: number;
gapsOuterLeft: number;
gapsOuterRight: number;
gapsInnerHorizontal: number;
gapsInnerVertical: number;
manualScrollStep: number;
presetWidths: string;
offScreenOpacity: number;
untileOnDrag: boolean;
stackColumnsByDefault: boolean;
resizeNeighborColumn: boolean;
reMaximize: boolean;
skipSwitcher: boolean;
scrollingLazy: boolean;
scrollingCentered: boolean;
scrollingGrouped: boolean;
tiledKeepBelow: boolean;
floatingKeepAbove: boolean;
windowRules: string;
};

View File

@@ -0,0 +1,158 @@
const defaultWindowRules = `[
{
"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": "steam",
"caption": "Steam Big Picture Mode",
"tile": false
},
{
"class": "zoom",
"caption": "Zoom Cloud Meetings|zoom|zoom <2>",
"tile": false
},
{
"class": "jetbrains-.*",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-.*",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
}
]`;
const configDef = [
{
name: "gapsOuterTop",
type: "UInt",
default: 16,
},
{
name: "gapsOuterBottom",
type: "UInt",
default: 16,
},
{
name: "gapsOuterLeft",
type: "UInt",
default: 16,
},
{
name: "gapsOuterRight",
type: "UInt",
default: 16,
},
{
name: "gapsInnerHorizontal",
type: "UInt",
default: 8,
},
{
name: "gapsInnerVertical",
type: "UInt",
default: 8,
},
{
name: "manualScrollStep",
type: "UInt",
default: 200,
},
{
name: "presetWidths",
type: "String",
default: "50%, 100%",
},
{
name: "offScreenOpacity",
type: "UInt",
default: 100,
},
{
name: "untileOnDrag",
type: "Bool",
default: true,
},
{
name: "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,
}
];

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

@@ -0,0 +1 @@
declare const console: Console;

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

@@ -0,0 +1,112 @@
type KWin = {
__brand: "KWin";
readConfig(key: string, defaultValue: any): any;
};
type Workspace = {
__brand: "Workspace";
readonly activities: string[];
readonly desktops: KwinDesktop[];
readonly currentDesktop: KwinDesktop;
readonly currentActivity: string;
readonly activeScreen: Output;
readonly windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
activeWindow: KwinClient|null;
readonly currentDesktopChanged: QSignal<[]>;
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient|null]>;
readonly screensChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly currentActivityChanged: QSignal<[]>;
readonly virtualScreenSizeChanged: QSignal<[]>;
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
};
const enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}
const enum MaximizedMode {
Unmaximized,
Vertically,
Horizontally,
Maximized,
}
type Tile = { __brand: "Tile" };
type Output = { __brand: "Output" };
type KwinClient = {
__brand: "KwinClient";
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient | null;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
readonly moveable: boolean;
readonly resizeable: boolean;
readonly fullScreenable: boolean;
readonly maximizable: boolean;
readonly output: Output;
readonly resourceClass: string;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
readonly popupWindow: boolean;
readonly pid: number;
fullScreen: boolean;
activities: string[]; // empty array means all activities
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile|null;
opacity: number;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>;
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
readonly interactiveMoveResizeFinished: QSignal<[]>;
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
};
type KwinDesktop = {
__brand: "KwinDesktop";
readonly id: string;
};
type ShortcutHandler = QmlObject & {
readonly activated: QSignal<[]>;
destroy(): void;
};

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

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

56
src/lib/extern/qt.ts vendored Normal file
View File

@@ -0,0 +1,56 @@
type Console = {
__brand: "Console";
log(...args: any[]): void;
assert(assertion: boolean, message?: string): void;
};
type Qt = {
__brand: "Qt";
rect(x: number, y: number, width: number, height: number): QmlRect;
createQmlObject(qml: string, parent: QmlObject): QmlObject;
};
type QmlObject = { __brand: "QmlObject" };
type QmlPoint = {
__brand: "QmlPoint";
x: number;
y: number;
};
type QmlRect = {
__brand: "QmlRect";
x: number;
y: number;
width: number;
height: number;
readonly top: number;
readonly bottom: number; // top + height
readonly left: number;
readonly right: number; // left + width
};
type QmlSize = {
__brand: "QmlSize";
width: number;
height: number;
};
type QSignal<T extends unknown[]> = {
__brand: "QSignal";
connect(handler: (...args: [...T]) => void): void;
disconnect(handler: (...args: [...T]) => void): void;
};
type QmlTimer = QmlObject & {
interval: number;
readonly triggered: QSignal<[]>;
restart(): void;
destroy(): void;
};

View File

@@ -0,0 +1,428 @@
class Actions {
constructor(
private readonly config: Actions.Config,
) {}
public readonly focusLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
leftColumn.focus();
}
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
rightColumn.focus();
}
public readonly focusUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const aboveWindow = column.getAboveWindow(window);
if (aboveWindow === null) {
return;
}
aboveWindow.focus();
}
public readonly focusDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const belowWindow = column.getBelowWindow(window);
if (belowWindow === null) {
return;
}
belowWindow.focus();
}
public readonly focusNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const belowWindow = column.getBelowWindow(window);
if (belowWindow !== null) {
belowWindow.focus();
} else {
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
rightColumn.getFirstWindow().focus();
}
}
public readonly focusPrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const aboveWindow = column.getAboveWindow(window);
if (aboveWindow !== null) {
aboveWindow.focus();
} else {
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
leftColumn.getLastWindow().focus();
}
}
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
}
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
}
public readonly windowMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
window.moveToColumn(leftColumn, true);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getLeftColumn(column));
window.moveToColumn(newColumn, true);
}
}
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom: boolean = true) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
window.moveToColumn(rightColumn, bottom);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn, true);
}
}
// TODO (optimization): only arrange moved windows
public readonly windowMoveUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.moveWindowUp(window);
}
// TODO (optimization): only arrange moved windows
public readonly windowMoveDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.moveWindowDown(window);
}
public readonly windowMoveNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const canMoveDown = window !== column.getLastWindow();
if (canMoveDown) {
column.moveWindowDown(window);
} else {
this.windowMoveRight(cm, dm, window, column, grid, false);
}
}
public readonly windowMovePrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const canMoveUp = window !== column.getFirstWindow();
if (canMoveUp) {
column.moveWindowUp(window);
} else {
this.windowMoveLeft(cm, dm, window, column, grid);
}
}
public readonly windowMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn, true);
}
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn, true);
}
public readonly windowToggleFloating = (cm: ClientManager, dm: DesktopManager) => {
if (Workspace.activeWindow === null) {
return;
}
cm.toggleFloatingClient(Workspace.activeWindow);
}
public readonly columnMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumnLeft(column);
}
public readonly columnMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumnRight(column);
}
public readonly columnMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumn(column, null);
}
public readonly columnMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumn(column, grid.getLastColumn());
}
public readonly columnToggleStacked = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.toggleStacked();
}
public readonly columnWidthIncrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.increaseWidth(column);
}
public readonly columnWidthDecrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.decreaseWidth(column);
}
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
}
public readonly cyclePresetWidthsReverse = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const nextWidth = this.config.presetWidths.prev(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
}
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
const visibleRange = desktop.getCurrentVisibleRange();
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
const availableSpace = desktop.tilingArea.width;
const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1);
const widths = fillSpace(
availableSpace - gapsWidth,
visibleColumns.map(column => ({ min: column.getMinWidth(), max: column.getMaxWidth() })),
);
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Range.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
}
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!Range.contains(visibleRange, focusedColumn)) {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]);
if (targetColumn === null) {
return;
}
const wantedVisibleColumns = [targetColumn, ...currentVisibleColumns];
while (true) {
const success = this.squeezeColumns(wantedVisibleColumns);
if (success) {
break;
}
const removedColumn = wantedVisibleColumns.pop();
if (removedColumn === focusedColumn) {
break; // don't scroll past the currently focused column
}
}
}
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!Range.contains(visibleRange, focusedColumn)) {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]);
if (targetColumn === null) {
return;
}
const wantedVisibleColumns = [...currentVisibleColumns, targetColumn];
while (true) {
const success = this.squeezeColumns(wantedVisibleColumns);
if (success) {
break;
}
const removedColumn = wantedVisibleColumns.shift();
if (removedColumn === focusedColumn) {
break; // don't scroll past the currently focused column
}
}
}
private readonly squeezeColumns = (columns: Column[]) => {
const firstColumn = columns[0];
const lastColumn = columns[columns.length-1];
const grid = firstColumn.grid;
const desktop = grid.desktop;
const availableSpace = desktop.tilingArea.width;
const gapsWidth = grid.config.gapsInnerHorizontal * (columns.length-1);
const columnConstraints = columns.map(column => ({ min: column.getMinWidth(), max: column.getWidth() }));
const minTotalWidth = gapsWidth + columnConstraints.reduce((acc, constraint) => acc + constraint.min, 0);
if (minTotalWidth > availableSpace) {
// there's nothing we can do
return false;
}
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
columns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Range.fromRanges(firstColumn, lastColumn));
return true;
}
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, -this.config.manualScrollStep);
}
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, this.config.manualScrollStep);
}
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
desktopManager.getCurrentDesktop().adjustScroll(amount, false);
}
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
}
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
}
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.desktop.scrollCenterRange(column);
}
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
grid.desktop.scrollToColumn(leftColumn);
}
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
grid.desktop.scrollToColumn(rightColumn);
}
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
dm.selectScreen(Workspace.activeScreen);
}
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.focus();
};
public readonly windowMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
window.moveToColumn(targetColumn, true);
grid.desktop.autoAdjustScroll();
};
public readonly columnMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return;
}
if (targetColumn.isToTheRightOf(column)) {
grid.moveColumn(column, targetColumn);
} else {
grid.moveColumn(column, grid.getLeftColumn(targetColumn));
}
};
public readonly columnMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
};
public readonly tailMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
};
}
namespace Actions {
export type Config = {
manualScrollStep: number;
presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
};
columnResizer: ColumnResizer;
};
export type ColumnResizer = {
increaseWidth(column: Column): void;
decreaseWidth(column: Column): void;
};
}

View File

@@ -0,0 +1,273 @@
function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
return [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
action: () => world.do(actions.windowToggleFloating),
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
action: () => world.doIfTiledFocused(actions.focusLeft),
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
action: () => world.doIfTiledFocused(actions.focusRight),
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
action: () => world.doIfTiledFocused(actions.focusUp),
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
action: () => world.doIfTiledFocused(actions.focusDown),
},
{
name: "focus-next",
description: "Move focus to the next window in grid",
action: () => world.doIfTiledFocused(actions.focusNext),
},
{
name: "focus-previous",
description: "Move focus to the previous window in grid",
action: () => world.doIfTiledFocused(actions.focusPrevious),
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
action: () => world.do(actions.focusStart),
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
action: () => world.do(actions.focusEnd),
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
action: () => world.doIfTiledFocused(actions.windowMoveLeft),
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
action: () => world.doIfTiledFocused(actions.windowMoveRight),
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
action: () => world.doIfTiledFocused(actions.windowMoveUp),
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
action: () => world.doIfTiledFocused(actions.windowMoveDown),
},
{
name: "window-move-next",
description: "Move window to the next position in grid",
action: () => world.doIfTiledFocused(actions.windowMoveNext),
},
{
name: "window-move-previous",
description: "Move window to the previous position in grid",
action: () => world.doIfTiledFocused(actions.windowMovePrevious),
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
action: () => world.doIfTiledFocused(actions.windowMoveStart),
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
action: () => world.doIfTiledFocused(actions.windowMoveEnd),
},
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "One window in the column visible, others shaded; not supported on Wayland",
defaultKeySequence: "Meta+X",
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
action: () => world.doIfTiledFocused(actions.columnMoveLeft),
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
action: () => world.doIfTiledFocused(actions.columnMoveRight),
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
action: () => world.doIfTiledFocused(actions.columnMoveStart),
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
action: () => world.doIfTiledFocused(actions.columnMoveEnd),
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
action: () => world.doIfTiledFocused(actions.columnWidthIncrease),
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
action: () => world.doIfTiledFocused(actions.columnWidthDecrease),
},
{
name: "cycle-preset-widths",
description: "Cycle through preset column widths",
defaultKeySequence: "Meta+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
},
{
name: "cycle-preset-widths-reverse",
description: "Cycle through preset column widths in reverse",
defaultKeySequence: "Meta+Shift+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidthsReverse),
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
action: () => world.do(actions.columnsWidthEqualize),
},
{
name: "columns-squeeze-left",
description: "Squeeze left column onto the screen",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+Ctrl+A",
action: () => world.doIfTiledFocused(actions.columnsSqueezeLeft),
},
{
name: "columns-squeeze-right",
description: "Squeeze right column onto the screen",
defaultKeySequence: "Meta+Ctrl+D",
action: () => world.doIfTiledFocused(actions.columnsSqueezeRight),
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
action: () => world.doIfTiledFocused(actions.gridScrollFocused),
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
action: () => world.do(actions.gridScrollLeftColumn),
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
action: () => world.do(actions.gridScrollRightColumn),
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
action: () => world.do(actions.gridScrollLeft),
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
action: () => world.do(actions.gridScrollRight),
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
action: () => world.do(actions.gridScrollStart),
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
action: () => world.do(actions.gridScrollEnd),
},
{
name: "screen-switch",
description: "Move Karousel grid to the current screen",
defaultKeySequence: "Meta+Ctrl+Return",
action: () => world.do(actions.screenSwitch),
},
];
}
function getNumKeyBindings(world: World, actions: Actions): NumKeyBinding[] {
return [
{
name: "focus-{}",
description: "Move focus to column {}",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
action: (i: number) => world.do(actions.focus.partial(i)),
},
{
name: "window-move-to-column-{}",
description: "Move window to column {}",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
defaultModifiers: "Meta+Shift",
fKeys: false,
action: (i: number) => world.doIfTiledFocused(actions.windowMoveToColumn.partial(i)),
},
{
name: "column-move-to-column-{}",
description: "Move column to position {}",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: false,
action: (i: number) => world.doIfTiledFocused(actions.columnMoveToColumn.partial(i)),
},
{
name: "column-move-to-desktop-{}",
description: "Move column to desktop {}",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
action: (i: number) => world.doIfTiledFocused(actions.columnMoveToDesktop.partial(i)),
},
{
name: "tail-move-to-desktop-{}",
description: "Move this and all following columns to desktop {}",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
action: (i: number) => world.doIfTiledFocused(actions.tailMoveToDesktop.partial(i)),
},
];
}

View File

@@ -0,0 +1,68 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence?: string;
action: () => void;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: (i: number) => void;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(keyBinding.action),
));
}
function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
const numKey = String(i + 1);
const keySequence = i < n ?
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
"";
shortcutActions.push(new ShortcutAction(
{
name: applyMacro(numKeyBinding.name, numKey),
description: applyMacro(numKeyBinding.description, numKey),
defaultKeySequence: keySequence,
},
catchWrap(() => numKeyBinding.action(i)),
));
}
}
function registerKeyBindings(world: World, config: Actions.Config) {
const actions = new Actions(config);
const shortcutActions: ShortcutAction[] = [];
for (const keyBinding of getKeyBindings(world, actions)) {
registerKeyBinding(shortcutActions, keyBinding);
}
for (const numKeyBinding of getNumKeyBindings(world, actions)) {
registerNumKeyBindings(shortcutActions, numKeyBinding);
}
return shortcutActions;
}

337
src/lib/layout/Column.ts Normal file
View File

@@ -0,0 +1,337 @@
class Column {
public grid: Grid;
public gridX: number;
private width: number; // TODO: increase column width to contain transients
private readonly windows: LinkedList<Window>;
private stacked: boolean;
private focusTaker: Window|null;
private static readonly minWidth = 40;
constructor(grid: Grid, leftColumn: Column|null) {
this.gridX = 0;
this.width = 0;
this.windows = new LinkedList();
this.stacked = grid.config.stackColumnsByDefault;
this.focusTaker = null;
this.grid = grid;
this.grid.onColumnAdded(this, leftColumn);
}
public moveToGrid(targetGrid: Grid, leftColumn: Column|null) {
if (targetGrid === this.grid) {
this.grid.moveColumn(this, leftColumn);
} else {
this.grid.onColumnRemoved(this, this.isFocused());
this.grid = targetGrid;
targetGrid.onColumnAdded(this, leftColumn);
for (const window of this.windows.iterator()) {
window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
}
}
}
public isToTheLeftOf(other: Column) {
return this.gridX < other.gridX;
}
public isToTheRightOf(other: Column) {
return this.gridX > other.gridX;
}
public moveWindowUp(window: Window) {
this.windows.moveBack(window);
this.grid.desktop.onLayoutChanged();
}
public moveWindowDown(window: Window) {
this.windows.moveForward(window);
this.grid.desktop.onLayoutChanged();
}
public getWindowCount() {
return this.windows.length();
}
public isEmpty() {
return this.getWindowCount() === 0;
}
public getFirstWindow(): Window {
return this.windows.getFirst()!;
}
public getLastWindow(): Window {
return this.windows.getLast()!;
}
public getAboveWindow(window: Window) {
return this.windows.getPrev(window);
}
public getBelowWindow(window: Window) {
return this.windows.getNext(window);
}
public getWidth() {
return this.width;
}
public getMinWidth() {
let maxMinWidth = Column.minWidth;
for (const window of this.windows.iterator()) {
const minWidth = window.client.kwinClient.minSize.width;
if (minWidth > maxMinWidth) {
maxMinWidth = minWidth;
}
}
return maxMinWidth;
}
public getMaxWidth() {
return this.grid.desktop.tilingArea.width;
}
public setWidth(width: number, setPreferred: boolean) {
width = clamp(width, this.getMinWidth(), this.getMaxWidth());
if (width === this.width) {
return;
}
this.width = width;
if (setPreferred) {
for (const window of this.windows.iterator()) {
window.client.preferredWidth = width;
}
}
this.grid.onColumnWidthChanged(this);
}
public adjustWidth(widthDelta: number, setPreferred: boolean) {
this.setWidth(this.width + widthDelta, setPreferred);
}
public updateWidth() {
let minErr = Infinity;
let closestPreferredWidth = this.width;
for (const window of this.windows.iterator()) {
const err = Math.abs(window.client.preferredWidth - this.width);
if (err < minErr) {
minErr = err;
closestPreferredWidth = window.client.preferredWidth;
}
}
this.setWidth(closestPreferredWidth, false);
}
// returns x position of left edge in grid space
public getLeft() {
return this.gridX;
}
// returns x position of right edge in grid space
public getRight() {
return this.gridX + this.width;
}
public onUserResizeWidth(
startWidth: number,
currentDelta: number,
resizingLeftSide: boolean,
neighbor?: { column: Column, startWidth: number },
) {
const oldColumnWidth = this.getWidth();
this.setWidth(startWidth + currentDelta, true);
const actualDelta = this.getWidth() - startWidth;
let leftEdgeDeltaStep = resizingLeftSide ? oldColumnWidth - this.getWidth() : 0;
if (neighbor !== undefined) {
const oldNeighborWidth = neighbor.column.getWidth();
neighbor.column.setWidth(neighbor.startWidth - actualDelta, true);
if (resizingLeftSide) {
leftEdgeDeltaStep -= neighbor.column.getWidth() - oldNeighborWidth;
}
}
this.grid.desktop.adjustScroll(-leftEdgeDeltaStep, true);
}
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
if (otherWindow === null) {
return;
}
window.height += heightDelta;
otherWindow.height -= heightDelta;
this.grid.desktop.onLayoutChanged();
}
public resizeWindows() {
const nWindows = this.windows.length();
if (nWindows === 0) {
return;
}
if (nWindows === 1) {
this.stacked = this.grid.config.stackColumnsByDefault;
}
let remainingPixels = this.grid.desktop.tilingArea.height - (nWindows-1) * this.grid.config.gapsInnerVertical;
let remainingWindows = nWindows;
for (const window of this.windows.iterator()) {
const windowHeight = Math.round(remainingPixels / remainingWindows);
window.height = windowHeight;
remainingPixels -= windowHeight;
remainingWindows--;
}
// TODO: respect min height
this.grid.desktop.onLayoutChanged();
}
public getFocusTaker() {
if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) {
return null;
}
return this.focusTaker;
}
public focus() {
const window = this.getFocusTaker() ?? this.windows.getFirst();
if (window === null) {
return;
}
window.focus();
}
public isFocused() {
const lastFocusedWindow = this.grid.getLastFocusedWindow();
if (lastFocusedWindow === null) {
return false;
}
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
}
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = Range.contains(visibleRange, this) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
}
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x);
return;
}
let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
window.client.setShade(false);
window.arrange(x, y, this.width, window.height);
y += window.height + this.grid.config.gapsInnerVertical;
}
}
public arrangeStacked(x: number) {
const expandedWindow = this.getFocusTaker();
let collapsedHeight;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.client.setShade(false);
} else {
window.client.setShade(true);
collapsedHeight = window.client.kwinClient.frameGeometry.height;
}
}
const nCollapsed = this.getWindowCount() - 1;
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.arrange(x, y, this.width, expandedHeight);
y += expandedHeight;
} else {
window.arrange(x, y, this.width, window.height);
y += collapsedHeight!;
}
y += this.grid.config.gapsInnerVertical;
}
}
public toggleStacked() {
if (this.windows.length() < 2) {
return;
}
this.stacked = !this.stacked;
this.grid.desktop.onLayoutChanged();
}
private canStack() {
for (const window of this.windows.iterator()) {
if (!window.client.kwinClient.shadeable) {
return false;
}
}
return true;
}
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);
} else {
this.windows.insertStart(window);
}
if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false);
}
// TODO: also change column width if the new window requires it
this.resizeWindows();
if (window.isFocused()) {
this.onWindowFocused(window);
}
this.grid.desktop.onLayoutChanged();
}
public onWindowRemoved(window: Window, passFocus: boolean) {
const lastWindow = this.windows.length() === 1;
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
this.windows.remove(window);
if (window === this.focusTaker) {
this.focusTaker = windowToFocus;
}
if (lastWindow) {
console.assert(this.isEmpty());
this.destroy(passFocus);
} else {
this.resizeWindows();
if (passFocus && windowToFocus !== null) {
windowToFocus.focus();
}
}
this.grid.desktop.onLayoutChanged();
}
public onWindowFocused(window: Window) {
this.grid.onColumnFocused(this);
this.focusTaker = window;
}
public restoreToTiled() {
const lastFocusedWindow = this.getFocusTaker();
if (lastFocusedWindow !== null) {
lastFocusedWindow.restoreToTiled();
}
}
private destroy(passFocus: boolean) {
this.grid.onColumnRemoved(this, passFocus);
}
}

247
src/lib/layout/Desktop.ts Normal file
View File

@@ -0,0 +1,247 @@
class Desktop {
public readonly grid: Grid;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(
public readonly kwinDesktop: KwinDesktop,
private readonly pinManager: PinManager,
private readonly config: Desktop.Config,
private readonly getScreen: () => Output,
layoutConfig: LayoutConfig,
) {
this.scrollX = 0;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.getScreen(), this.kwinDesktop);
if (rectEquals(newClientArea, this.clientArea) && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
private static getClientArea(screen: Output, kwinDesktop: KwinDesktop) {
return Workspace.clientArea(ClientAreaOption.PlacementArea, screen, kwinDesktop);
}
private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(kwinDesktop, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
left,
top,
right - left,
bottom - top,
)
}
public scrollIntoView(range: Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
let targetScrollX: number;
if (left < initialVisibleRange.getLeft()) {
targetScrollX = left;
} else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width;
} else {
targetScrollX = initialVisibleRange.getLeft();
}
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), true);
}
public scrollCenterVisible(focusedColumn: Column) {
const columnRange = new Desktop.ColumnRange(focusedColumn);
const visibleRange = this.getCurrentVisibleRange();
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal);
this.scrollCenterRange(columnRange);
}
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null || focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(focusedColumn);
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return Range.create(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
return this.getVisibleRange(this.scrollX);
}
private clampScrollX(x: number) {
return this.config.clamper.clampScrollX(this, x);
}
public setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
this.dirtyScroll = false;
}
public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
this.dirty = false;
}
public forceArrange() {
this.dirty = true;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}
}
namespace Desktop {
export type Config = {
marginTop: number;
marginBottom: number;
marginLeft: number;
marginRight: number;
scroller: Desktop.Scroller;
clamper: Desktop.Clamper;
};
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
function canFit(column: Column) {
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth();
}
function isUsable(column: Column|null) {
return column !== null && canFit(column);
}
let leftColumn = grid.getLeftColumn(this.left);
let rightColumn = grid.getRightColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
}
if (!isUsable(rightColumn)) {
rightColumn = null;
}
}
checkColumns();
const visibleCenter = visibleRange.getLeft() + visibleRange.getWidth() / 2;
while (leftColumn !== null || rightColumn !== null) {
const leftToCenter = leftColumn === null ? Infinity : Math.abs(leftColumn.getLeft() - visibleCenter);
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getLeftColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getRightColumn(rightColumn!);
}
checkColumns();
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
};
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
};
}

227
src/lib/layout/Grid.ts Normal file
View File

@@ -0,0 +1,227 @@
class Grid {
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
private readonly columns: LinkedList<Column>;
private lastFocusedColumn: Column|null;
private width: number;
private userResize: boolean; // is any part of the grid being resized by the user
private readonly userResizeFinishedDelayer: Delayer;
constructor(desktop: Desktop, config: LayoutConfig) {
this.desktop = desktop;
this.config = config;
this.columns = new LinkedList();
this.lastFocusedColumn = null;
this.width = 0;
this.userResize = false;
this.userResizeFinishedDelayer = new Delayer(50, () => {
// this delay prevents windows' contents from freezing after resizing
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
this.desktop.arrange();
});
}
public moveColumn(column: Column, leftColumn: Column|null) {
if (column === leftColumn) {
return;
}
const movedLeft = leftColumn === null ? true : column.isToTheRightOf(leftColumn);
const firstMovedColumn = movedLeft ? column : this.getRightColumn(column);
this.columns.move(column, leftColumn);
this.columnsSetX(firstMovedColumn);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public moveColumnLeft(column: Column) {
this.columns.moveBack(column);
this.columnsSetX(column);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public moveColumnRight(column: Column) {
const rightColumn = this.columns.getNext(column);
if (rightColumn === null) {
return;
}
this.moveColumnLeft(rightColumn);
}
public getWidth() {
return this.width;
}
public isUserResizing() {
return this.userResize;
}
public getLeftColumn(column: Column) {
return this.columns.getPrev(column);
}
public getRightColumn(column: Column) {
return this.columns.getNext(column);
}
public getFirstColumn() {
return this.columns.getFirst();
}
public getLastColumn() {
return this.columns.getLast();
}
public getColumnAtIndex(i: number) {
return this.columns.getItemAtIndex(i);
}
public getLastFocusedColumn() {
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
return null;
}
return this.lastFocusedColumn;
}
public getLastFocusedWindow() {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn === null) {
return null;
}
return lastFocusedColumn.getFocusTaker();
}
private columnsSetX(firstMovedColumn: Column|null) {
const lastUnmovedColumn = firstMovedColumn === null ? this.columns.getLast() : this.columns.getPrev(firstMovedColumn);
let x = lastUnmovedColumn === null ? 0 : lastUnmovedColumn.getRight() + this.config.gapsInnerHorizontal;
if (firstMovedColumn !== null) {
for (const column of this.columns.iteratorFrom(firstMovedColumn)) {
column.gridX = x;
x += column.getWidth() + this.config.gapsInnerHorizontal;
}
}
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
return column;
}
}
return null;
}
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
last = column;
} else if (last !== null) {
break;
}
}
return last;
}
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
yield column;
}
}
}
public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize);
x += column.getWidth() + this.config.gapsInnerHorizontal;
}
const focusedWindow = this.getLastFocusedWindow();
if (focusedWindow !== null) {
focusedWindow.client.ensureTransientsVisible(this.desktop.clientArea);
}
}
public onColumnAdded(column: Column, leftColumn: Column|null) {
if (leftColumn === null) {
this.columns.insertStart(column);
} else {
this.columns.insertAfter(column, leftColumn);
}
this.columnsSetX(column);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public onColumnRemoved(column: Column, passFocus: boolean) {
const isLastColumn = this.columns.length() === 1;
const rightColumn = this.getRightColumn(column);
const columnToFocus = isLastColumn ? null : this.getLeftColumn(column) ?? rightColumn;
if (column === this.lastFocusedColumn) {
this.lastFocusedColumn = columnToFocus;
}
this.columns.remove(column);
this.columnsSetX(rightColumn);
this.desktop.onLayoutChanged();
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} else {
this.desktop.autoAdjustScroll();
}
}
public onColumnWidthChanged(column: Column) {
const rightColumn = this.columns.getNext(column);
this.columnsSetX(rightColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) {
this.desktop.autoAdjustScroll();
}
}
public onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null && lastFocusedColumn !== column) {
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;
this.desktop.scrollToColumn(column);
}
public onScreenSizeChanged() {
for (const column of this.columns.iterator()) {
column.updateWidth();
column.resizeWindows();
}
}
public onUserResizeStarted() {
this.userResize = true;
}
public onUserResizeFinished() {
this.userResize = false;
this.userResizeFinishedDelayer.run();
}
public evacuateTail(targetGrid: Grid, startColumn: Column) {
for (const column of this.columns.iteratorFrom(startColumn)) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
public evacuate(targetGrid: Grid) {
for (const column of this.columns.iterator()) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
public destroy() {
this.userResizeFinishedDelayer.destroy();
}
}

View File

@@ -0,0 +1,12 @@
type LayoutConfig = {
gapsInnerHorizontal: number;
gapsInnerVertical: number;
offScreenOpacity: number;
stackColumnsByDefault: boolean;
resizeNeighborColumn: boolean;
reMaximize: boolean;
skipSwitcher: boolean;
tiledKeepBelow: boolean;
maximizedKeepAbove: boolean;
untileOnDrag: boolean;
};

41
src/lib/layout/Range.ts Normal file
View File

@@ -0,0 +1,41 @@
type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
};
namespace Range {
export function create(x: number, width: number) {
return new Basic(x, width);
}
export function fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new Basic(left, right - left);
}
export function contains(parent: Range, child: Range) {
return child.getLeft() >= parent.getLeft() &&
child.getRight() <= parent.getRight();
}
class Basic {
constructor(
private readonly x: number,
private readonly width: number,
) {}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
}
}

133
src/lib/layout/Window.ts Normal file
View File

@@ -0,0 +1,133 @@
class Window {
public column: Column;
public readonly client: ClientWrapper;
public height: number;
public readonly focusedState: Window.State;
private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedMode: MaximizedMode.Unmaximized,
};
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this, true);
}
public moveToColumn(targetColumn: Column, bottom: boolean) {
if (targetColumn === this.column) {
return;
}
this.column.onWindowRemoved(this, this.isFocused() && targetColumn.grid !== this.column.grid);
this.column = targetColumn;
targetColumn.onWindowAdded(this, bottom);
}
public arrange(x: number, y: number, width: number, height: number) {
if (this.skipArrange) {
// window is maximized, fullscreen, or being manually resized, prevent fighting with the user
return;
}
let maximized = false;
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedMode !== MaximizedMode.Unmaximized) {
this.client.setMaximize(
this.focusedState.maximizedMode === MaximizedMode.Horizontally || this.focusedState.maximizedMode === MaximizedMode.Maximized,
this.focusedState.maximizedMode === MaximizedMode.Vertically || this.focusedState.maximizedMode === MaximizedMode.Maximized,
);
maximized = true;
}
if (this.focusedState.fullScreen) {
this.client.setFullScreen(true);
maximized = true;
}
}
if (!maximized) {
this.client.place(x, y, width, height);
}
}
public focus() {
if (this.client.isShaded()) {
// workaround for KWin deactivating clients when unshading immediately after activation
this.client.setShade(false);
}
this.client.focus();
}
public isFocused() {
return this.client.isFocused();
}
public onFocused() {
if (this.column.grid.config.reMaximize && (
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
this.focusedState.fullScreen
)) {
// We need to maximize/fullscreen this window, but we can't do it here.
// We need to do it in `arrange` to ensure it happens after placement.
this.column.grid.desktop.forceArrange();
}
this.column.onWindowFocused(this);
}
public restoreToTiled() {
if (this.isFocused()) {
return;
}
this.client.setFullScreen(false);
this.client.setMaximize(false, false);
}
public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = maximizedMode !== MaximizedMode.Unmaximized;
this.skipArrange = maximized;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = maximized;
}
if (this.isFocused()) {
this.focusedState.maximizedMode = maximizedMode;
}
this.column.grid.desktop.onLayoutChanged();
}
public onFullScreenChanged(fullScreen: boolean) {
this.skipArrange = fullScreen;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !fullScreen;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = fullScreen;
}
if (this.isFocused()) {
this.focusedState.fullScreen = fullScreen;
}
this.column.grid.desktop.onLayoutChanged();
}
public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true);
this.column.grid.desktop.onLayoutChanged();
}
public destroy(passFocus: boolean) {
this.column.onWindowRemoved(this, passFocus);
}
}
namespace Window {
export type State = {
fullScreen: boolean;
maximizedMode: MaximizedMode;
};
}

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,93 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: RegExp;
constructor(windowRules: WindowRule[]) {
const [floatRegex, tileRegex, followCaptionRegex] = WindowRuleEnforcer.createWindowRuleRegexes(windowRules);
this.preferFloating = new ClientMatcher(floatRegex);
this.preferTiling = new ClientMatcher(tileRegex);
this.followCaption = followCaptionRegex;
}
public shouldTile(kwinClient: KwinClient) {
return this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!this.preferFloating.matches(kwinClient)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.test(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = Clients.canTileNow(kwinClient) && enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
clientManager.tileKwinClient(kwinClient, desktop.grid);
} else {
clientManager.floatKwinClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleRegexes(windowRules: WindowRule[]) {
const floatRegexes: string[] = [];
const tileRegexes: string[] = [];
const followCaptionRegexes: string[] = [];
for (const windowRule of windowRules) {
const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
const ruleString = ClientMatcher.getRuleString(
WindowRuleEnforcer.wrapParens(ruleClass),
WindowRuleEnforcer.wrapParens(ruleCaption)
);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
if (ruleCaption !== ".*") {
followCaptionRegexes.push(ruleClass);
}
}
return [
WindowRuleEnforcer.joinRegexes(floatRegexes),
WindowRuleEnforcer.joinRegexes(tileRegexes),
WindowRuleEnforcer.joinRegexes(followCaptionRegexes),
];
}
private static parseRegex(rawRule: string | undefined) {
if (rawRule === undefined || rawRule === "" || rawRule === ".*") {
return ".*";
} else {
return rawRule;
}
}
private static joinRegexes(regexes: string[]) {
if (regexes.length === 0) {
return new RegExp("a^"); // match nothing
}
if (regexes.length === 1) {
return new RegExp("^(" + regexes[0] + ")$");
}
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^(" + joinedRegexes + ")$");
}
private static wrapParens(str: string) {
return "(" + str + ")";
}
}

View File

@@ -1,5 +1,5 @@
class Delayer {
private readonly timer: QQmlTimer;
private readonly timer: QmlTimer;
constructor(delay: number, f: () => void) {
this.timer = initQmlTimer();
@@ -7,18 +7,18 @@ class Delayer {
this.timer.triggered.connect(f);
}
run() {
public run() {
this.timer.restart();
}
destroy() {
public destroy() {
this.timer.destroy();
}
}
function initQmlTimer() {
return Qt.createQmlObject(
`import QtQuick 2.15
return <QmlTimer>Qt.createQmlObject(
`import QtQuick 6.0
Timer {}`,
qmlBase
);

View File

@@ -5,13 +5,13 @@ class Doer {
this.nCalls = 0;
}
do (f: () => void) {
public do (f: () => void) {
this.nCalls++;
f();
this.nCalls--;
}
isDoing() {
public isDoing() {
return this.nCalls > 0;
}
}

View File

@@ -1,7 +1,7 @@
class LinkedList<T> {
private firstNode: LinkedListNode<T>|null;
private lastNode: LinkedListNode<T>|null;
private readonly itemMap: Map<T, LinkedListNode<T>>;
private firstNode: LinkedList.Node<T>|null;
private lastNode: LinkedList.Node<T>|null;
private readonly itemMap: Map<T, LinkedList.Node<T>>;
constructor() {
this.firstNode = null;
@@ -17,31 +17,31 @@ class LinkedList<T> {
return node;
}
insertBefore(item: T, nextItem: T) {
public insertBefore(item: T, nextItem: T) {
const nextNode = this.getNode(nextItem);
this.insert(item, nextNode.prev, nextNode);
}
insertAfter(item: T, prevItem: T) {
public insertAfter(item: T, prevItem: T) {
const prevNode = this.getNode(prevItem);
this.insert(item, prevNode, prevNode.next);
}
insertStart(item: T) {
public insertStart(item: T) {
this.insert(item, null, this.firstNode);
}
insertEnd(item: T) {
public insertEnd(item: T) {
this.insert(item, this.lastNode, null);
}
private insert(item: T, prevNode: LinkedListNode<T>|null, nextNode: LinkedListNode<T>|null) {
const node = new LinkedListNode(item);
private insert(item: T, prevNode: LinkedList.Node<T>|null, nextNode: LinkedList.Node<T>|null) {
const node = new LinkedList.Node(item);
this.itemMap.set(item, node);
this.insertNode(node, prevNode, nextNode);
}
private insertNode(node: LinkedListNode<T>, prevNode: LinkedListNode<T>|null, nextNode: LinkedListNode<T>|null) {
private insertNode(node: LinkedList.Node<T>, prevNode: LinkedList.Node<T>|null, nextNode: LinkedList.Node<T>|null) {
node.prev = prevNode;
node.next = nextNode;
if (nextNode !== null) {
@@ -60,31 +60,31 @@ class LinkedList<T> {
}
}
getPrev(item: T) {
public getPrev(item: T) {
const prevNode = this.getNode(item).prev;
return prevNode === null ? null : prevNode.item;
}
getNext(item: T) {
public getNext(item: T) {
const nextNode = this.getNode(item).next;
return nextNode === null ? null : nextNode.item;
}
getFirst() {
public getFirst() {
if (this.firstNode === null) {
return null;
}
return this.firstNode.item;
}
getLast() {
public getLast() {
if (this.lastNode === null) {
return null;
}
return this.lastNode.item;
}
getItemAtIndex(index: number) {
public getItemAtIndex(index: number) {
let node = this.firstNode;
if (node === null) {
return null;
@@ -98,13 +98,13 @@ class LinkedList<T> {
return node.item;
}
remove(item: T) {
public remove(item: T) {
const node = this.getNode(item);
this.itemMap.delete(item);
this.removeNode(node);
}
private removeNode(node: LinkedListNode<T>) {
private removeNode(node: LinkedList.Node<T>) {
const prevNode = node.prev;
const nextNode = node.next;
if (prevNode !== null) {
@@ -121,11 +121,11 @@ class LinkedList<T> {
}
}
contains(item: T) {
public contains(item: T) {
return this.itemMap.has(item);
}
private swap(node0: LinkedListNode<T>, node1: LinkedListNode<T>) {
private swap(node0: LinkedList.Node<T>, node1: LinkedList.Node<T>) {
console.assert(node0.next === node1 && node1.prev === node0);
const prevNode = node0.prev;
const nextNode = node1.next;
@@ -150,7 +150,7 @@ class LinkedList<T> {
}
}
move(item: T, prevItem: T|null) {
public move(item: T, prevItem: T|null) {
const node = this.getNode(item);
this.removeNode(node);
if (prevItem === null) {
@@ -161,7 +161,7 @@ class LinkedList<T> {
}
}
moveBack(item: T) {
public moveBack(item: T) {
const node = this.getNode(item);
if (node.prev !== null) {
console.assert(node !== this.firstNode);
@@ -169,7 +169,7 @@ class LinkedList<T> {
}
}
moveForward(item: T) {
public moveForward(item: T) {
const node = this.getNode(item);
if (node.next !== null) {
console.assert(node !== this.lastNode);
@@ -177,32 +177,34 @@ class LinkedList<T> {
}
}
length() {
public length() {
return this.itemMap.size;
}
*iterator() {
public *iterator() {
for (let node = this.firstNode; node !== null; node = node.next) {
yield node.item;
}
}
*iteratorFrom(startItem: T) {
for (let node: LinkedListNode<T>|null = this.getNode(startItem); node !== null; node = node.next) {
public *iteratorFrom(startItem: T) {
for (let node: LinkedList.Node<T>|null = this.getNode(startItem); node !== null; node = node.next) {
yield node.item;
}
}
}
// TODO (optimization): reuse nodes
class LinkedListNode<T> {
public readonly item: T;
public prev: LinkedListNode<T>|null;
public next: LinkedListNode<T>|null;
namespace LinkedList {
// TODO (optimization): reuse nodes
export class Node<T> {
public readonly item: T;
public prev: Node<T> | null;
public next: Node<T> | null;
constructor(item: T) {
this.item = item;
this.prev = null;
this.next = null;
constructor(item: T) {
this.item = item;
this.prev = null;
this.next = null;
}
}
}

View File

@@ -0,0 +1,24 @@
class RateLimiter {
private i = 0;
private intervalStart = 0;
constructor(
private readonly n: number,
private readonly intervalMs: number,
) {}
public acquire() {
const now = Date.now();
if (now - this.intervalStart >= this.intervalMs) {
this.i = 0;
this.intervalStart = now;
}
if (this.i < this.n) {
this.i++;
return true;
} else {
return false;
}
}
}

View File

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

View File

@@ -1,16 +1,16 @@
class SignalManager {
private connections: { signal: QSignal, handler: (...args: any[]) => void }[];
private connections: { signal: QSignal<any>, handler: (...args: any) => void }[];
constructor() {
this.connections = [];
}
connect(signal: QSignal, handler: (...args: any[]) => void) {
public connect<T extends unknown[]>(signal: QSignal<T>, handler: (...args: [...T]) => void) {
signal.connect(handler);
this.connections.push({ signal: signal, handler: handler });
}
destroy() {
public destroy() {
for (const connection of this.connections) {
connection.signal.disconnect(connection.handler);
}

View File

@@ -0,0 +1,39 @@
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}
function uniq(sortedArray: any[]) {
const filtered = [];
let lastItem;
for (const item of sortedArray) {
if (item !== lastItem) {
filtered.push(item);
lastItem = item;
}
}
return filtered;
}
function mapGetOrInit<K, V>(map: Map<K, V>, key: K, defaultItem: V) {
const item = map.get(key);
if (item !== undefined) {
return item;
} else {
map.set(key, defaultItem);
return defaultItem;
}
}
function findMinPositive<T>(items: T[], evaluate: (item: T) => number) {
let bestScore = Infinity;
let bestItem = undefined;
for (const item of items) {
const score = evaluate(item);
if (score > 0 && score < bestScore) {
bestScore = score;
bestItem = item;
}
}
return bestItem;
}

View File

@@ -0,0 +1,98 @@
function fillSpace(availableSpace: number, items: { min: number, max: number }[]) {
if (items.length === 0) {
return [];
}
const middleSize = findMiddleSize(availableSpace, items);
const sizes = items.map(item => clamp(middleSize, item.min, item.max));
if (middleSize !== Math.floor(availableSpace / items.length)) {
distributeRemainder(availableSpace, middleSize, sizes, items);
}
return sizes;
function findMiddleSize(availableSpace: number, items: { min: number, max: number }[]) {
const ranges = buildRanges(items);
let requiredSpace = items.reduce((acc, item) => acc + item.min, 0);
for (const range of ranges) {
const rangeSize = range.end - range.start;
const maxRequiredSpaceDelta = rangeSize * range.n;
if (requiredSpace + maxRequiredSpaceDelta >= availableSpace) {
const positionInRange = (availableSpace - requiredSpace) / maxRequiredSpaceDelta;
return Math.floor(range.start + rangeSize * positionInRange);
}
requiredSpace += maxRequiredSpaceDelta;
}
return ranges[ranges.length-1].end;
}
function buildRanges(items: { min: number, max: number }[]) {
const fenceposts = extractFenceposts(items);
if (fenceposts.length === 1) {
return [{
start: fenceposts[0].value,
end: fenceposts[0].value,
n: items.length,
}];
}
const ranges: Range[] = [];
let n = 0;
for (let i = 1; i < fenceposts.length; i++) {
const startFencepost = fenceposts[i-1];
const endFencepost = fenceposts[i];
n = n - startFencepost.nMax + startFencepost.nMin;
ranges.push({
start: startFencepost.value,
end: endFencepost.value,
n: n,
});
}
return ranges;
}
function extractFenceposts(items: { min: number, max: number }[]) {
const fenceposts = new Map<number, Fencepost>();
for (const item of items) {
mapGetOrInit(fenceposts, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
mapGetOrInit(fenceposts, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
}
const array = Array.from(fenceposts.values());
array.sort((a, b) => a.value - b.value);
return array;
}
function distributeRemainder(availableSpace: number, middleSize: number, sizes: number[], constraints: { max: number }[]) {
const indexes = Array.from(sizes.keys())
.filter(i => sizes[i] === middleSize);
indexes.sort((a, b) => constraints[a].max - constraints[b].max);
const requiredSpace = sum(...sizes);
let remainder = availableSpace - requiredSpace;
let n = indexes.length;
for (const i of indexes) {
if (remainder <= 0) {
break;
}
const enlargable = constraints[i].max - sizes[i];
if (enlargable > 0) {
const enlarge = Math.min(enlargable, Math.ceil(remainder / n));
sizes[i] += enlarge;
remainder -= enlarge;
}
n--;
}
}
type Range = {
start: number,
end: number,
n: number,
};
type Fencepost = {
value: number,
nMin: number,
nMax: number,
}
}

View File

@@ -0,0 +1,10 @@
interface Function {
partial<H extends any[], T extends any[], R>(
this: (...args: [...H, ...T]) => R,
...head: H
) : (...tail: T) => R;
}
Function.prototype.partial = function<H extends any[], T extends any[]>(...head: H) {
return (...tail: T) => this(...head, ...tail);
}

3
src/lib/utils/log.ts Normal file
View File

@@ -0,0 +1,3 @@
function log(...args: any[]) {
console.log("Karousel:", ...args);
}

View File

@@ -8,7 +8,11 @@ function clamp(value: number, min: number, max: number) {
return value;
}
function rectEqual(a: QRect, b: QRect) {
function sum(...list: number[]) {
return list.reduce((acc, val) => acc + val);
}
function rectEquals(a: QmlRect, b: QmlRect) {
return a.x === b.x &&
a.y === b.y &&
a.width === b.width &&

3
src/lib/utils/strings.ts Normal file
View File

@@ -0,0 +1,3 @@
function applyMacro(base: string, value: string) {
return base.replace("{}", String(value));
}

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

@@ -0,0 +1,56 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
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.screensChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.selectScreen(Workspace.activeScreen);
});
});
manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
});
});
manager.connect(Workspace.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
});
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -0,0 +1,222 @@
class ClientManager {
private readonly config: ClientManager.Config;
private readonly clientMap: Map<KwinClient, ClientWrapper>;
private lastFocusedClient: KwinClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(
config: Config,
private readonly world: World,
private readonly desktopManager: DesktopManager,
private readonly pinManager: PinManager,
) {
this.world = world;
this.config = config;
this.desktopManager = desktopManager;
this.pinManager = pinManager;
this.clientMap = new Map();
this.lastFocusedClient = null;
let parsedWindowRules: WindowRule[] = [];
try {
parsedWindowRules = JSON.parse(config.windowRules);
} catch (error: any) {
notificationInvalidWindowRules.sendEvent();
log("failed to parse windowRules:", error);
}
this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules);
}
public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient));
let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (
Clients.canTileEver(kwinClient) &&
!kwinClient.fullScreen &&
!Clients.isFullScreenGeometry(kwinClient) &&
this.windowRuleEnforcer.shouldTile(kwinClient)
) {
Clients.makeTileable(kwinClient);
console.assert(Clients.canTileNow(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
console.assert(desktop !== undefined);
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
}
const client = new ClientWrapper(
kwinClient,
constructState,
this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient),
);
this.clientMap.set(kwinClient, client);
}
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
console.assert(this.hasClient(kwinClient));
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
this.clientMap.delete(kwinClient);
}
private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient || kwinClient.transientFor === null) {
return null;
}
const transientFor = this.clientMap.get(kwinClient.transientFor);
if (transientFor === undefined) {
return null;
}
return transientFor;
}
public minimizeClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(
() => new ClientState.TiledMinimized(this.world, client),
kwinClient === this.lastFocusedClient,
);
}
}
public tileClient(client: ClientWrapper, grid: Grid) {
if (client.stateManager.getState() instanceof ClientState.Tiled) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
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);
if (client === undefined) {
return;
}
this.tileClient(client, grid);
}
public floatKwinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
this.floatClient(client);
}
public pinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.getMaximizedMode() !== MaximizedMode.Unmaximized) {
// the client is not really kwin-tiled, just maximized
kwinClient.tile = null;
return;
}
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
this.pinManager.addClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
}
public unpinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
this.pinManager.removeClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
}
public toggleFloatingClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const clientState = client.stateManager.getState();
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient);
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
} else if (clientState instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
}
public hasClient(kwinClient: KwinClient) {
return this.clientMap.has(kwinClient);
}
public onClientFocused(kwinClient: KwinClient) {
this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient);
if (window !== null) {
window.onFocused();
}
}
public findTiledWindow(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return null;
}
return this.findTiledWindowOfClient(client);
}
private findTiledWindowOfClient(client: ClientWrapper): Window|null {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientState.Tiled) {
return clientState.window;
} else if (client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor);
} else {
return null;
}
}
private removeAllClients() {
for (const kwinClient of Array.from(this.clientMap.keys())) {
this.removeClient(kwinClient, false);
}
}
public destroy() {
this.removeAllClients();
}
}
namespace ClientManager {
export type Config = {
floatingKeepAbove: boolean;
};
}

View File

@@ -0,0 +1,188 @@
class ClientWrapper {
public readonly stateManager: ClientState.Manager;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor(
public readonly kwinClient: KwinClient,
constructInitialState: (client: ClientWrapper) => ClientState.State,
public transientFor: ClientWrapper | null,
private readonly rulesSignalManager: SignalManager | null,
) {
this.kwinClient = kwinClient;
this.transientFor = transientFor;
this.transients = [];
if (transientFor !== null) {
transientFor.addTransient(this);
}
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
}
public place(x: number, y: number, width: number, height: number) {
this.manipulatingGeometry.do(() => {
if (this.kwinClient.resize) {
// window is being manually resized, prevent fighting with the user
return;
}
this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement;
if (this.kwinClient.frameGeometry !== this.lastPlacement) {
// frameGeometry assignment failed. This sometimes happens on Wayland
// when a window is off-screen, effectively making it stuck there.
this.kwinClient.frameGeometry.x = x; // This makes it unstuck.
this.kwinClient.frameGeometry = this.lastPlacement;
}
});
}
private moveTransient(dx: number, dy: number, kwinDesktops: KwinDesktop[]) {
if (this.stateManager.getState() instanceof ClientState.Floating) {
if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx,
frame.y + dy,
frame.width,
frame.height,
);
}
for (const transient of this.transients) {
transient.moveTransient(dx, dy, kwinDesktops);
}
}
}
public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktops);
}
}
public focus() {
Workspace.activeWindow = this.kwinClient;
}
public isFocused() {
return Workspace.activeWindow === this.kwinClient;
}
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.kwinClient.setMaximize(vertically, horizontally);
});
}
public setFullScreen(fullScreen: boolean) {
if (!this.kwinClient.fullScreenable) {
return;
}
this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen;
});
}
public setShade(shade: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.shade = shade;
});
}
public isShaded() {
return this.kwinClient.shade;
}
public getMaximizedMode() {
return this.maximizedMode;
}
public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
return this.manipulatingGeometry.isDoing();
}
private addTransient(transient: ClientWrapper) {
this.transients.push(transient);
}
private removeTransient(transient: ClientWrapper) {
const i = this.transients.indexOf(transient);
this.transients.splice(i, 1);
}
public ensureTransientsVisible(screenSize: QmlRect) {
for (const transient of this.transients) {
if (transient.stateManager.getState() instanceof ClientState.Floating) {
transient.ensureVisible(screenSize);
transient.ensureTransientsVisible(screenSize);
}
}
}
public ensureVisible(screenSize: QmlRect) {
if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return;
}
const frame = this.kwinClient.frameGeometry;
if (frame.left < screenSize.left) {
frame.x = screenSize.left;
} else if (frame.right > screenSize.right) {
frame.x = screenSize.right - frame.width;
}
}
public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy();
}
if (this.transientFor !== null) {
this.transientFor.removeTransient(this);
}
for (const transient of this.transients) {
transient.transientFor = null;
}
}
private static initSignalManager(client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
if (maximizedMode !== MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
client.kwinClient.tile = null;
}
client.maximizedMode = maximizedMode;
});
return manager;
}
}

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

@@ -0,0 +1,61 @@
namespace Clients {
const prohibitedClasses = [
"ksmserver-logout-greeter",
"xwaylandvideobridge",
];
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable &&
kwinClient.resizeable &&
!kwinClient.popupWindow &&
!prohibitedClasses.includes(kwinClient.resourceClass);
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) &&
!kwinClient.minimized &&
kwinClient.desktops.length === 1 &&
kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktops.length !== 1) {
kwinClient.desktops = [Workspace.currentDesktop];
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [Workspace.currentActivity];
}
}
export function getKwinDesktopApprox(kwinClient: KwinClient) {
switch (kwinClient.desktops.length) {
case 0:
return Workspace.currentDesktop;
case 1:
return kwinClient.desktops[0];
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
return Workspace.currentDesktop;
} else {
return kwinClient.desktops[0];
}
}
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.clientGeometry.width === fullScreenArea.width &&
kwinClient.clientGeometry.height === fullScreenArea.height;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.includes(kwinDesktop);
}
export function isOnOneOfVirtualDesktops(kwinClient: KwinClient, kwinDesktops: KwinDesktop[]) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.some(d => kwinDesktops.includes(d));
}
}

View File

@@ -0,0 +1,142 @@
class DesktopManager {
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private selectedScreen: Output;
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(
private readonly pinManager: PinManager,
private readonly config: Desktop.Config,
public readonly layoutConfig: LayoutConfig,
currentActivity: string,
currentDesktop: KwinDesktop,
) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktops = new Map();
this.selectedScreen = Workspace.activeScreen;
this.kwinActivities = new Set(Workspace.activities);
this.kwinDesktops = new Set(Workspace.desktops);
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.selectedScreen,
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;
}
public selectScreen(screen: Output) {
this.selectedScreen = screen;
}
private removeActivity(activity: string) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
desktop.destroy();
this.desktops.delete(desktopKey);
}
}
public destroy() {
for (const desktop of this.desktops.values()) {
desktop.destroy();
}
}
public *getAllDesktops() {
for (const desktop of this.desktops.values()) {
yield desktop;
}
}
public getDesktopsForClient(kwinClient: KwinClient) {
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
return desktops;
}
// empty array means all
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
class PinManager {
private readonly pinnedClients: Set<KwinClient>;
constructor() {
this.pinnedClients = new Set();
}
public addClient(kwinClient: KwinClient) {
this.pinnedClients.add(kwinClient);
}
public removeClient(kwinClient: KwinClient) {
this.pinnedClients.delete(kwinClient);
}
public getAvailableSpace(kwinDesktop: KwinDesktop, screen: QmlRect) {
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
let lots = [baseLot];
for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, kwinDesktop) || client.minimized) {
continue;
}
const newLots: PinManager.Lot[] = [];
for (const lot of lots) {
lot.split(newLots, client.frameGeometry);
}
lots = newLots;
}
let largestLot = baseLot;
let largestArea = 0;
for (const lot of lots) {
const area = lot.area();
if (area > largestArea) {
largestArea = area;
largestLot = lot;
}
}
return largestLot;
}
}
namespace PinManager {
export class Lot {
private static readonly minWidth = 200;
private static readonly minHeight = 200;
constructor(
public readonly top: number,
public readonly bottom: number,
public readonly left: number,
public readonly right: number,
) {}
public split(destLots: Lot[], obstacle: QmlRect) {
if (!this.contains(obstacle)) {
// don't split
destLots.push(this);
return;
}
if (obstacle.top - this.top >= Lot.minHeight) {
destLots.push(new Lot(this.top, obstacle.top, this.left, this.right));
}
if (this.bottom - obstacle.bottom >= Lot.minHeight) {
destLots.push(new Lot(obstacle.bottom, this.bottom, this.left, this.right));
}
if (obstacle.left - this.left >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, this.left, obstacle.left));
}
if (this.right - obstacle.right >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, obstacle.right, this.right));
}
}
private contains(obstacle: QmlRect) {
return obstacle.right > this.left && obstacle.left < this.right &&
obstacle.bottom > this.top && obstacle.top < this.bottom;
}
public area() {
return (this.bottom - this.top) * (this.right - this.left);
}
}
}

137
src/lib/world/World.ts Normal file
View File

@@ -0,0 +1,137 @@
class World {
private readonly desktopManager: DesktopManager;
private readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer;
constructor(config: Config) {
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
let presetWidths = {
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
prev: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
getWidths: (minWidth: number, maxWidth: number): number[] => [],
};
try {
presetWidths = new PresetWidths(config.presetWidths, config.gapsInnerHorizontal);
} catch (error: any) {
notificationInvalidPresetWidths.sendEvent();
log("failed to parse presetWidths:", error);
}
this.shortcutActions = registerKeyBindings(this, {
manualScrollStep: config.manualScrollStep,
presetWidths: presetWidths,
columnResizer: config.scrollingCentered ? new RawResizer(presetWidths) : new ContextualResizer(presetWidths),
});
this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks are taken into account by `Workspace.clientArea`
for (const desktop of this.desktopManager.getAllDesktops()) {
desktop.onLayoutChanged();
}
this.update();
});
this.pinManager = new PinManager();
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
untileOnDrag: config.untileOnDrag,
};
this.desktopManager = new DesktopManager(
this.pinManager,
{
marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight,
scroller: World.createScroller(config),
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
},
layoutConfig,
Workspace.currentActivity,
Workspace.currentDesktop,
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
this.update();
}
private static createScroller(config: Config) {
if (config.scrollingLazy) {
return new LazyScroller();
} else if (config.scrollingCentered) {
return new CenteredScroller();
} else if (config.scrollingGrouped) {
return new GroupedScroller();
} else {
log("No scrolling mode selected, using default");
return new LazyScroller();
}
}
private addExistingClients() {
const kwinClients = Workspace.windows;
for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i];
this.clientManager.addClient(kwinClient);
}
}
private update() {
this.desktopManager.getCurrentDesktop().arrange();
}
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
f(this.clientManager, this.desktopManager);
this.update();
}
public doIfTiled(
kwinClient: KwinClient,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
const window = this.clientManager.findTiledWindow(kwinClient);
if (window === null) {
return;
}
const column = window.column;
const grid = column.grid;
f(this.clientManager, this.desktopManager, window, column, grid);
this.update();
}
public doIfTiledFocused(
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
if (Workspace.activeWindow === null) {
return;
}
this.doIfTiled(Workspace.activeWindow, f);
}
public destroy() {
this.workspaceSignalManager.destroy();
for (const shortcutAction of this.shortcutActions) {
shortcutAction.destroy();
}
this.clientManager.destroy();
this.desktopManager.destroy();
}
public onScreenResized() {
this.screenResizedDelayer.run();
}
}

View File

@@ -0,0 +1,25 @@
namespace ClientState {
export class Docked implements State {
private readonly world: World;
private readonly signalManager: SignalManager;
constructor(world: World, kwinClient: KwinClient) {
this.world = world;
this.signalManager = Docked.initSignalManager(world, kwinClient);
world.onScreenResized();
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
this.world.onScreenResized();
}
private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, () => {
world.onScreenResized();
});
return manager;
}
}
}

View File

@@ -0,0 +1,64 @@
namespace ClientState {
export class Floating implements State {
private readonly client: ClientWrapper;
private readonly config: ClientManager.Config;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
this.client = client;
this.config = config;
if (config.floatingKeepAbove) {
client.kwinClient.keepAbove = true;
}
if (limitHeight && client.kwinClient.tile === null) {
Floating.limitHeight(client);
}
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
}
// TODO: move to `Tiled.restoreClientAfterTiling`
private static limitHeight(client: ClientWrapper) {
const placementArea = Workspace.clientArea(
ClientAreaOption.PlacementArea,
client.kwinClient.output,
Clients.getKwinDesktopApprox(client.kwinClient),
);
const clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth;
client.place(
clientRect.x,
clientRect.y,
width,
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
);
}
private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager();
manager.connect(kwinClient.tileChanged, () => {
// on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
});
return manager;
}
}
}

View File

@@ -0,0 +1,26 @@
namespace ClientState {
export class Manager {
private state: State;
constructor(initialState: State) {
this.state = initialState;
}
public setState(constructNewState: () => State, passFocus: boolean) {
this.state.destroy(passFocus);
this.state = constructNewState();
}
public getState() {
return this.state;
}
public destroy(passFocus: boolean) {
this.state.destroy(passFocus);
}
}
export type State = {
destroy(passFocus: boolean): void;
};
}

View File

@@ -0,0 +1,91 @@
namespace ClientState {
export class Pinned implements State {
private readonly kwinClient: KwinClient;
private readonly pinManager: PinManager;
private readonly desktopManager: DesktopManager;
private readonly config: ClientManager.Config;
private readonly signalManager: SignalManager;
constructor(world: World, pinManager: PinManager, desktopManager: DesktopManager, kwinClient: KwinClient, config: ClientManager.Config) {
this.kwinClient = kwinClient;
this.pinManager = pinManager;
this.desktopManager = desktopManager;
this.config = config;
if (config.floatingKeepAbove) {
kwinClient.keepAbove = true;
}
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
this.pinManager.removeClient(this.kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
desktop.onPinsChanged();
}
}
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
const manager = new SignalManager();
let oldActivities = kwinClient.activities;
let oldDesktops = kwinClient.desktops;
manager.connect(kwinClient.tileChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
return;
}
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
});
});
manager.connect(kwinClient.minimizedChanged, () => {
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
});
});
manager.connect(kwinClient.desktopsChanged, () => {
const changedDesktops = oldDesktops.length === 0 || kwinClient.desktops.length === 0 ?
[] :
union(oldDesktops, kwinClient.desktops);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
desktop.onPinsChanged();
}
});
oldDesktops = kwinClient.desktops;
});
manager.connect(kwinClient.activitiesChanged, () => {
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
[] :
union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
desktop.onPinsChanged();
}
});
oldActivities = kwinClient.activities;
});
return manager;
}
}
}

View File

@@ -0,0 +1,239 @@
namespace ClientState {
export class Tiled implements State {
public readonly window: Window;
private readonly defaultState: Tiled.WindowState;
private readonly signalManager: SignalManager;
private static readonly maxExternalFrameGeometryChangedIntervalMs = 1000;
constructor(world: World, client: ClientWrapper, grid: Grid) {
this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
Tiled.prepareClientForTiling(client, grid.config);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column);
this.window = window;
this.signalManager = Tiled.initSignalManager(world, window, grid.config);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
const window = this.window;
const grid = window.column.grid;
const client = window.client;
window.destroy(passFocus);
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window, config: LayoutConfig) {
const client = window.client;
const kwinClient = client.kwinClient;
const manager = new SignalManager();
manager.connect(kwinClient.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple desktops are not supported
clientManager.floatClient(client);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
});
});
manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported
clientManager.floatClient(client);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
});
})
manager.connect(kwinClient.minimizedChanged, () => {
console.assert(kwinClient.minimized);
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
world.do(() => {
window.onMaximizedChanged(maximizedMode);
});
});
let resizing = false;
let resizeStartWidth = 0;
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
if (kwinClient.move) {
if (config.untileOnDrag) {
world.do((clientManager, desktopManager) => {
clientManager.floatClient(client);
});
}
return;
}
if (kwinClient.resize) {
resizing = true;
resizeStartWidth = window.column.getWidth();
if (config.resizeNeighborColumn) {
const resizeNeighborColumn = Tiled.getResizeNeighborColumn(window);
if (resizeNeighborColumn !== null) {
resizeNeighbor = {
column: resizeNeighborColumn,
startWidth: resizeNeighborColumn.getWidth(),
};
}
}
window.column.grid.onUserResizeStarted();
}
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (resizing) {
resizing = false;
resizeNeighbor = undefined;
window.column.grid.onUserResizeFinished();
}
});
let externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
return;
}
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
const newCenterY = newGeometry.y + newGeometry.height/2;
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
if (dx !== 0 || dy !== 0) {
// TODO: instead of passing dx and dy, remember relative (to the parent) x and y for each
// transient window and use them for `moveTransients` and `ensureTransientsVisible`
client.moveTransients(dx, dy);
}
if (kwinClient.resize) {
world.do(() => {
if (newGeometry.width !== oldGeometry.width) {
window.column.onUserResizeWidth(
resizeStartWidth,
newGeometry.width - resizeStartWidth,
newGeometry.left !== oldGeometry.left,
resizeNeighbor,
);
}
if (newGeometry.height !== oldGeometry.height) {
window.column.adjustWindowHeight(
window,
newGeometry.height - oldGeometry.height,
newGeometry.y !== oldGeometry.y,
);
}
});
} else if (
!window.column.grid.isUserResizing() &&
!client.isManipulatingGeometry(newGeometry) &&
client.getMaximizedMode() === MaximizedMode.Unmaximized &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
if (externalFrameGeometryChangedRateLimiter.acquire()) {
world.do(() => window.onFrameGeometryChanged());
}
}
});
manager.connect(kwinClient.fullScreenChanged, () => {
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
});
manager.connect(kwinClient.tileChanged, () => {
// on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
});
return manager;
}
private static getResizeNeighborColumn(window: Window) {
const kwinClient = window.client.kwinClient;
const column = window.column;
if (Workspace.cursorPos.x > kwinClient.clientGeometry.right) {
return column.grid.getRightColumn(column);
} else if (Workspace.cursorPos.x < kwinClient.clientGeometry.left) {
return column.grid.getLeftColumn(column);
} else {
return null;
}
}
private static moveWindowToGrid(window: Window, grid: Grid) {
if (grid === window.column.grid) {
// window already on the given grid
return;
}
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn, true);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = true;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.kwinClient.keepAbove = false;
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
}
client.setMaximize(false, false);
}
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, defaultState: Tiled.WindowState, screenSize: QmlRect) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = defaultState.skipSwitcher;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = false;
}
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);
}
client.ensureVisible(screenSize);
}
}
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean;
};
}
}

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);
return world;
}

View File

@@ -1,4 +1,8 @@
function loadConfig() {
function init() {
return new World(loadConfig());
}
function loadConfig(): Config {
const config: any = {};
for (const entry of configDef) {
config[entry.name] = KWin.readConfig(entry.name, entry.default);

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

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

View File

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

View File

@@ -0,0 +1,23 @@
tests.register("Center focused", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client0, client1, client2] = workspaceMock.createClientsWithWidths(300, 152, 300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client0));
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
Assert.assert(workspaceMock.activeWindow === client2);
Assert.columnsFillTilingArea([client0, client1, client2]);
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, screen, client2);
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
qtMock.fireShortcut("karousel-focus-left");
Assert.centered(config, screen, client2, { message: "No scrolling should have occured" });
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
});

View File

@@ -0,0 +1,124 @@
tests.register("columns squeeze side", 1, () => {
const baseTestCases = [
{ widths: [500, 500], blocked: [false, false], possible: true },
{ widths: [500, 768], blocked: [false, false], possible: true },
{ widths: [500, 500], blocked: [false, true], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, false], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, true], possible: true },
{ widths: [500, 200, 200], blocked: [true, false, true], possible: true },
{ widths: [500, 500, 500], blocked: [false, true, true], possible: false },
];
const testCasesLeft = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
name: "left " + i,
action: "karousel-columns-squeeze-left",
focus: baseTestCase.widths.length-1,
}));
const testCasesRight = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
widths: baseTestCase.widths.slice().reverse(),
blocked: baseTestCase.blocked.slice().reverse(),
name: "right " + i,
action: "karousel-columns-squeeze-right",
focus: 0,
}));
const testCases = [...testCasesLeft, ...testCasesRight];
for (const testCase of testCases) {
const assertOpt = { message: `Case: ${testCase.name}` };
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const clients = workspaceMock.createClientsWithWidths(...testCase.widths);
workspaceMock.activeWindow = clients[testCase.focus];
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
clients[i].minSize = new MockQmlSize(testCase.widths[i], 100);
}
}
if (testCase.possible) {
qtMock.fireShortcut(testCase.action);
Assert.columnsFillTilingArea(clients, assertOpt);
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
Assert.equal(clients[i].frameGeometry.width, testCase.widths[i], assertOpt);
}
}
}
const frames = clients.map(client => client.frameGeometry);
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry);
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], assertOpt);
}
}
});
tests.register("columns squeeze side (just scroll)", 1, () => {
const baseTestCases = [
{ focus: 0, startVisible: [true, true, false], endVisible: [true, true, false] },
{ focus: 1, startVisible: [false, true, true], endVisible: [true, true, false] },
{ focus: 2, startVisible: [false, true, true], endVisible: [false, true, true] },
];
const testCasesLeft = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
name: "left " + i,
action: "karousel-columns-squeeze-left",
scrollStart: false,
}));
const testCasesRight = baseTestCases.map((baseTestCase, i) => ({
focus: 2 - baseTestCase.focus,
startVisible: baseTestCase.startVisible.slice().reverse(),
endVisible: baseTestCase.endVisible.slice().reverse(),
name: "right " + i,
action: "karousel-columns-squeeze-right",
scrollStart: true,
}));
const testCases = [...testCasesLeft, ...testCasesRight];
for (const testCase of testCases) {
const assertMsg = `Case: ${testCase.name}`;
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
function assertVisible(clients: KwinClient[], visible: boolean[]) {
for (let i = 0; i < clients.length; i++) {
if (visible[i]) {
Assert.fullyVisible(clients[i].frameGeometry, { message: assertMsg, skip: 1 });
} else {
Assert.notFullyVisible(clients[i].frameGeometry, { message: assertMsg, skip: 1 });
}
}
}
const clients = workspaceMock.createClientsWithWidths(300, 300, 300);
for (const client of clients) {
client.minSize = new MockQmlSize(300, 100);
}
if (testCase.scrollStart) {
qtMock.fireShortcut("karousel-grid-scroll-start");
}
workspaceMock.activeWindow = clients[testCase.focus];
assertVisible(clients, testCase.startVisible);
qtMock.fireShortcut(testCase.action);
assertVisible(clients, testCase.endVisible);
const frames = clients.map(client => client.frameGeometry);
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry);
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], { message: assertMsg });
}
}
});

View File

@@ -0,0 +1,44 @@
tests.register("External resize", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
function getClientDesiredFrame(width: number) {
return new MockQmlRect(10, 10, width, 200);
}
function getTiledFrame(width: number) {
return new MockQmlRect(
Math.round((screen.width - width) / 2),
tilingArea.top,
width,
tilingArea.height,
);
}
const [client] = workspaceMock.createClientsWithFrames(getClientDesiredFrame(100));
Assert.equalRects(client.frameGeometry, getTiledFrame(100), { message: "We should tile the window, respecting its desired width" });
function testExternalResizing() {
client.frameGeometry = getClientDesiredFrame(110);
Assert.equalRects(client.frameGeometry, getTiledFrame(110), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(120);
Assert.equalRects(client.frameGeometry, getTiledFrame(120), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(130);
Assert.equalRects(client.frameGeometry, getTiledFrame(130), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(140);
Assert.equalRects(client.frameGeometry, getTiledFrame(140), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(200);
Assert.equalRects(client.frameGeometry, getClientDesiredFrame(200), { message: "We should give up and let the client have its desired frame" });
}
timeControl(addTime => {
testExternalResizing();
addTime(1000);
// the concession has expired, let's test again
testExternalResizing();
});
});

191
src/tests/flows/layering.ts Normal file
View File

@@ -0,0 +1,191 @@
tests.register("tiledKeepBelow", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = true;
config.floatingKeepAbove = false;
const { qtMock, workspaceMock, world } = init(config);
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const [client] = workspaceMock.createClients(1);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
});
tests.register("floatingKeepAbove", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = false;
config.floatingKeepAbove = true;
const { qtMock, workspaceMock, world } = init(config);
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const [client] = workspaceMock.createClients(1);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
});
tests.register("No layering", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = false;
config.floatingKeepAbove = false;
// In this mode, Karousel shouldn't change keepBelow or keepAbove.
// Except when tiling a window, keepAbove should still be cleared.
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const testCases = [
{ keepBelow: false, keepAbove: false },
{ keepBelow: false, keepAbove: true },
{ keepBelow: true, keepAbove: false },
{ keepBelow: true, keepAbove: true },
];
for (const testCase of testCases) {
const assertOptions = { message: JSON.stringify(testCase) };
const { qtMock, workspaceMock, world } = init(config);
const [client] = workspaceMock.createClients(1);
client.keepBelow = testCase.keepBelow;
client.keepAbove = testCase.keepAbove;
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.assert(!client.keepAbove, assertOptions);
client.keepAbove = testCase.keepAbove;
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.assert(!client.keepAbove, assertOptions);
}
});

98
src/tests/flows/layout.ts Normal file
View File

@@ -0,0 +1,98 @@
tests.register("Focus and move windows", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2, client3] = workspaceMock.createClients(3);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
Assert.assert(clientManager.hasClient(client3));
});
Assert.assert(workspaceMock.activeWindow === client3);
function testLayout(shortcutName: string, grid: KwinClient[][]) {
qtMock.fireShortcut(shortcutName);
Assert.grid(config, screen, 100, grid, true, { skip: 1 });
}
function testFocus(shortcutName: string, expectedFocus: KwinClient) {
qtMock.fireShortcut(shortcutName);
Assert.assert(workspaceMock.activeWindow === expectedFocus, {
message: `wrong activeWindow: ${workspaceMock.activeWindow?.pid}`,
skip: 1,
});
};
testLayout("karousel-column-move-right", [ [client1], [client2], [client3] ]);
testLayout("karousel-window-move-left", [ [client1], [client2,client3] ]);
testLayout("karousel-window-move-left", [ [client1], [client3], [client2] ]);
testLayout("karousel-window-move-left", [ [client1,client3], [client2] ]);
testFocus("karousel-focus-right", client2);
testLayout("karousel-window-move-left", [ [client1,client3,client2] ]);
testLayout("karousel-window-move-left", [ [client2], [client1,client3] ]);
testLayout("karousel-window-move-left", [ [client2], [client1,client3] ]);
testFocus("karousel-focus-2", client3);
testFocus("karousel-focus-up", client1);
testLayout("karousel-column-move-left", [ [client1,client3], [client2] ]);
testLayout("karousel-window-move-right", [ [client3], [client1], [client2] ]);
testFocus("karousel-focus-3", client2);
testLayout("karousel-window-move-start", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-to-column-3", [ [client3], [client1,client2] ]);
testLayout("karousel-column-move-left", [ [client1,client2], [client3] ]);
testLayout("karousel-column-move-end", [ [client3], [client1,client2] ]);
testLayout("karousel-column-move-to-column-1", [ [client1,client2], [client3] ]);
testLayout("karousel-column-move-right", [ [client3], [client1,client2] ]);
testLayout("karousel-window-move-previous", [ [client3], [client2,client1] ]);
testLayout("karousel-window-move-previous", [ [client3], [client2], [client1] ]);
testLayout("karousel-window-move-previous", [ [client3,client2], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2,client3], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-next", [ [client2,client3], [client1] ]);
testLayout("karousel-window-move-next", [ [client3,client2], [client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client2], [client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client2,client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client1,client2] ]);
testLayout("karousel-window-move-next", [ [client3], [client1], [client2] ]);
testLayout("karousel-window-move-next", [ [client3], [client1], [client2] ]);
testLayout("karousel-window-move-left", [ [client3], [client1,client2] ]);
const col1Win1 = client3;
const col2Win1 = client1;
const col2Win2 = client2;
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-down", col2Win2);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-right", col2Win2);
testFocus("karousel-focus-right", col2Win2);
testFocus("karousel-focus-2", col2Win2);
testFocus("karousel-focus-1", col1Win1);
testFocus("karousel-focus-2", col2Win2);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-end", col2Win2);
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-right", col2Win1);
testFocus("karousel-focus-2", col2Win1);
testFocus("karousel-focus-1", col1Win1);
testFocus("karousel-focus-2", col2Win1);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-end", col2Win1);
testFocus("karousel-focus-down", col2Win2);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-next", col2Win1);
testFocus("karousel-focus-next", col2Win2);
testFocus("karousel-focus-next", col2Win2);
testFocus("karousel-focus-previous", col2Win1);
testFocus("karousel-focus-previous", col1Win1);
testFocus("karousel-focus-previous", col1Win1);
});

View File

@@ -0,0 +1,47 @@
tests.register("LazyScroller", 20, () => {
const config = getDefaultConfig();
config.scrollingLazy = true;
config.scrollingCentered = false;
config.scrollingGrouped = false;
const { qtMock, workspaceMock, world } = init(config);
const [client1] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1]], true);
const [client2] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2]], true);
const [client3] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-left"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
workspaceMock.activeWindow = client1;
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, screen, 300, [[client1]], true);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
});

View File

@@ -0,0 +1,145 @@
tests.register("Maximization", 100, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(kwinClient));
});
const columnLeftX = screen.width/2 - 300/2;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
kwinClient.fullScreen = true;
Assert.equalRects(kwinClient.frameGeometry, screen);
kwinClient.fullScreen = false;
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
kwinClient.setMaximize(true, true);
Assert.equalRects(kwinClient.frameGeometry, screen);
kwinClient.setMaximize(true, false);
Assert.rect(kwinClient.frameGeometry, columnLeftX, 0, 300, screen.height);
kwinClient.setMaximize(false, false);
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
});
tests.register("Maximize with transient", 100, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const parent = new MockKwinClient(new MockQmlRect(10, 20, 300, 200));
const child = new MockKwinClient(new MockQmlRect(14, 24, 50, 50), parent);
workspaceMock.createWindows(parent);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(parent));
});
runOneOf(
() => parent.fullScreen = true,
() => parent.setMaximize(true, true),
);
Assert.equalRects(parent.frameGeometry, screen);
workspaceMock.createWindows(child);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(child));
});
Assert.rect(child.frameGeometry, 14, 24, 50, 50);
Assert.equalRects(parent.frameGeometry, screen);
});
tests.register("Re-maximize disabled", 100, () => {
const config = getDefaultConfig();
config.reMaximize = false;
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => client2.fullScreen = true,
() => client2.setMaximize(true, true),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
() => qtMock.fireShortcut("karousel-focus-end"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
});
tests.register("Re-maximize enabled", 100, () => {
const config = getDefaultConfig();
config.reMaximize = true;
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => client2.fullScreen = true,
() => client2.setMaximize(true, true),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
() => qtMock.fireShortcut("karousel-focus-end"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
});

View File

@@ -0,0 +1,40 @@
tests.register("Pass focus", 20, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client0, client1a, client1b, client1c, client4, client5, client6] = workspaceMock.createClients(7);
workspaceMock.activeWindow = client1b;
qtMock.fireShortcut("karousel-window-move-left");
workspaceMock.activeWindow = client1c;
qtMock.fireShortcut("karousel-window-move-left");
workspaceMock.activeWindow = client1b;
workspaceMock.activeWindow = client5;
function removeWindow(client: MockKwinClient) {
runOneOf(
() => workspaceMock.removeWindow(client),
() => client.desktops = [workspaceMock.desktops[1]],
);
}
removeWindow(client5);
Assert.equal(workspaceMock.activeWindow, client4);
qtMock.fireShortcut("karousel-column-move-to-desktop-2");
Assert.equal(workspaceMock.activeWindow, client1b);
removeWindow(client1b);
Assert.equal(workspaceMock.activeWindow, client1a);
removeWindow(client1a);
Assert.equal(workspaceMock.activeWindow, client1c);
removeWindow(client1c);
Assert.equal(workspaceMock.activeWindow, client0);
removeWindow(client0);
Assert.equal(workspaceMock.activeWindow, client6);
removeWindow(client6);
Assert.equal(workspaceMock.activeWindow, null);
});

View File

@@ -0,0 +1,39 @@
tests.register("Pin", 20, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const screenHalfLeft = new MockQmlRect(0, 0, screen.width/2, screen.height);
const screenHalfRight = new MockQmlRect(screen.width/2, 0, screen.width/2, screen.height);
const [pinned, tiled1, tiled2] = workspaceMock.createClients(3);
Assert.grid(config, screen, 100, [ [pinned], [tiled1], [tiled2] ], true);
pinned.pin(screenHalfLeft);
Assert.equalRects(pinned.frameGeometry, screenHalfLeft);
Assert.grid(config, screenHalfRight, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.unpin();
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screen, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = true;
Assert.grid(config, screen, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = false;
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 100, [ [tiled1], [tiled2] ], true);
workspaceMock.activeWindow = pinned;
qtMock.fireShortcut("karousel-window-toggle-floating");
Assert.assert(pinned.tile === null);
pinned.frameGeometry = new MockQmlRect(10, 20, 100, 200); // This is needed because the window's preferredWidth can change when pinning, because frameGeometryChanged can fire before tileChanged. TODO: Ensure pinned window keeps its preferredWidth.
Assert.grid(config, screen, 100, [ [tiled1], [tiled2], [pinned] ], true);
});

View File

@@ -0,0 +1,134 @@
tests.register("Preset Widths default", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
function getRect(columnWidth: number) {
return new MockQmlRect(
(screen.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
Assert.equalRects(kwinClient.frameGeometry, getRect(300));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
});
tests.register("Preset Widths custom", 1, () => {
const config = getDefaultConfig();
config.presetWidths = "500px, 250px, 100px, 50%";
const { qtMock, workspaceMock, world } = init(config);
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
function getRect(columnWidth: number) {
return new MockQmlRect(
(screen.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(200);
Assert.equalRects(kwinClient.frameGeometry, getRect(200));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
});
tests.register("Preset Widths fill screen uniform", 1, () => {
for (let nColumns = 1; nColumns < 10; nColumns++) {
const config = getDefaultConfig();
config.presetWidths = String(1 / nColumns);
const { qtMock, workspaceMock, world } = init(config);
let firstClient, lastClient;
for (let i = 0; i < nColumns; i++) {
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
if (i === 0) {
firstClient = kwinClient;
}
if (i === nColumns-1) {
lastClient = kwinClient;
}
qtMock.fireShortcut("karousel-cycle-preset-widths");
}
const left = tilingArea.left;
const right = tilingArea.right;
const maxLeftoverPx = nColumns - 1;
const eps = Math.ceil(maxLeftoverPx / 2);
Assert.between(firstClient!.frameGeometry.left, left, left+eps, { message: `nColumns: ${nColumns}` });
Assert.between(lastClient!.frameGeometry.right, right-eps, right, { message: `nColumns: ${nColumns}` });
}
});
tests.register("Preset Widths fill screen non-uniform", 1, () => {
const config = getDefaultConfig();
config.presetWidths = String("50%, 25%");
const { qtMock, workspaceMock, world } = init(config);
const [clientThin1] = workspaceMock.createClientsWithWidths(100);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const [clientThin2] = workspaceMock.createClientsWithWidths(100);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const [clientWide] = workspaceMock.createClientsWithWidths(300);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
const quarterWidth = halfWidth/2 - config.gapsInnerHorizontal/2;
const height = tilingArea.height;
const left1 = tilingArea.left;
const left2 = left1 + config.gapsInnerHorizontal + quarterWidth;
const left3 = left2 + config.gapsInnerHorizontal + quarterWidth;
Assert.rect(clientThin1.frameGeometry, left1, tilingArea.top, quarterWidth, height);
Assert.rect(clientThin2.frameGeometry, left2, tilingArea.top, quarterWidth, height);
Assert.rect(clientWide.frameGeometry, left3, tilingArea.top, halfWidth, height);
Assert.equal(clientWide.frameGeometry.right, tilingArea.right);
});

Some files were not shown because too many files have changed in this diff Show More