253 Commits

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
122 changed files with 4089 additions and 1257 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.

1
.gitignore vendored
View File

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

View File

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

View File

@@ -1,17 +1,11 @@
# 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.
Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally.
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.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
@@ -28,6 +22,13 @@ Karousel requires the following QML modules:
- Doesn't support windows on all desktops
- Doesn't support windows on multiple activities
## Installation
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
Or clone the repo and run `make install` (requires 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:
@@ -38,12 +39,16 @@ Here's the default ones:
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| (unassigned) | Move focus to the next window in grid |
| (unassigned) | Move focus to the previous window in grid |
| Meta+Home | Move focus to start |
| Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
| Meta+Shift+W | Move window up |
| Meta+Shift+S | Move window down |
| (unassigned) | Move window to the next position in grid |
| (unassigned) | Move window to the previous position in grid |
| Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
@@ -53,7 +58,10 @@ Here's the default ones:
| 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 |
@@ -61,6 +69,7 @@ Here's the default ones:
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end |
| Meta+Ctrl+Return | Move Karousel grid to the current screen |
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |

View File

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

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,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

@@ -301,22 +301,19 @@
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_manualResizeStep">
<widget class="QLabel" name="label_presetWidths">
<property name="text">
<string>Manual resize step size:</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="QSpinBox" name="kcfg_manualResizeStep">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
<widget class="QLineEdit" name="kcfg_presetWidths">
<property name="toolTip">
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
</property>
</widget>
</item>

View File

@@ -25,4 +25,14 @@ Item {
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

@@ -9,7 +9,7 @@
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"Version": "0.9.1",
"Version": "0.11",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"

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,346 +0,0 @@
namespace Actions {
export function getAction(world: World, config: Config, name: string) {
switch (name) {
case "focus-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
};
case "focus-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
};
case "focus-up": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
};
case "focus-down": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
};
case "focus-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
};
case "focus-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
};
case "window-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
window.moveToColumn(prevColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
});
};
case "window-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
window.moveToColumn(nextColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
});
};
case "window-move-up": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
};
case "window-move-down": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
};
case "window-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
};
case "window-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
};
case "window-toggle-floating": return () => {
const kwinClient = Workspace.activeWindow;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
};
case "column-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
};
case "column-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
};
case "column-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(null);
});
};
case "column-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
});
};
case "column-toggle-stacked": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked();
});
};
case "column-width-increase": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.increaseWidth(column, config.manualResizeStep);
});
};
case "column-width-decrease": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.decreaseWidth(column, config.manualResizeStep);
});
};
case "columns-width-equalize": return () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
};
case "grid-scroll-left": return () => {
gridScroll(world, -config.manualScrollStep);
};
case "grid-scroll-right": return () => {
gridScroll(world, config.manualScrollStep);
};
case "grid-scroll-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
});
};
case "grid-scroll-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
});
};
case "grid-scroll-focused": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
};
case "grid-scroll-left-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.desktop.scrollToColumn(prevColumn);
});
};
case "grid-scroll-right-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.desktop.scrollToColumn(nextColumn);
});
};
default: throw new Error("unknown action: " + name);
}
}
export function getNumAction(world: World, name: string) {
switch (name) {
case "focus-": return (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.focus();
});
};
case "window-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll();
});
};
case "column-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
});
};
case "column-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
};
case "tail-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
};
default: throw new Error("unknown num action: " + name);
}
}
function gridScroll(world: World, amount: number) {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
grid.desktop.adjustScroll(amount, false);
});
}
export type Config = {
manualScrollStep: number,
manualResizeStep: number,
columnResizer: ColumnResizer,
};
export type ColumnResizer = {
increaseWidth(column: Column, step: number): void,
decreaseWidth(column: Column, step: number): void,
}
}

View File

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

View File

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

View File

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

45
src/extern/qt.d.ts vendored
View File

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

View File

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

View File

@@ -1,20 +1,11 @@
type KeyBinding = {
name: string;
type DocsKeyBinding = {
description: string;
comment?: string;
defaultKeySequence: string;
}
keySequence: string;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
}
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)[]) {
@@ -63,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,185 +0,0 @@
const keyBindings: KeyBinding[] = [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
},
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "One window in the column visible, others shaded; not supported on Wayland",
defaultKeySequence: "Meta+X",
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
},
];
const numKeyBindings: NumKeyBinding[] = [
{
name: "focus-",
description: "Move focus to column ",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
},
{
name: "window-move-to-column-",
description: "Move window to column ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
defaultModifiers: "Meta+Shift",
fKeys: false,
},
{
name: "column-move-to-column-",
description: "Move column to position ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: false,
},
{
name: "column-move-to-desktop-",
description: "Move column to desktop ",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
},
{
name: "tail-move-to-desktop-",
description: "Move this and all following columns to desktop ",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
},
];

View File

@@ -1,11 +0,0 @@
type LayoutConfig = {
gapsInnerHorizontal: number,
gapsInnerVertical: number,
offScreenOpacity: number,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
tiledKeepBelow: boolean,
maximizedKeepAbove: 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

@@ -1,9 +1,15 @@
class ContextualResizer {
public increaseWidth(column: Column, step: number) {
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();
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
return;
}
@@ -17,13 +23,12 @@ class ContextualResizer {
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
const newWidth = ContextualResizer.findNextStep(
const newWidth = findMinPositive(
[
visibleRange.getWidth(),
column.getWidth() + step,
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => width - column.getWidth(),
)
@@ -35,11 +40,13 @@ class ContextualResizer {
desktop.scrollCenterVisible(column);
}
public decreaseWidth(column: Column, step: number) {
public decreaseWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
return;
}
@@ -50,11 +57,11 @@ class ContextualResizer {
return;
}
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
let leftOffScreenColumn = grid.getLeftColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
let rightOffScreenColumn = grid.getRightColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
@@ -64,12 +71,11 @@ class ContextualResizer {
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = ContextualResizer.findNextStep(
const newWidth = findMinPositive(
[
visibleRange.getWidth(),
column.getWidth() - step,
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => column.getWidth() - width,
)
@@ -80,17 +86,4 @@ class ContextualResizer {
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
let bestScore = Infinity;
let bestStep = undefined;
for (const step of steps) {
const score = evaluate(step);
if (score > 0 && score < bestScore) {
bestScore = score;
bestStep = step;
}
}
return bestStep;
}
}

View File

@@ -0,0 +1,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);
}
}

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

@@ -1,8 +1,4 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false
@@ -31,28 +27,23 @@ const defaultWindowRules = `[
"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-idea",
"class": "jetbrains-.*",
"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",
"class": "jetbrains-.*",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
}
@@ -62,32 +53,32 @@ const configDef = [
{
name: "gapsOuterTop",
type: "UInt",
default: 18,
default: 16,
},
{
name: "gapsOuterBottom",
type: "UInt",
default: 18,
default: 16,
},
{
name: "gapsOuterLeft",
type: "UInt",
default: 18,
default: 16,
},
{
name: "gapsOuterRight",
type: "UInt",
default: 18,
default: 16,
},
{
name: "gapsInnerHorizontal",
type: "UInt",
default: 18,
default: 8,
},
{
name: "gapsInnerVertical",
type: "UInt",
default: 18,
default: 8,
},
{
name: "manualScrollStep",
@@ -95,9 +86,9 @@ const configDef = [
default: 200,
},
{
name: "manualResizeStep",
type: "UInt",
default: 600,
name: "presetWidths",
type: "String",
default: "50%, 100%",
},
{
name: "offScreenOpacity",

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

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

View File

@@ -1,8 +1,12 @@
declare const KWin: {
type KWin = {
__brand: "KWin";
readConfig(key: string, defaultValue: any): any;
};
declare const Workspace: {
type Workspace = {
__brand: "Workspace";
readonly activities: string[];
readonly desktops: KwinDesktop[];
readonly currentDesktop: KwinDesktop;
@@ -11,18 +15,19 @@ declare const Workspace: {
readonly windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
activeWindow: KwinClient;
activeWindow: KwinClient|null;
readonly currentDesktopChanged: QSignal<[]>
readonly currentDesktopChanged: QSignal<[]>;
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient]>;
readonly desktopsChanged: QSignal<[]>;
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);
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
};
const enum ClientAreaOption {
@@ -43,15 +48,17 @@ const enum MaximizedMode {
Maximized,
}
type Tile = unknown;
type Output = unknown;
type Tile = { __brand: "Tile" };
type Output = { __brand: "Output" };
type KwinClient = {
__brand: "KwinClient";
interface KwinClient {
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly transientFor: KwinClient | null;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
@@ -65,6 +72,7 @@ interface KwinClient {
readonly normalWindow: boolean;
readonly managed: boolean;
readonly popupWindow: boolean;
readonly pid: number;
fullScreen: boolean;
activities: string[]; // empty array means all activities
@@ -75,14 +83,14 @@ interface KwinClient {
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile;
tile: Tile|null;
opacity: number;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>;
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
@@ -90,13 +98,15 @@ interface KwinClient {
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
}
};
type KwinDesktop = {
__brand: "KwinDesktop";
interface KwinDesktop {
readonly id: string;
}
};
type ShortcutHandler = {
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

@@ -2,7 +2,8 @@ type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
defaultKeySequence?: string;
action: () => void;
};
type NumKeyBinding = {
@@ -11,6 +12,7 @@ type NumKeyBinding = {
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: (i: number) => void;
};
function catchWrap(f: () => void) {
@@ -24,14 +26,14 @@ function catchWrap(f: () => void) {
};
}
function registerKeyBinding(world: World, config: Actions.Config, shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
function registerKeyBinding(shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(Actions.getAction(world, config, keyBinding.name)),
catchWrap(keyBinding.action),
));
}
function registerNumKeyBindings(world: World, shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
@@ -39,28 +41,27 @@ function registerNumKeyBindings(world: World, shortcutActions: ShortcutAction[],
const keySequence = i < n ?
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
"";
const action = Actions.getNumAction(world, numKeyBinding.name);
shortcutActions.push(new ShortcutAction(
{
name: numKeyBinding.name + numKey,
description: numKeyBinding.description + numKey,
name: applyMacro(numKeyBinding.name, numKey),
description: applyMacro(numKeyBinding.description, numKey),
defaultKeySequence: keySequence,
},
catchWrap(() => action(i)),
catchWrap(() => numKeyBinding.action(i)),
));
}
}
// TODO: refactor
function registerKeyBindings(world: World, config: Actions.Config) {
const actions = new Actions(config);
const shortcutActions: ShortcutAction[] = [];
for (const keyBinding of keyBindings) {
registerKeyBinding(world, config, shortcutActions, keyBinding);
for (const keyBinding of getKeyBindings(world, actions)) {
registerKeyBinding(shortcutActions, keyBinding);
}
for (const numKeyBinding of numKeyBindings) {
registerNumKeyBindings(world, shortcutActions, numKeyBinding);
for (const numKeyBinding of getNumKeyBindings(world, actions)) {
registerNumKeyBindings(shortcutActions, numKeyBinding);
}
return shortcutActions;

View File

@@ -5,46 +5,39 @@ class Column {
private readonly windows: LinkedList<Window>;
private stacked: boolean;
private focusTaker: Window|null;
private static readonly minWidth = 10;
private static readonly minWidth = 40;
constructor(grid: Grid, prevColumn: Column|null) {
constructor(grid: Grid, leftColumn: Column|null) {
this.gridX = 0;
this.width = 0;
this.windows = new LinkedList();
this.stacked = grid.config.stackColumnsByDefault;
this.focusTaker = null;
this.grid = grid;
this.grid.onColumnAdded(this, prevColumn);
this.grid.onColumnAdded(this, leftColumn);
}
public moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
public moveToGrid(targetGrid: Grid, leftColumn: Column|null) {
if (targetGrid === this.grid) {
this.grid.onColumnMoved(this, prevColumn);
this.grid.moveColumn(this, leftColumn);
} else {
this.grid.onColumnRemoved(this, false);
this.grid.onColumnRemoved(this, this.isFocused());
this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn);
targetGrid.onColumnAdded(this, leftColumn);
for (const window of this.windows.iterator()) {
window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
}
}
}
public moveAfter(prevColumn: Column|null) {
if (prevColumn === this) {
return;
}
this.grid.onColumnMoved(this, prevColumn);
}
public isAfter(other: Column) {
return this.gridX > other.gridX;
}
public isBefore(other: Column) {
public isToTheLeftOf(other: Column) {
return this.gridX < other.gridX;
}
public isToTheRightOf(other: Column) {
return this.gridX > other.gridX;
}
public moveWindowUp(window: Window) {
this.windows.moveBack(window);
this.grid.desktop.onLayoutChanged();
@@ -63,11 +56,19 @@ class Column {
return this.getWindowCount() === 0;
}
public getPrevWindow(window: Window) {
public getFirstWindow(): Window {
return this.windows.getFirst()!;
}
public getLastWindow(): Window {
return this.windows.getLast()!;
}
public getAboveWindow(window: Window) {
return this.windows.getPrev(window);
}
public getNextWindow(window: Window) {
public getBelowWindow(window: Window) {
return this.windows.getNext(window);
}
@@ -132,6 +133,27 @@ class Column {
return this.gridX + this.width;
}
public onUserResizeWidth(
startWidth: number,
currentDelta: number,
resizingLeftSide: boolean,
neighbor?: { column: Column, startWidth: number },
) {
const oldColumnWidth = this.getWidth();
this.setWidth(startWidth + currentDelta, true);
const actualDelta = this.getWidth() - startWidth;
let leftEdgeDeltaStep = resizingLeftSide ? oldColumnWidth - this.getWidth() : 0;
if (neighbor !== undefined) {
const oldNeighborWidth = neighbor.column.getWidth();
neighbor.column.setWidth(neighbor.startWidth - actualDelta, true);
if (resizingLeftSide) {
leftEdgeDeltaStep -= neighbor.column.getWidth() - oldNeighborWidth;
}
}
this.grid.desktop.adjustScroll(-leftEdgeDeltaStep, true);
}
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
if (otherWindow === null) {
@@ -181,9 +203,17 @@ class Column {
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 = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
const opacity = Range.contains(visibleRange, this) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
@@ -245,18 +275,13 @@ class Column {
return true;
}
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);
} else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
this.windows.insertStart(window);
}
}
public onWindowAdded(window: Window) {
this.windows.insertEnd(window);
if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false);
}
@@ -273,7 +298,7 @@ class Column {
public onWindowRemoved(window: Window, passFocus: boolean) {
const lastWindow = this.windows.length() === 1;
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
this.windows.remove(window);

View File

@@ -1,8 +1,5 @@
class Desktop {
public readonly grid: Grid;
public readonly kwinDesktop: KwinDesktop;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
@@ -10,22 +7,25 @@ class Desktop {
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
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.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(kwinDesktop);
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.kwinDesktop);
if (newClientArea === this.clientArea && !this.dirtyPins) {
const newClientArea = Desktop.getClientArea(this.getScreen(), this.kwinDesktop);
if (rectEquals(newClientArea, this.clientArea) && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
@@ -37,8 +37,8 @@ class Desktop {
this.autoAdjustScroll();
}
private static getClientArea(kwinDesktop: KwinDesktop) {
return Workspace.clientArea(ClientAreaOption.PlacementArea, Workspace.activeScreen, kwinDesktop);
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) {
@@ -55,7 +55,7 @@ class Desktop {
)
}
public scrollIntoView(range: Desktop.Range) {
public scrollIntoView(range: Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
@@ -72,10 +72,10 @@ class Desktop {
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Desktop.Range) {
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), false);
this.adjustScroll(Math.round(windowCenter - screenCenter), true);
}
public scrollCenterVisible(focusedColumn: Column) {
@@ -95,13 +95,13 @@ class Desktop {
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
if (this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
return Range.create(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
@@ -125,40 +125,6 @@ class Desktop {
this.setScroll(this.scrollX + dx, force);
}
public equalizeVisibleColumnsWidths() {
const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
for (const minWidth of minWidths) {
if (minWidth > remainingWidth / remainingColumns) {
remainingWidth -= minWidth;
remainingColumns--;
}
}
const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
@@ -169,6 +135,10 @@ class Desktop {
this.dirty = false;
}
public forceArrange() {
this.dirty = true;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
@@ -187,48 +157,14 @@ class Desktop {
namespace Desktop {
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
scroller: Desktop.Scroller,
clamper: Desktop.Clamper,
marginTop: number;
marginBottom: number;
marginLeft: number;
marginRight: number;
scroller: Desktop.Scroller;
clamper: Desktop.Clamper;
};
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange {
private left: Column;
private right: Column;
@@ -240,7 +176,7 @@ namespace Desktop {
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
public addNeighbors(visibleRange: Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
@@ -251,8 +187,8 @@ namespace Desktop {
return column !== null && canFit(column);
}
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
let leftColumn = grid.getLeftColumn(this.left);
let rightColumn = grid.getRightColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
@@ -269,10 +205,10 @@ namespace Desktop {
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getPrevColumn(leftColumn!);
leftColumn = grid.getLeftColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getNextColumn(rightColumn!);
rightColumn = grid.getRightColumn(rightColumn!);
}
checkColumns();
}
@@ -303,9 +239,9 @@ namespace Desktop {
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
}
};
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
}
};
}

View File

@@ -1,5 +1,3 @@
import Range = Desktop.Range;
class Grid {
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
@@ -24,6 +22,18 @@ class Grid {
});
}
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);
@@ -32,11 +42,11 @@ class Grid {
}
public moveColumnRight(column: Column) {
const nextColumn = this.columns.getNext(column);
if (nextColumn === null) {
const rightColumn = this.columns.getNext(column);
if (rightColumn === null) {
return;
}
this.moveColumnLeft(nextColumn);
this.moveColumnLeft(rightColumn);
}
public getWidth() {
@@ -47,11 +57,11 @@ class Grid {
return this.userResize;
}
public getPrevColumn(column: Column) {
public getLeftColumn(column: Column) {
return this.columns.getPrev(column);
}
public getNextColumn(column: Column) {
public getRightColumn(column: Column) {
return this.columns.getNext(column);
}
@@ -94,19 +104,19 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) {
if (Range.contains(visibleRange, column)) {
return column;
}
}
return null;
}
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) {
if (Range.contains(visibleRange, column)) {
last = column;
} else if (last !== null) {
break;
@@ -115,29 +125,14 @@ class Grid {
return last;
}
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) {
if (Range.contains(visibleRange, column)) {
yield column;
}
}
}
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
let width = 0;
let nVisible = 0;
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
width += column.getWidth();
nVisible++;
}
if (nVisible > 0) {
width += (nVisible-1) * this.config.gapsInnerHorizontal;
}
return width;
}
public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize);
@@ -150,11 +145,11 @@ class Grid {
}
}
public onColumnAdded(column: Column, prevColumn: Column|null) {
if (prevColumn === null) {
public onColumnAdded(column: Column, leftColumn: Column|null) {
if (leftColumn === null) {
this.columns.insertStart(column);
} else {
this.columns.insertAfter(column, prevColumn);
this.columns.insertAfter(column, leftColumn);
}
this.columnsSetX(column);
this.desktop.onLayoutChanged();
@@ -163,14 +158,14 @@ class Grid {
public onColumnRemoved(column: Column, passFocus: boolean) {
const isLastColumn = this.columns.length() === 1;
const nextColumn = this.getNextColumn(column);
const columnToFocus = isLastColumn ? null : this.getPrevColumn(column) ?? nextColumn;
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(nextColumn);
this.columnsSetX(rightColumn);
this.desktop.onLayoutChanged();
if (passFocus && columnToFocus !== null) {
@@ -180,18 +175,9 @@ class Grid {
}
}
public onColumnMoved(column: Column, prevColumn: Column|null) {
const movedLeft = prevColumn === null ? true : column.isAfter(prevColumn);
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
this.columnsSetX(firstMovedColumn);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public onColumnWidthChanged(column: Column) {
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
const rightColumn = this.columns.getNext(column);
this.columnsSetX(rightColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) {
this.desktop.autoAdjustScroll();
@@ -200,7 +186,7 @@ class Grid {
public onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null) {
if (lastFocusedColumn !== null && lastFocusedColumn !== column) {
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;

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

View File

@@ -14,16 +14,16 @@ class Window {
};
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this);
column.onWindowAdded(this, true);
}
public moveToColumn(targetColumn: Column) {
public moveToColumn(targetColumn: Column, bottom: boolean) {
if (targetColumn === this.column) {
return;
}
this.column.onWindowRemoved(this, false);
this.column.onWindowRemoved(this, this.isFocused() && targetColumn.grid !== this.column.grid);
this.column = targetColumn;
targetColumn.onWindowAdded(this);
targetColumn.onWindowAdded(this, bottom);
}
public arrange(x: number, y: number, width: number, height: number) {
@@ -36,7 +36,7 @@ class Window {
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedMode > MaximizedMode.Unmaximized) {
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,
@@ -66,6 +66,14 @@ class Window {
}
public onFocused() {
if (this.column.grid.config.reMaximize && (
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
this.focusedState.fullScreen
)) {
// We need to maximize/fullscreen this window, but we can't do it here.
// We need to do it in `arrange` to ensure it happens after placement.
this.column.grid.desktop.forceArrange();
}
this.column.onWindowFocused(this);
}
@@ -75,11 +83,10 @@ class Window {
}
this.client.setFullScreen(false);
this.client.setMaximize(false, false);
this.column.grid.desktop.onLayoutChanged();
}
public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = maximizedMode > MaximizedMode.Unmaximized;
const maximized = maximizedMode !== MaximizedMode.Unmaximized;
this.skipArrange = maximized;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
@@ -107,31 +114,6 @@ class Window {
this.column.grid.desktop.onLayoutChanged();
}
public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height;
if (widthDelta !== 0) {
this.column.adjustWidth(widthDelta, true);
let leftEdgeDelta = newGeometry.left - oldGeometry.left;
const resizingLeftSide = leftEdgeDelta !== 0;
if (resizeNeighborColumn && this.column.grid.config.resizeNeighborColumn) {
const neighborColumn = resizingLeftSide ? this.column.grid.getPrevColumn(this.column) : this.column.grid.getNextColumn(this.column);
if (neighborColumn !== null) {
const oldNeighborWidth = neighborColumn.getWidth();
neighborColumn.adjustWidth(-widthDelta, true);
if (resizingLeftSide) {
leftEdgeDelta -= neighborColumn.getWidth() - oldNeighborWidth;
}
}
}
this.column.grid.desktop.adjustScroll(-leftEdgeDelta, true);
}
if (heightDelta !== 0) {
this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y);
}
}
public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true);
@@ -145,7 +127,7 @@ class Window {
namespace Window {
export type State = {
fullScreen: boolean,
maximizedMode: MaximizedMode,
}
fullScreen: boolean;
maximizedMode: MaximizedMode;
};
}

View File

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

View File

@@ -11,13 +11,12 @@ class WindowRuleEnforcer {
}
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
return this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!this.preferFloating.matches(kwinClient)
);
}
@@ -29,7 +28,7 @@ class WindowRuleEnforcer {
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
const shouldTile = Clients.canTileNow(kwinClient) && enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
@@ -49,7 +48,10 @@ class WindowRuleEnforcer {
for (const windowRule of windowRules) {
const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
const ruleString = ClientMatcher.getRuleString(ruleClass, ruleCaption);
const ruleString = ClientMatcher.getRuleString(
WindowRuleEnforcer.wrapParens(ruleClass),
WindowRuleEnforcer.wrapParens(ruleCaption)
);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
if (ruleCaption !== ".*") {

View File

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

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

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

20
src/lib/utils/math.ts Normal file
View File

@@ -0,0 +1,20 @@
function clamp(value: number, min: number, max: number) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
return value;
}
function sum(...list: number[]) {
return list.reduce((acc, val) => acc + val);
}
function rectEquals(a: QmlRect, b: QmlRect) {
return a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height;
}

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

View File

@@ -2,10 +2,6 @@ function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
Clients.makeTileable(kwinClient);
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
@@ -17,7 +13,7 @@ function initWorkspaceSignalHandlers(world: World) {
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
if (kwinClient === null) {
return;
}
@@ -34,16 +30,22 @@ function initWorkspaceSignalHandlers(world: World) {
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.desktopsChanged, () => {
manager.connect(Workspace.screensChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
})
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, () => {

View File

@@ -1,15 +1,17 @@
class ClientManager {
private readonly world: World;
private readonly config: ClientManager.Config;
private readonly desktopManager: DesktopManager;
private readonly pinManager: PinManager;
private readonly clientMap: Map<KwinClient, ClientWrapper>;
private lastFocusedClient: KwinClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(config: Config, world: World, desktopManager: DesktopManager, pinManager: PinManager) {
constructor(
config: Config,
private readonly world: World,
private readonly desktopManager: DesktopManager,
private readonly pinManager: PinManager,
) {
this.world = world;
this.config = { keepAbove: config.floatingKeepAbove };
this.config = config;
this.desktopManager = desktopManager;
this.pinManager = pinManager;
this.clientMap = new Map();
@@ -27,13 +29,21 @@ class ClientManager {
public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
} else 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);
}
@@ -58,7 +68,7 @@ class ClientManager {
}
private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient) {
if (!kwinClient.transient || kwinClient.transientFor === null) {
return null;
}
@@ -118,6 +128,11 @@ class ClientManager {
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)) {
@@ -163,27 +178,27 @@ class ClientManager {
public onClientFocused(kwinClient: KwinClient) {
this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient, true);
const window = this.findTiledWindow(kwinClient);
if (window !== null) {
window.onFocused();
}
}
public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
public findTiledWindow(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return null;
}
return this.findTiledWindowOfClient(client, followTransient);
return this.findTiledWindowOfClient(client);
}
private findTiledWindowOfClient(client: ClientWrapper, followTransient: boolean): Window|null {
private findTiledWindowOfClient(client: ClientWrapper): Window|null {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientState.Tiled) {
return clientState.window;
} else if (followTransient && client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor, true);
} else if (client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor);
} else {
return null;
}
@@ -202,6 +217,6 @@ class ClientManager {
namespace ClientManager {
export type Config = {
keepAbove: boolean,
}
floatingKeepAbove: boolean;
};
}

View File

@@ -1,20 +1,17 @@
class ClientWrapper {
public readonly kwinClient: KwinClient;
public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor(
kwinClient: KwinClient,
public readonly kwinClient: KwinClient,
constructInitialState: (client: ClientWrapper) => ClientState.State,
transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null,
public transientFor: ClientWrapper | null,
private readonly rulesSignalManager: SignalManager | null,
) {
this.kwinClient = kwinClient;
this.transientFor = transientFor;
@@ -180,7 +177,7 @@ class ClientWrapper {
const manager = new SignalManager();
manager.connect(client.kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
if (maximizedMode > MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
if (maximizedMode !== MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
client.kwinClient.tile = null;
}
client.maximizedMode = maximizedMode;

View File

@@ -1,10 +1,21 @@
namespace Clients {
const prohibitedClasses = [
"ksmserver-logout-greeter",
"xwaylandvideobridge",
];
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable && kwinClient.resizeable && !kwinClient.popupWindow;
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;
return canTileEver(kwinClient) &&
!kwinClient.minimized &&
kwinClient.desktops.length === 1 &&
kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
@@ -36,7 +47,8 @@ namespace Clients {
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.frameGeometry === fullScreenArea;
return kwinClient.clientGeometry.width === fullScreenArea.width &&
kwinClient.clientGeometry.height === fullScreenArea.height;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {

View File

@@ -1,16 +1,21 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private selectedScreen: Output;
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string, currentDesktop: 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);
@@ -43,7 +48,13 @@ class DesktopManager {
private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = new Desktop(kwinDesktop, this.pinManager, this.config, this.layoutConfig);
const desktop = new Desktop(
kwinDesktop,
this.pinManager,
this.config,
() => this.selectedScreen,
this.layoutConfig,
);
this.desktops.set(desktopKey, desktop);
return desktop;
}
@@ -72,6 +83,10 @@ class DesktopManager {
this.kwinDesktops = newDesktops;
}
public selectScreen(screen: Output) {
this.selectedScreen = screen;
}
private removeActivity(activity: string) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(activity, kwinDesktop);

View File

@@ -17,7 +17,7 @@ class PinManager {
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)) {
if (!Clients.isOnVirtualDesktop(client, kwinDesktop) || client.minimized) {
continue;
}

View File

@@ -1,19 +1,30 @@
class World {
public readonly untileOnDrag: boolean;
private readonly desktopManager: DesktopManager;
public readonly clientManager: ClientManager;
private readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer;
constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag;
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,
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
presetWidths: presetWidths,
columnResizer: config.scrollingCentered ? new RawResizer(presetWidths) : new ContextualResizer(presetWidths),
});
this.screenResizedDelayer = new Delayer(1000, () => {
@@ -36,6 +47,7 @@ class World {
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
untileOnDrag: config.untileOnDrag,
};
this.desktopManager = new DesktopManager(
@@ -45,10 +57,7 @@ class World {
marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight,
scroller: config.scrollingLazy ? new LazyScroller() :
config.scrollingCentered ? new CenteredScroller() :
config.scrollingGrouped ? new GroupedScroller() :
console.assert(false),
scroller: World.createScroller(config),
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
},
layoutConfig,
@@ -60,6 +69,19 @@ class World {
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++) {
@@ -79,10 +101,9 @@ class World {
public doIfTiled(
kwinClient: KwinClient,
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
const window = this.clientManager.findTiledWindow(kwinClient);
if (window === null) {
return;
}
@@ -93,10 +114,12 @@ class World {
}
public doIfTiledFocused(
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
this.doIfTiled(Workspace.activeWindow, followTransient, f);
if (Workspace.activeWindow === null) {
return;
}
this.doIfTiled(Workspace.activeWindow, f);
}
public destroy() {

View File

@@ -7,7 +7,7 @@ namespace ClientState {
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
this.client = client;
this.config = config;
if (config.keepAbove) {
if (config.floatingKeepAbove) {
client.kwinClient.keepAbove = true;
}
if (limitHeight && client.kwinClient.tile === null) {
@@ -18,11 +18,9 @@ namespace ClientState {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.client.kwinClient.keepAbove = false;
}
}
// TODO: move to `Tiled.restoreClientAfterTiling`
private static limitHeight(client: ClientWrapper) {
const placementArea = Workspace.clientArea(
ClientAreaOption.PlacementArea,
@@ -50,7 +48,7 @@ namespace ClientState {
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {

View File

@@ -21,6 +21,6 @@ namespace ClientState {
}
export type State = {
destroy(passFocus: boolean): void,
destroy(passFocus: boolean): void;
};
}

View File

@@ -11,7 +11,7 @@ namespace ClientState {
this.pinManager = pinManager;
this.desktopManager = desktopManager;
this.config = config;
if (config.keepAbove) {
if (config.floatingKeepAbove) {
kwinClient.keepAbove = true;
}
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
@@ -19,9 +19,6 @@ namespace ClientState {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.kwinClient.keepAbove = true;
}
this.pinManager.removeClient(this.kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
desktop.onPinsChanged();
@@ -53,7 +50,15 @@ namespace ClientState {
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, () => {

View File

@@ -3,6 +3,7 @@ namespace ClientState {
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 };
@@ -12,7 +13,7 @@ namespace ClientState {
const window = new Window(client, column);
this.window = window;
this.signalManager = Tiled.initSignalManager(world, window);
this.signalManager = Tiled.initSignalManager(world, window, grid.config);
}
public destroy(passFocus: boolean) {
@@ -26,7 +27,7 @@ namespace ClientState {
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window) {
private static initSignalManager(world: World, window: Window, config: LayoutConfig) {
const client = window.client;
const kwinClient = client.kwinClient;
const manager = new SignalManager();
@@ -36,7 +37,7 @@ namespace ClientState {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple desktops are not supported
clientManager.floatKwinClient(kwinClient);
clientManager.floatClient(client);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
@@ -48,7 +49,7 @@ namespace ClientState {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported
clientManager.floatKwinClient(kwinClient);
clientManager.floatClient(client);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
@@ -69,12 +70,13 @@ namespace ClientState {
});
let resizing = false;
let resizingBorder = false;
let resizeStartWidth = 0;
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
if (kwinClient.move) {
if (world.untileOnDrag) {
if (config.untileOnDrag) {
world.do((clientManager, desktopManager) => {
clientManager.floatKwinClient(kwinClient);
clientManager.floatClient(client);
});
}
return;
@@ -82,8 +84,16 @@ namespace ClientState {
if (kwinClient.resize) {
resizing = true;
resizingBorder = Workspace.cursorPos.x > kwinClient.clientGeometry.right ||
Workspace.cursorPos.x < kwinClient.clientGeometry.left;
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();
}
});
@@ -91,10 +101,12 @@ namespace ClientState {
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) {
@@ -112,18 +124,38 @@ namespace ClientState {
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(() => window.onUserResize(oldGeometry, resizingBorder));
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
) {
world.do(() => window.onFrameGeometryChanged());
if (externalFrameGeometryChangedRateLimiter.acquire()) {
world.do(() => window.onFrameGeometryChanged());
}
}
});
@@ -143,6 +175,18 @@ namespace ClientState {
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
@@ -150,7 +194,7 @@ namespace ClientState {
}
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn);
window.moveToColumn(newColumn, true);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
@@ -160,6 +204,7 @@ namespace ClientState {
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
@@ -188,7 +233,7 @@ namespace ClientState {
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean,
}
skipSwitcher: boolean;
};
}
}

View File

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

View File

@@ -1,3 +1,7 @@
function init() {
return new World(loadConfig());
}
function loadConfig(): Config {
const config: any = {};
for (const entry of configDef) {

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

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

View File

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

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

View File

@@ -0,0 +1,110 @@
tests.register("User resize", 10, () => {
const config = getDefaultConfig();
config.resizeNeighborColumn = true;
const h = getWindowHeight(2);
let clientLeft: MockKwinClient, clientRightTop: MockKwinClient, clientRightBottom: MockKwinClient;
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeft, clientRightTop);
Assert.rect(clientLeft.frameGeometry, left, tilingArea.top, leftWidth, tilingArea.height);
Assert.rect(clientRightTop.frameGeometry, left+leftWidth+gapH, tilingArea.top, rightWidth, topHeight);
Assert.rect(clientRightBottom.frameGeometry, left+leftWidth+gapH, tilingArea.top+topHeight+gapV, rightWidth, bottomHeight);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, false, false, false, new MockQmlSize(10, 20));
assertSizes(310, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 300, h, h);
workspaceMock.resizeWindow(clientRightTop, false, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 290, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, false, false, false, new MockQmlSize(-10, 20));
assertSizes(310, 280, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, false, false, true, new MockQmlSize(0, 20));
assertSizes(310, 280, h-40, h+40);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 20));
assertSizes(310, 290, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 290, h, h);
workspaceMock.resizeWindow(clientRightTop, true, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 280, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, true, false, new MockQmlSize(-10, 20));
assertSizes(320, 270, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, false, true, new MockQmlSize(0, 20));
assertSizes(320, 270, h-40, h+40);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
clientRightBottom.minSize = new MockQmlSize(295, h-20);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 20));
assertSizes(310, 295, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 295, h, h);
workspaceMock.resizeWindow(clientRightTop, true, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 295, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, true, false, new MockQmlSize(-10, 20));
assertSizes(310, 295, h-20, h+20);
workspaceMock.resizeWindow(clientRightTop, true, true, false, new MockQmlSize(-10, 0));
assertSizes(310, 295, h-20, h+20);
// TODO
// workspaceMock.resizeWindow(clientRightBottom, true, false, true, new MockQmlSize(0, -80));
// assertSizes(310, 295, h+60, h-20);
}
{
const { qtMock, workspaceMock, world } = init(config);
const [clientLeftTop, clientLeftBottom, clientRight] = workspaceMock.createClientsWithWidths(300, 200, 300);
clientLeftBottom.minSize = new MockQmlSize(295, h-20);
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeftTop, clientRight);
Assert.rect(clientLeftTop.frameGeometry, left, tilingArea.top, leftWidth, topHeight);
Assert.rect(clientLeftBottom.frameGeometry, left, tilingArea.top+topHeight+gapV, leftWidth, bottomHeight);
Assert.rect(clientRight.frameGeometry, left+leftWidth+gapH, tilingArea.top, rightWidth, tilingArea.height);
}
workspaceMock.activeWindow = clientLeftBottom;
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(-10, 0));
assertSizes(295, 305, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(10, 0));
assertSizes(305, 295, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(-20, 0), new MockQmlSize(20, 0));
assertSizes(305, 295, h, h);
}
});

1
src/tests/main.ts Normal file
View File

@@ -0,0 +1 @@
tests.run();

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

@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"include": [
"../lib/**/*",
"./utils/**/*",
"./units/**/*",
"./flows/**/*",
"./main.ts"
]
}

View File

@@ -0,0 +1,45 @@
tests.register("PresetWidths", 1, () => {
const minWidth = 50;
const maxWidth = 800;
const spacing = 10;
const testCases = [
{ str: "100%, 50%", result: [395, 800] },
{ str: "105%, 50%", result: [395, 800] },
{ str: "100px,50 px", result: [50, 100] },
{ str: "900px,25 px", result: [50, 800] },
{ str: " 100px, 25 % , 0.1 ", result: [71, 100, 192] },
{ str: "100px, 25%, 0.1, 100px", result: [71, 100, 192] },
{ str: "100px, -25 % , 0.1 ", error: true },
{ str: "100px, 25 % , -0.1 ", error: true },
{ str: "100px, 25 % , 0.1p", error: true },
{ str: "100px, % , 0.1 ", error: true },
{ str: "100px, , 0.1 ", error: true },
{ str: "100px, 0, 0.1 ", error: true },
{ str: "100px,, 0.1 ", error: true },
{ str: "100px, 25 % , ", error: true },
{ str: "asdf", error: true },
{ str: "", error: true },
{ str: " ", error: true },
];
function assertWidths(presetWidths: PresetWidths, expectedWidths: number[]) {
let currentWidth = 0;
for (const expectedWidth of expectedWidths) {
currentWidth = presetWidths.next(currentWidth, minWidth, maxWidth);
Assert.equal(currentWidth, expectedWidth);
}
const repeatedWidth = presetWidths.next(currentWidth, minWidth, maxWidth);
Assert.equal(repeatedWidth, expectedWidths[0]);
}
for (const testCase of testCases) {
try {
const presetWidths = new PresetWidths(testCase.str, spacing);
Assert.assert(!testCase.error);
assertWidths(presetWidths, testCase.result!);
} catch (error) {
Assert.assert(testCase.error === true);
}
}
});

View File

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

View File

@@ -0,0 +1,22 @@
tests.register("RateLimiter", 1, () => {
const rateLimiter = new RateLimiter(3, 100);
function testRateLimiter() {
Assert.assert(rateLimiter.acquire());
Assert.assert(rateLimiter.acquire());
Assert.assert(rateLimiter.acquire());
Assert.assert(!rateLimiter.acquire());
Assert.assert(!rateLimiter.acquire());
}
timeControl(addTime => {
testRateLimiter();
addTime(10);
Assert.assert(!rateLimiter.acquire(), { message: "The interval hasn't expired yet" });
addTime(90);
// the rate limiter interval has expired, let's test again
testRateLimiter();
});
});

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