221 Commits

Author SHA1 Message Date
Peter Fajdiga
9bda7d1a09 bump version to 0.7.1 2024-02-12 21:27:34 +01:00
Peter Fajdiga
2ce72bcee8 qt.d.ts: add console.trace 2024-02-12 21:17:37 +01:00
Peter Fajdiga
ff3f6c5d6b Desktop: fix ColumnRange when a column's width is still 0 2024-02-12 21:10:12 +01:00
Peter Fajdiga
3ab230b498 Makefile: append version number to package file name 2024-02-11 20:37:28 +01:00
Peter Fajdiga
ba9f362a1c .gitignore: ignore suffixed packages 2024-02-11 20:37:27 +01:00
Peter Fajdiga
ad6c3f1cae bump version to 0.7 2024-02-11 20:37:26 +01:00
Peter Fajdiga
ba4dd2a9c1 config.ui: relabel scrollingGrouped button 2024-02-11 20:37:25 +01:00
Peter Fajdiga
bb61853009 config.ui: reorder tabs 2024-02-11 20:37:22 +01:00
Peter Fajdiga
0cfd9b9e36 Desktop: scrollCenterRange: add parameter prioritiseVisible 2024-01-22 08:55:05 +01:00
Peter Fajdiga
43c4f7ef9a Actions: columnWidthIncrease: add steps for fully visible screen-edge columns 2024-01-22 08:55:05 +01:00
Peter Fajdiga
9cb3f33ecb Actions: extract function findNextStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
31b9e61ae3 config.ui: add manualResizeStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
668e6696ab Actions: column width increase/decrease: replace screen-relative steps with column-relative steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
e63959cfbf Actions: getWidthSteps: ignore screen-relative steps too close to existing steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
ef2650beb8 Actions: improve snapping in columnWidthDecrease 2024-01-22 08:55:05 +01:00
Peter Fajdiga
750c47c040 implement resize steps (resolves #25) 2024-01-22 08:55:05 +01:00
Peter Fajdiga
88ca0d02e1 generators/config/kcfg.ts: escape xml characters in default config values 2024-01-21 19:40:12 +01:00
Peter Fajdiga
aba786b754 Desktop: rename scrollIntoView 2024-01-21 18:39:53 +01:00
Peter Fajdiga
47aa625c99 Actions: column width increase/decrease: use getCurrentVisibleRange 2024-01-21 18:39:25 +01:00
Peter Fajdiga
03c7cc6503 Desktop.scrollToRange: simplify 2024-01-21 18:37:34 +01:00
Peter Fajdiga
9e9ff2b74f remove overscroll feature (resolves #23) 2024-01-21 18:26:18 +01:00
Peter Fajdiga
5674624e6f Desktop.equalizeVisibleColumnsWidths: simplify scroll at the end 2024-01-21 18:17:29 +01:00
Peter Fajdiga
44dd88ef7c Desktop.equalizeVisibleColumnsWidths: handle columns with limited min width 2024-01-21 18:17:29 +01:00
Peter Fajdiga
f800d6ecf0 Desktop: rewrite ColumnRange.addNeighbors 2024-01-21 18:17:29 +01:00
Peter Fajdiga
3477e17bb3 Desktop: scrollCenterRange: replace parameter requireVisible with condition 2024-01-21 18:17:29 +01:00
Peter Fajdiga
755c781646 rename parameters of doIfTiled passed functions 2024-01-21 18:17:29 +01:00
Peter Fajdiga
926345ba31 move column width increase/decrease code to Actions.ts 2024-01-21 18:17:29 +01:00
Peter Fajdiga
a2295ede43 Desktop: add method scrollCenterVisible (moved from ScrollerGrouped) 2024-01-21 18:17:29 +01:00
Peter Fajdiga
ca80a7ca28 Grid.onColumnWidthChanged: fix autoAdjustScroll call 2024-01-14 15:40:24 +01:00
Peter Fajdiga
64474b1677 readme: mention Niri 2024-01-14 09:55:57 +01:00
Peter Fajdiga
eca63cbc16 readme: update key bindings 2023-12-30 17:26:59 +01:00
Peter Fajdiga
3a8baf4cd7 bump version to 0.6 2023-12-30 17:18:44 +01:00
Peter Fajdiga
dc14171ae7 config: add Wayland window classes to window rules (resolves #24) 2023-12-27 19:45:07 +01:00
Peter Fajdiga
6dcf8979c2 config: add yakuake to window rules 2023-12-27 19:41:30 +01:00
Peter Fajdiga
fe5661c07f Grid: auto-scroll after removing a non-focused column 2023-12-24 14:58:44 +01:00
Peter Fajdiga
90b783b34b Grid: remove unused methods getLeftOffScreenColumn and getRightOffScreenColumn 2023-12-24 12:52:10 +01:00
Peter Fajdiga
fb40bd9592 Grid.decreaseColumnWidth: prevent scrolling away from focused column when reaching minimum width 2023-12-24 12:50:52 +01:00
Peter Fajdiga
768d95450d refactor arrange functions 2023-12-24 09:29:31 +01:00
Peter Fajdiga
e95a0e44c9 prevent translucent windows during resizing 2023-12-24 09:21:49 +01:00
Peter Fajdiga
c1b8d05919 defaultWindowRules: update Zoom entry 2023-12-24 09:21:44 +01:00
Peter Fajdiga
e98ce18105 add opacity settings for obscured windows 2023-12-16 12:10:26 +01:00
Peter Fajdiga
25a9efc8e4 config.ui: split up parameters and behavior tabs 2023-12-16 11:39:42 +01:00
Peter Fajdiga
db48644944 package/contents/ui: add footer spacer to take up excess space 2023-12-16 11:39:42 +01:00
Peter Fajdiga
950e0de076 Desktop: prevent scrolling when unnecessary (add dirtyScroll variable) 2023-12-16 10:07:02 +01:00
Peter Fajdiga
05ffe0895e Revert "Desktop.scrollToCenterRange: force scroll"
This reverts commit dda63d68cde58c7f4a7162b11a2fd614365d36ff.
2023-12-16 09:33:26 +01:00
Peter Fajdiga
61db5ca69f use different implementations of clampScrollX in different scrollers 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f7b5dd0b9c Desktop.equalizeVisibleColumnsWidths: use Desktop.RangeImpl.fromRanges 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f83f60c98f Grid: reimplement increaseColumnWidth and decreaseColumnWidth 2023-12-16 09:33:26 +01:00
Peter Fajdiga
3e8734eefb Desktop.scrollToCenterRange: force scroll 2023-12-16 09:33:26 +01:00
Peter Fajdiga
bed0ea7ed8 Actions: make gridScrollFocused center focused column again 2023-12-16 09:33:26 +01:00
Peter Fajdiga
92c99f0b87 rename focusColumn -> scrollToColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
b2024bc8aa Actions: make scrolling actions use focusColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
58f358313b config.ui: change description for kcfg_scrollingLazy 2023-12-16 09:33:26 +01:00
Peter Fajdiga
352a7061f6 config.ui: shorten description for kcfg_scrollingCentered 2023-12-16 09:33:26 +01:00
Peter Fajdiga
7314c0ee24 add ScrollerGrouped 2023-12-16 09:33:26 +01:00
Peter Fajdiga
c65361853c config.ui: use button groups 2023-12-16 09:32:26 +01:00
Peter Fajdiga
fa53e765b3 qt.d.ts: rename QmlTimer 2023-12-16 09:30:19 +01:00
Peter Fajdiga
4d35681ee2 qt.d.ts: rename QmlSize 2023-12-16 09:30:14 +01:00
Peter Fajdiga
1824bcdf85 fix usages of QmlRect.right and .bottom 2023-12-12 17:33:10 +01:00
Peter Fajdiga
ce1b402bf2 qt.d.ts: add comments to QmlRect fields 2023-12-10 21:08:00 +01:00
Peter Fajdiga
2df6d5d8e6 qt.d.ts: rename QmlRect 2023-12-10 21:06:03 +01:00
Peter Fajdiga
e7d33030ba read and use scrolling configuration 2023-12-05 22:15:49 +01:00
Peter Fajdiga
2bd000f0a6 config.ui: add scrolling configuration 2023-12-05 22:03:22 +01:00
Peter Fajdiga
6313d8f18e implement scrollers 2023-12-05 21:59:01 +01:00
Peter Fajdiga
464ec3bcb1 Grid.increaseColumnWidth: fix scroll adjustment after resize 2023-12-02 20:57:46 +01:00
Peter Fajdiga
fae793cb09 Desktop.equalizeVisibleColumnsWidths: adjust scroll after resizing 2023-12-02 20:50:13 +01:00
Peter Fajdiga
d7346a6fab Desktop.clampScrollX: simplify 2023-12-02 20:38:14 +01:00
Peter Fajdiga
0e5efd2be7 Desktop: receive Range in parameters instead of Column 2023-12-02 17:36:29 +01:00
Peter Fajdiga
a1a315790e turn Desktop.Range into an interface (well, into a type) 2023-12-02 14:12:41 +01:00
Peter Fajdiga
9d62499bf0 Grid.Range: make fields private 2023-12-02 14:09:23 +01:00
Peter Fajdiga
97cf61d1dd Grid: extract method calculateVisibleRange 2023-12-02 14:06:28 +01:00
Peter Fajdiga
5d83c6dd2c rename Grid.ScrollPos to Grid.Range 2023-12-02 13:55:39 +01:00
Peter Fajdiga
8915e8a9da Grid.getVisibleColumnsWidth: use getVisibleColumns 2023-12-02 12:44:57 +01:00
Peter Fajdiga
22e4c47189 add action columns-width-equalize (resolves #22) 2023-12-02 12:44:57 +01:00
Peter Fajdiga
552d2b851f readme: list QML dependencies 2023-11-12 10:02:33 +01:00
Peter Fajdiga
c4ce795359 config.ui: add stacked mode description to tooltip 2023-09-29 10:23:33 +02:00
Peter Fajdiga
1ac1fc3c2b readme: update key bindings 2023-09-29 10:21:47 +02:00
Peter Fajdiga
d0e041d16a keyBindings: add comment for column-toggle-stacked 2023-09-29 10:21:26 +02:00
Peter Fajdiga
8fc3fc976d Window: WindowState -> Window.State 2023-09-29 08:58:38 +02:00
Peter Fajdiga
e0eeace9dc ClientState.Manager: ; -> , 2023-09-29 08:58:37 +02:00
Peter Fajdiga
84e2a06b35 Tiled: define type WindowState 2023-09-29 08:56:45 +02:00
Peter Fajdiga
3373e02658 add skipSwitcher setting 2023-09-29 08:52:59 +02:00
Peter Fajdiga
43f425c868 config: set overscroll default to 0 2023-09-22 17:45:11 +02:00
Peter Fajdiga
5c5127f7ce bump version to 0.5 2023-09-22 17:42:16 +02:00
Peter Fajdiga
8664f05998 ClientManager: prevent glitchy placement when maximizing a pinned client 2023-09-22 17:36:54 +02:00
Peter Fajdiga
e4f6a32d42 make pinning work with kwin-tiled windows of any frameGeometry 2023-09-22 17:25:32 +02:00
Peter Fajdiga
2882bb8d5d base pinning on KwinClient.tile instead of frameGeometry 2023-09-22 14:40:25 +02:00
Peter Fajdiga
5ef71c92ce Column: prevent stacking columns with unshadeable windows 2023-09-22 11:15:35 +02:00
Peter Fajdiga
de59979a6e Tiled: restructure branches in moveResizedChanged handler 2023-09-22 10:54:12 +02:00
Peter Fajdiga
71d7d60837 ClientManager: use kwinClient parameters 2023-09-20 20:14:30 +02:00
Peter Fajdiga
7f44d23dd0 kwin.d.ts: merge AbstractClient and TopLevel 2023-09-20 20:13:23 +02:00
Peter Fajdiga
aceffae5f9 config.ui: add tooltips 2023-09-20 19:53:33 +02:00
Peter Fajdiga
09e5eaca88 add re-maximize setting 2023-09-19 21:15:01 +02:00
Peter Fajdiga
734dd8a4cc fix re-full-screen bug with Kate on Wayland 2023-09-19 21:05:59 +02:00
Peter Fajdiga
aeabb396f9 Revert "remove re-maximize and re-fullscreen functionality" (fixes #18)
This reverts commit 4f06f17b
2023-09-19 21:04:27 +02:00
Peter Fajdiga
05b9ebc325 handle grid changes during a full-screen window 2023-09-19 18:58:50 +02:00
Peter Fajdiga
4728afb5ea Grid: set preferred width in increaseColumnWidth and decreaseColumnWidth 2023-09-17 19:17:50 +02:00
Peter Fajdiga
653cb20e43 ClientWrapper: remove obsolete TODO 2023-09-17 19:17:50 +02:00
Peter Fajdiga
0cb2b68bf6 implement workaround that fixes #19 2023-09-17 19:17:50 +02:00
Peter Fajdiga
b8b8900754 Tiled: call World.do only if condition satisfied 2023-09-17 08:00:32 +02:00
Peter Fajdiga
14c006b5b1 Window.onFullScreenChanged: remove check for isFocused 2023-09-16 15:34:12 +02:00
Peter Fajdiga
9fe6be9b91 implement pinning (resolves #8) 2023-09-16 14:55:38 +02:00
Peter Fajdiga
97d1592318 add option for setting keepAbove on floating windows (fixes #15) 2023-09-16 14:51:46 +02:00
Peter Fajdiga
cc8cc04b05 WindowRuleEnforcer.shouldTile: don't tile transient windows 2023-09-10 18:18:48 +02:00
Peter Fajdiga
cc74d3610a WindowRuleEnforcer.shouldTile: explode condition 2023-09-10 18:17:25 +02:00
Peter Fajdiga
088725402e WindowRuleEnforcer: refactor function -> private static 2023-09-10 17:31:54 +02:00
Peter Fajdiga
25b7507b30 fix incorrect movement of transient windows (bug introduced in f4e9822f29) 2023-09-09 15:01:59 +02:00
Peter Fajdiga
9b32caafdc config/loader.ts: declare return type of loadConfig 2023-09-09 12:58:56 +02:00
Peter Fajdiga
668d579d63 prevent resizing new floating windows 2023-09-09 11:08:01 +02:00
Peter Fajdiga
f4e9822f29 ClientManager.addClient: avoid constructing state twice 2023-09-09 09:19:37 +02:00
Peter Fajdiga
bdb0a4aeb0 ClientWrapper: refactor constructor parameter initialState -> constructInitialState 2023-09-09 09:11:01 +02:00
Peter Fajdiga
dde1a12fce ClientManager.addClient: use variable kwinClient 2023-09-09 09:01:01 +02:00
Peter Fajdiga
c57c8391fb ClientWrapper: move tiling- and floating-specific functions to ClientState.Floating and ClientState.Tiled 2023-09-08 15:40:16 +02:00
Peter Fajdiga
ec64b47ceb Tiled: add comment regarding kwinClient.fullScreen 2023-09-06 22:53:36 +02:00
Peter Fajdiga
b055345e48 readme: update key bindings 2023-09-04 20:24:17 +02:00
Peter Fajdiga
eb43f45287 bump version to 0.4 2023-09-04 20:20:06 +02:00
Peter Fajdiga
e61d7538b2 re-arrange desktop when switching to it 2023-09-03 19:58:01 +02:00
Peter Fajdiga
a3c0976f55 main.qml: remove start and stop logs 2023-09-03 19:25:10 +02:00
Peter Fajdiga
b372489eb5 ClientState: remove unnecessary ClientState. prefixes 2023-09-03 19:13:33 +02:00
Peter Fajdiga
fa974a68aa kwin.d.ts: add comments for desktop and activities 2023-09-03 19:13:33 +02:00
Peter Fajdiga
20517aed7f Desktop.updateArea: set dirty flag 2023-09-03 14:15:01 +02:00
Peter Fajdiga
b49082d51d DesktopManager.getDesktop: fix desktop number validity check 2023-09-02 12:33:08 +02:00
Peter Fajdiga
08135a4ad4 prevent setting incorrect frameGeometry to windows that went from Karousel-tiled to Kwin-quick-tiled 2023-09-02 10:54:47 +02:00
Peter Fajdiga
681ae38d85 ClientState.Manager.setState: construct the new state after destroying the old one 2023-09-02 10:45:02 +02:00
Peter Fajdiga
9912a8d917 ClientState: split into files 2023-09-02 10:24:07 +02:00
Peter Fajdiga
1bcf768588 unset kwinClient.tile on maximize 2023-09-02 09:42:53 +02:00
Peter Fajdiga
c84fddc618 Tiled: guess maximized or full screen based on frameGeometry 2023-09-02 09:42:52 +02:00
Peter Fajdiga
ad62dafdc7 kwin.d.ts: move desktop property to TopLevel to reflect documentation 2023-09-02 09:14:45 +02:00
Peter Fajdiga
425c5c9e5b remove rectEqual 2023-09-02 09:07:28 +02:00
Peter Fajdiga
4b0f259c6d Window.onFrameGeometryChanged: remove parameter oldGeometry 2023-09-02 09:01:08 +02:00
Peter Fajdiga
a5ecc94479 prepend "Karousel:" to logs 2023-09-01 21:17:11 +02:00
Peter Fajdiga
a3f479e2e6 World: rename desktopManager 2023-09-01 17:24:42 +02:00
Peter Fajdiga
daef95731b add semicolons after type aliases 2023-09-01 17:24:42 +02:00
Peter Fajdiga
3b3bb679de qt.d.ts: define QmlObject as unknown 2023-09-01 17:24:42 +02:00
Peter Fajdiga
566c8fe53d kwin.d.ts: mark readonly properties 2023-09-01 17:24:42 +02:00
Peter Fajdiga
07ce7d4e60 Tiled: rename client 2023-09-01 14:18:40 +02:00
Peter Fajdiga
0dfc29b1eb define enum ClientAreaOption 2023-09-01 14:03:52 +02:00
Peter Fajdiga
4d784c5d01 keyBindings: allow up to 12 numerical key bindings, regardless of defaults 2023-08-30 21:59:44 +02:00
Peter Fajdiga
a4d27a2885 keyBindings: add comments regarding clashes with default KDE shortcuts 2023-08-30 21:46:48 +02:00
Peter Fajdiga
f703f0655a rename directory "clientState" 2023-08-30 21:21:14 +02:00
Peter Fajdiga
963949b039 clientstate.Manager: redefine State type 2023-08-30 21:20:17 +02:00
Peter Fajdiga
22ee707207 Desktop: correctly set clientArea (fixes bug from 0e59f382) 2023-08-30 20:58:00 +02:00
Peter Fajdiga
8d6e4f9bc7 add type parameters to QSignal 2023-08-29 23:39:53 +02:00
Peter Fajdiga
075f6c7e3d qt.d.ts: define void return type for QSignal methods 2023-08-29 23:16:41 +02:00
Peter Fajdiga
5404b61d20 kwin.d.ts: remove X11Client 2023-08-29 22:29:06 +02:00
Peter Fajdiga
76b0016055 create workaround for issue #9 2023-08-29 22:27:36 +02:00
Peter Fajdiga
64cdb90f4a define kwin client types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
80ecc7e6c9 define kwin types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
0e59f382b4 Desktop: refactor clientArea and tilingArea 2023-08-29 22:19:16 +02:00
Peter Fajdiga
6001dd5b02 define qt types 2023-08-29 22:19:16 +02:00
Peter Fajdiga
201dd4463e ClientMatcher: remove String cast 2023-08-29 22:19:16 +02:00
Peter Fajdiga
10718bc2c7 use type instead of interface 2023-08-29 21:03:32 +02:00
Peter Fajdiga
b15bb85037 split up extern.d.ts into multiple files 2023-08-29 21:03:19 +02:00
Peter Fajdiga
4904d075ae show a notification if window rules JSON invalid 2023-08-28 23:53:11 +02:00
Peter Fajdiga
7871bbbe6d capitalize "Window Rules" tab title in config dialog 2023-08-28 23:49:04 +02:00
Peter Fajdiga
d91ea7b412 ClientWrapper: handle Kwin's quick tiles 2023-08-27 09:40:13 +02:00
Peter Fajdiga
37e9b85279 Tiled: untile when tiling using Kwin's tiling system 2023-08-25 17:15:12 +02:00
Peter Fajdiga
0bdb4af0e6 create ClientState namespace 2023-08-25 15:27:16 +02:00
Peter Fajdiga
4f06f17ba7 remove re-maximize and re-fullscreen functionality 2023-08-25 14:22:31 +02:00
Peter Fajdiga
382cbe101b Window: unset fullscreen before unmaximizing 2023-08-25 13:58:43 +02:00
Peter Fajdiga
e580acf979 update column widths after screen size change 2023-08-25 12:46:01 +02:00
Peter Fajdiga
453c4ece2c Grid: rename field desktop 2023-08-25 11:58:27 +02:00
Peter Fajdiga
15b77d0207 re-arrange after window move 2023-08-25 11:02:08 +02:00
Peter Fajdiga
d23c13c344 Desktop: fix unnecessary overscroll removing right margin on window close 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c4307e187f use namespaces instead of modules 2023-08-25 11:01:30 +02:00
Peter Fajdiga
463da59197 add krunner to default window rules 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c5ec40e5ea World.update: make private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
2f4268fc94 mark remaining methods and fields explicitly public or private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
f1911b1247 Desktop: rename ScrollPos 2023-08-25 11:01:30 +02:00
Peter Fajdiga
5b71f1c48f DesktopManager: rename methods 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c7e7b91f3f rename ScrollView to Desktop 2023-08-25 11:01:30 +02:00
Peter Fajdiga
4b3a403559 rename desktopNumber and virtualDesktop 2023-08-25 11:01:30 +02:00
Peter Fajdiga
9477b7e337 refactor arrange 2023-08-25 11:01:30 +02:00
Peter Fajdiga
c87ef982ae ClientWrapper: prevent moving transient windows on other desktops 2023-08-25 11:01:30 +02:00
Peter Fajdiga
de3e78424a ScrollView: ScrollPos -> ScrollView.Pos 2023-08-25 11:01:30 +02:00
Peter Fajdiga
3039033ea9 World.getFocusedWindow: add ability to follow transientFor 2023-08-25 11:01:30 +02:00
Peter Fajdiga
de0f89062a World.doIfTiled: extract helper method findTiledWindow 2023-08-25 11:01:30 +02:00
Peter Fajdiga
0831c1be8b LinkedList: LinkedListNode -> LinkedList.Node 2023-08-25 11:01:30 +02:00
Peter Fajdiga
d6bfe2fd03 mark methods explicitly public or private 2023-08-25 11:01:30 +02:00
Peter Fajdiga
048bf2a51a Grid.moveColumnLeft: call ScrollView.onGridReordered 2023-08-25 11:01:30 +02:00
Peter Fajdiga
a949dca458 readme: update key bindings 2023-08-16 21:40:27 +02:00
Peter Fajdiga
57c4643098 bump version to 0.3.1 2023-08-16 21:25:17 +02:00
Peter Fajdiga
e92563b424 Grid.decreaseColumnWidth: fix scroll left bug 2023-08-16 21:19:54 +02:00
Peter Fajdiga
671326bdd7 Grid: remove unneeded arrange calls 2023-08-16 21:19:54 +02:00
Peter Fajdiga
5e9db7d2cd prevent off-screen transients (fixes #11) 2023-08-16 21:19:53 +02:00
Peter Fajdiga
b447eacdfd World: pass same config object to ScrollViewManager 2023-08-15 17:21:10 +02:00
Peter Fajdiga
94f6e6f33b Revert "remove actions gridScrollLeft and gridScrollRight"
This reverts commit bdf62b65
2023-08-15 17:18:36 +02:00
Peter Fajdiga
85b0221220 rename Actions.ts 2023-08-15 17:08:01 +02:00
Peter Fajdiga
1894b055f7 create Actions module 2023-08-15 17:07:48 +02:00
Peter Fajdiga
05f7550a3b move KwinClient util functions into Clients module 2023-08-15 17:05:32 +02:00
Peter Fajdiga
a04f629de0 Column: make width private 2023-08-13 23:34:37 +02:00
Peter Fajdiga
4bda4d0d7c Window: update skipArrange comment 2023-08-13 20:53:18 +02:00
Peter Fajdiga
8bf076948a readme: key binding configuration 2023-08-09 22:45:45 +02:00
Peter Fajdiga
04bd85a287 readme: update key bindings 2023-07-21 09:07:18 +02:00
Peter Fajdiga
b8cf677084 bump version to 0.3 2023-07-21 09:04:11 +02:00
Peter Fajdiga
e67e6f4d62 World: remove config property 2023-07-15 19:18:34 +02:00
Peter Fajdiga
bdf62b65e4 remove actions gridScrollLeft and gridScrollRight 2023-07-15 19:15:15 +02:00
Peter Fajdiga
3238d70772 extract LayoutConfig 2023-07-15 19:15:14 +02:00
Peter Fajdiga
8382696d01 extract ScrollView.Config 2023-07-15 19:13:11 +02:00
Peter Fajdiga
18470b391f Grid: count horizontal gap as part of the partially visible window 2023-07-15 18:33:05 +02:00
Peter Fajdiga
ed8ec7c794 move Grid.isColumnVisible to Column.isVisible 2023-07-15 12:39:15 +02:00
Peter Fajdiga
fce442c25d rename action columnToggleStacked 2023-07-07 14:40:32 +02:00
Peter Fajdiga
81ef0e0442 remove action columnExpand 2023-07-07 14:37:24 +02:00
Peter Fajdiga
0fbb0fe90e promote ScrollPos to class 2023-07-07 14:04:46 +02:00
Peter Fajdiga
b12b70a294 ScrollView: remove gridToTilingSpace 2023-07-07 13:51:18 +02:00
Peter Fajdiga
0a3ba5c963 ScrollView: refactor scrollCenterColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
fa17b1fcc2 ScrollView: refactor getScrollPosForColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
68d2c5bbd8 fix scrollCenterColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
5c8da41647 add helper function getScrollPosForColumn 2023-07-07 13:51:18 +02:00
Peter Fajdiga
9e808f99c9 move Column functionality from ScrollView back to Grid 2023-07-07 13:51:18 +02:00
Peter Fajdiga
29fa65613e extract scrolling functionality from Grid to ScrollView 2023-07-07 13:51:16 +02:00
Peter Fajdiga
9c1592b626 add setting untileOnDrag 2023-07-05 12:20:00 +02:00
Peter Fajdiga
dec4281bb9 remove actions expandVisibleColumns and shrinkVisibleColumns 2023-07-05 11:52:11 +02:00
Peter Fajdiga
41facafac7 Grid: prevent overscrolling when it would unnecessarily hide a fully visible window 2023-07-05 11:47:16 +02:00
Peter Fajdiga
0635e20622 Grid: add helper method gridToTilingSpace 2023-07-05 11:29:42 +02:00
Peter Fajdiga
454a14724d Column: add methods getLeft and getRight 2023-07-01 22:53:28 +02:00
Peter Fajdiga
0fff1ce837 update "tilingArea space" comments 2023-07-01 21:41:30 +02:00
Peter Fajdiga
ee8bac5a42 columnWidthIncrease and Decrease 2023-07-01 11:16:38 +02:00
Peter Fajdiga
dba5e07a86 add setting resizeNeighborColumn 2023-06-24 13:09:02 +02:00
Peter Fajdiga
e2a5625d41 resize neighbor column on edge resize 2023-06-24 13:03:22 +02:00
Peter Fajdiga
6e9edad39d Column: respect min width of windows 2023-06-24 11:07:53 +02:00
55 changed files with 2685 additions and 1363 deletions

2
.gitignore vendored
View File

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

View File

@@ -1,6 +1,7 @@
.PHONY: * .PHONY: *
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern.d.ts TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
config: config:
mkdir -p ./package/contents/config mkdir -p ./package/contents/config
@@ -16,7 +17,7 @@ uninstall:
kpackagetool5 --type=KWin/Script -r ./package kpackagetool5 --type=KWin/Script -r ./package
package: package:
tar -czf ./karousel.tar.gz ./package tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package
logs: logs:
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f

View File

@@ -13,22 +13,31 @@ unprompted reflow of window content.
Windows are automatically centered when possible. And when running out of width, windows can be Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally. scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
[Cardboard](https://gitlab.com/cardboardwm/cardboard). [Cardboard](https://gitlab.com/cardboardwm/cardboard).
## Dependencies
Karousel requires the following QML modules:
- QtQuick 2.15
- org.kde.kwin 3.0
- org.kde.notification 1.0
## Limitations ## Limitations
- Doesn't support multiple screens - Doesn't support multiple screens
- Doesn't support windows on all desktops - Doesn't support windows on all desktops
- Doesn't support windows on multiple activities - Doesn't support windows on multiple activities
## Key bindings ## Key bindings
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
Here's the default ones:
| Shortcut | Action | | Shortcut | Action |
| --- | --- | | --- | --- |
| Meta+Space | Toggle floating | | Meta+Space | Toggle floating |
| Meta+A | Move focus left | | Meta+A | Move focus left |
| Meta+D | Move focus right | | Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up | | Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down | | Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Home | Move focus to start | | Meta+Home | Move focus to start |
| Meta+End | Move focus to end | | Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) | | Meta+Shift+A | Move window left (Moves window out of and into columns) |
@@ -37,14 +46,14 @@ Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) an
| Meta+Shift+S | Move window down | | Meta+Shift+S | Move window down |
| Meta+Shift+Home | Move window to start | | Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end | | Meta+Shift+End | Move window to end |
| Meta+X | Expand window (Expands focused window vertically; toggles stacked layout for focused column) | | Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+Ctrl+Shift+A | Move column left | | Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right | | Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start | | Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end | | Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl+X | Expand column (Expands focused column horizontally to fill the screen) | | Meta+Ctrl++ | Increase column width |
| Meta+Alt++ | Expand fully visible columns (Expands fully visible columns to fill the screen) | | Meta+Ctrl+- | Decrease column width |
| Meta+Alt+- | Shrink visible columns (Shrinks fully and partially visible columns, making them fully visible and filling the screen) | | Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) | | Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left | | Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right | | Meta+Alt+D | Scroll one column to the right |
@@ -52,7 +61,7 @@ Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) an
| Meta+Alt+PgDown | Scroll right | | Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start | | Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end | | Meta+Alt+End | Scroll to end |
| Meta+[N] | Move focus to column N | | 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+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) | | Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |
| Meta+Ctrl+Shift+F[N] | Move column to desktop N | | Meta+Ctrl+Shift+F[N] | Move column to desktop N |

View File

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

View File

@@ -1,4 +1,4 @@
interface KeyBinding { type KeyBinding = {
name: string; name: string;
description: string; description: string;
comment?: string; comment?: string;
@@ -6,7 +6,7 @@ interface KeyBinding {
action: string; action: string;
} }
interface NumKeyBinding { type NumKeyBinding = {
name: string; name: string;
description: string; description: string;
comment?: string; comment?: string;

View File

@@ -1,5 +1,6 @@
import QtQuick 2.15 import QtQuick 2.15
import org.kde.kwin 3.0 import org.kde.kwin 3.0
import org.kde.notification 1.0
import "./main.js" as Karousel import "./main.js" as Karousel
Item { Item {
@@ -9,11 +10,19 @@ Item {
Component.onCompleted: { Component.onCompleted: {
qmlBase.karouselInstance = Karousel.init(); qmlBase.karouselInstance = Karousel.init();
print("script started");
} }
Component.onDestruction: { Component.onDestruction: {
qmlBase.karouselInstance.destroy(); qmlBase.karouselInstance.destroy();
print("script stopped"); }
Notification {
id: notificationInvalidWindowRules
componentName: "plasma_workspace"
eventId: "notification"
title: "Karousel"
text: "Your Window Rules JSON is malformed, please review your Karousel configuration"
flags: Notification.Persistent
urgency: Notification.HighUrgency
} }
} }

View File

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

View File

@@ -9,7 +9,7 @@
}], }],
"Id": "karousel", "Id": "karousel",
"ServiceTypes": ["KWin/Script"], "ServiceTypes": ["KWin/Script"],
"Version": "0.2.1", "Version": "0.7.1",
"License": "GPLv3", "License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel", "Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues" "BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"

419
src/Actions.ts Normal file
View File

@@ -0,0 +1,419 @@
namespace Actions {
export function init(world: World, config: Config) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
},
focusRight: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
},
focusUp: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
},
focusDown: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
},
focusStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
},
focusEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
},
windowMoveLeft: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
window.moveToColumn(prevColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
});
},
windowMoveRight: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
window.moveToColumn(nextColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
});
},
windowMoveUp: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
},
windowMoveDown: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
},
windowMoveStart: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
},
windowMoveEnd: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
},
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
},
columnMoveLeft: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
},
columnMoveRight: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
},
columnMoveStart: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(null);
});
},
columnMoveEnd: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
});
},
columnToggleStacked: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked();
});
},
columnWidthIncrease: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
return;
}
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
const newWidth = findNextStep(
[
visibleRange.getWidth(),
column.getWidth() + config.manualResizeStep,
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
],
width => width - column.getWidth(),
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column, false);
desktop.onLayoutChanged();
desktop.autoAdjustScroll();
});
},
columnWidthDecrease: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
const visibleColumnsWidth = rightVisibleColumn.getRight() - leftVisibleColumn.getLeft();
const unusedWidth = visibleRange.getWidth() - visibleColumnsWidth;
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = findNextStep(
[
visibleRange.getWidth(),
column.getWidth() - config.manualResizeStep,
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
],
width => column.getWidth() - width,
)
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column, true);
desktop.onLayoutChanged();
desktop.autoAdjustScroll();
});
},
columnsWidthEqualize: () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
},
gridScrollLeft: () => {
gridScroll(world, -config.manualScrollStep);
},
gridScrollRight: () => {
gridScroll(world, config.manualScrollStep);
},
gridScrollStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
});
},
gridScrollEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
});
},
gridScrollFocused: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
},
gridScrollLeftColumn: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.desktop.scrollToColumn(prevColumn);
});
},
gridScrollRightColumn: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.desktop.scrollToColumn(nextColumn);
});
},
};
}
export function initNum(world: World) {
return {
focusColumn: (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
targetColumn.focus();
});
},
windowMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll();
});
},
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return null;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
});
},
columnMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
},
tailMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
},
};
}
function gridScroll(world: World, amount: number) {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
grid.desktop.adjustScroll(amount, false);
});
}
function 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;
}
export type Config = {
manualScrollStep: number,
manualResizeStep: number,
};
}

10
src/ClientAreaOption.ts Normal file
View File

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

View File

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

View File

@@ -5,8 +5,18 @@ type Config = {
gapsOuterRight: number, gapsOuterRight: number,
gapsInnerHorizontal: number, gapsInnerHorizontal: number,
gapsInnerVertical: number, gapsInnerVertical: number,
overscroll: number,
manualScrollStep: number, manualScrollStep: number,
manualResizeStep: number,
offScreenOpacity: number,
untileOnDrag: boolean,
stackColumnsByDefault: boolean, stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
scrollingLazy: boolean,
scrollingCentered: boolean,
scrollingGrouped: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string, windowRules: string,
} };

View File

@@ -7,22 +7,45 @@ const defaultWindowRules = `[
"class": "kcalc", "class": "kcalc",
"tile": false "tile": false
}, },
{
"class": "org.kde.kcalc",
"tile": false
},
{ {
"class": "kfind", "class": "kfind",
"tile": true "tile": true
}, },
{
"class": "org.kde.kfind",
"tile": true
},
{ {
"class": "kruler", "class": "kruler",
"tile": false "tile": false
}, },
{ {
"class": "zoom", "class": "org.kde.kruler",
"caption": "Zoom Cloud Meetings", "tile": false
},
{
"class": "krunner",
"tile": false
},
{
"class": "org.kde.krunner",
"tile": false
},
{
"class": "yakuake",
"tile": false
},
{
"class": "org.kde.yakuake",
"tile": false "tile": false
}, },
{ {
"class": "zoom", "class": "zoom",
"caption": "zoom", "caption": "Zoom Cloud Meetings|zoom|zoom <2>",
"tile": false "tile": false
}, },
{ {
@@ -78,21 +101,71 @@ const configDef = [
"type": "UInt", "type": "UInt",
"default": 18 "default": 18
}, },
{
"name": "overscroll",
"type": "UInt",
"default": 18
},
{ {
"name": "manualScrollStep", "name": "manualScrollStep",
"type": "UInt", "type": "UInt",
"default": 200 "default": 200
}, },
{
"name": "manualResizeStep",
"type": "UInt",
"default": 600
},
{
"name": "offScreenOpacity",
"type": "UInt",
"default": 100
},
{
"name": "untileOnDrag",
"type": "Bool",
"default": true
},
{ {
"name": "stackColumnsByDefault", "name": "stackColumnsByDefault",
"type": "Bool", "type": "Bool",
"default": false "default": false
}, },
{
"name": "resizeNeighborColumn",
"type": "Bool",
"default": false
},
{
"name": "reMaximize",
"type": "Bool",
"default": false
},
{
"name": "skipSwitcher",
"type": "Bool",
"default": false
},
{
"name": "scrollingLazy",
"type": "Bool",
"default": true
},
{
"name": "scrollingCentered",
"type": "Bool",
"default": false
},
{
"name": "scrollingGrouped",
"type": "Bool",
"default": false
},
{
"name": "tiledKeepBelow",
"type": "Bool",
"default": true
},
{
"name": "floatingKeepAbove",
"type": "Bool",
"default": false
},
{ {
"name": "windowRules", "name": "windowRules",
"type": "String", "type": "String",

View File

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

13
src/extern.d.ts vendored
View File

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

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

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

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

@@ -0,0 +1,77 @@
declare const KWin: {
// Functions
readConfig(key: string, defaultValue: any): any;
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
};
declare const workspace: {
// Read-write Properties
readonly desktops: number;
readonly currentDesktop: number;
readonly currentActivity: string;
// Read-write Properties
activeClient: KwinClient;
// Signals
currentDesktopChanged: QSignal<[oldDesktopNumber: number]>
clientAdded: QSignal<[KwinClient]>;
clientRemoved: QSignal<[KwinClient]>;
clientMinimized: QSignal<[KwinClient]>;
clientUnminimized: QSignal<[KwinClient]>;
clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
clientActivated: QSignal<[KwinClient]>;
numberDesktopsChanged: QSignal<[oldNumberOfVirtualDesktops: number]>;
currentActivityChanged: QSignal<[newActivity: string]>;
virtualScreenSizeChanged: QSignal<[void]>;
// Functions
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
clientList(): KwinClient[];
};
type Tile = any;
interface KwinClient {
// Read-only Properties
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: QmlSize;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
readonly screen: number;
readonly resourceClass: QByteArray;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
opacity: number;
// Read-write Properties
fullScreen: boolean;
activities: string[]; // empty array means all activities
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktop: number; // -1 means all desktops
tile: Tile;
// Signals
fullScreenChanged: QSignal<[void]>;
desktopChanged: QSignal<[void]>;
activitiesChanged: QSignal<[KwinClient]>;
captionChanged: QSignal<[void]>;
tileChanged: QSignal<[Tile]>;
moveResizedChanged: QSignal<[void]>;
moveResizeCursorChanged: QSignal<[void]>;
clientStartUserMovedResized: QSignal<[void]>;
frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QmlRect]>;
// Functions
setMaximize(vertically: boolean, horizontally: boolean): void;
}

42
src/extern/qt.d.ts vendored Normal file
View File

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

View File

@@ -14,18 +14,21 @@ const keyBindings: KeyBinding[] = [
{ {
"name": "focus-right", "name": "focus-right",
"description": "Move focus right", "description": "Move focus right",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+D", "defaultKeySequence": "Meta+D",
"action": "focusRight", "action": "focusRight",
}, },
{ {
"name": "focus-up", "name": "focus-up",
"description": "Move focus up", "description": "Move focus up",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+W", "defaultKeySequence": "Meta+W",
"action": "focusUp", "action": "focusUp",
}, },
{ {
"name": "focus-down", "name": "focus-down",
"description": "Move focus down", "description": "Move focus down",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+S", "defaultKeySequence": "Meta+S",
"action": "focusDown", "action": "focusDown",
}, },
@@ -80,11 +83,11 @@ const keyBindings: KeyBinding[] = [
"action": "windowMoveEnd", "action": "windowMoveEnd",
}, },
{ {
"name": "window-expand", "name": "column-toggle-stacked",
"description": "Expand window", "description": "Toggle stacked layout for focused column",
"comment": "Expands focused window vertically; toggles stacked layout for focused column", "comment": "One window in the column visible, others shaded; not supported on Wayland",
"defaultKeySequence": "Meta+X", "defaultKeySequence": "Meta+X",
"action": "windowExpand", "action": "columnToggleStacked",
}, },
{ {
"name": "column-move-left", "name": "column-move-left",
@@ -111,25 +114,22 @@ const keyBindings: KeyBinding[] = [
"action": "columnMoveEnd", "action": "columnMoveEnd",
}, },
{ {
"name": "column-expand", "name": "column-width-increase",
"description": "Expand column", "description": "Increase column width",
"comment": "Expands focused column horizontally to fill the screen", "defaultKeySequence": "Meta+Ctrl++",
"action": "columnWidthIncrease",
},
{
"name": "column-width-decrease",
"description": "Decrease column width",
"defaultKeySequence": "Meta+Ctrl+-",
"action": "columnWidthDecrease",
},
{
"name": "columns-width-equalize",
"description": "Equalize widths of visible columns",
"defaultKeySequence": "Meta+Ctrl+X", "defaultKeySequence": "Meta+Ctrl+X",
"action": "columnExpand", "action": "columnsWidthEqualize",
},
{
"name": "expand-visible-columns",
"description": "Expand fully visible columns",
"comment": "Expands fully visible columns to fill the screen",
"defaultKeySequence": "Meta+Alt++",
"action": "expandVisibleColumns",
},
{
"name": "shrink-visible-columns",
"description": "Shrink visible columns",
"comment": "Shrinks fully and partially visible columns, making them fully visible and filling the screen",
"defaultKeySequence": "Meta+Alt+-",
"action": "shrinkVisibleColumns",
}, },
{ {
"name": "grid-scroll-focused", "name": "grid-scroll-focused",
@@ -180,6 +180,7 @@ const numKeyBindings: NumKeyBinding[] = [
{ {
"name": "focus-", "name": "focus-",
"description": "Move focus to column ", "description": "Move focus to column ",
"comment": "Clashes with default KDE shortcuts, may require manual remapping",
"defaultModifiers": "Meta", "defaultModifiers": "Meta",
"fKeys": false, "fKeys": false,
"action": "focusColumn", "action": "focusColumn",

View File

@@ -1,27 +1,27 @@
interface KeyBinding { type KeyBinding = {
name: string; name: string;
description: string; description: string;
comment?: string; comment?: string;
defaultKeySequence: string; defaultKeySequence: string;
action: keyof ReturnType<typeof initActions>; action: keyof ReturnType<typeof Actions.init>;
} };
interface NumKeyBinding { type NumKeyBinding = {
name: string; name: string;
description: string; description: string;
comment?: string; comment?: string;
defaultModifiers: string; defaultModifiers: string;
fKeys: boolean; fKeys: boolean;
action: keyof ReturnType<typeof initNumActions>; action: keyof ReturnType<typeof Actions.initNum>;
} };
function catchWrap(f: () => void) { function catchWrap(f: () => void) {
return () => { return () => {
try { try {
f(); f();
} catch (error: any) { } catch (error: any) {
console.log(error); log(error);
console.log(error.stack); log(error.stack);
} }
}; };
} }
@@ -38,24 +38,27 @@ function registerKeyBinding(name: string, description: string, keySequence: stri
function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) { function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) {
const numPrefix = fKeys ? "F" : ""; const numPrefix = fKeys ? "F" : "";
const n = fKeys ? 12 : 9; const n = fKeys ? 12 : 9;
for (let i = 0; i < n; i++) { for (let i = 0; i < 12; i++) {
const numKey = String(i + 1); const numKey = String(i + 1);
const keySequence = i < n ?
modifiers + "+" + numPrefix + numKey :
"";
registerKeyBinding( registerKeyBinding(
name + numKey, name + numKey,
description + numKey, description + numKey,
modifiers + "+" + numPrefix + numKey, keySequence,
() => callback(i), () => callback(i),
); );
} }
} }
function registerKeyBindings(world: World) { function registerKeyBindings(world: World, config: Config) {
const actions = initActions(world); const actions = Actions.init(world, config);
for (const binding of keyBindings) { for (const binding of keyBindings) {
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]); registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
} }
const numActions = initNumActions(world); const numActions = Actions.initNum(world);
for (const binding of numKeyBindings) { for (const binding of numKeyBindings) {
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]); registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
} }

View File

@@ -1,24 +1,23 @@
class Column { class Column {
public grid: Grid; public grid: Grid;
public gridX: number; public gridX: number;
public width: number; // TODO: increase column width to contain transients private width: number; // TODO: increase column width to contain transients
private readonly windows: LinkedList<Window>; private readonly windows: LinkedList<Window>;
private stacked: boolean; private stacked: boolean;
private focusTaker: Window|null; private focusTaker: Window|null;
private widthBeforeExpand: number; private static readonly minWidth = 10;
constructor(grid: Grid, prevColumn: Column|null) { constructor(grid: Grid, prevColumn: Column|null) {
this.gridX = 0; this.gridX = 0;
this.width = 0; this.width = 0;
this.windows = new LinkedList(); this.windows = new LinkedList();
this.stacked = grid.world.config.stackColumnsByDefault; this.stacked = grid.config.stackColumnsByDefault;
this.focusTaker = null; this.focusTaker = null;
this.widthBeforeExpand = 0;
this.grid = grid; this.grid = grid;
this.grid.onColumnAdded(this, prevColumn); this.grid.onColumnAdded(this, prevColumn);
} }
moveToGrid(targetGrid: Grid, prevColumn: Column|null) { public moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
if (targetGrid === this.grid) { if (targetGrid === this.grid) {
this.grid.onColumnMoved(this, prevColumn); this.grid.onColumnMoved(this, prevColumn);
} else { } else {
@@ -26,60 +25,73 @@ class Column {
this.grid = targetGrid; this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn); targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.kwinClient.desktop = targetGrid.desktop; window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber;
} }
} }
} }
moveAfter(prevColumn: Column|null) { public moveAfter(prevColumn: Column|null) {
if (prevColumn === this) { if (prevColumn === this) {
return; return;
} }
this.grid.onColumnMoved(this, prevColumn); this.grid.onColumnMoved(this, prevColumn);
} }
isAfter(other: Column) { public isAfter(other: Column) {
return this.gridX > other.gridX; return this.gridX > other.gridX;
} }
isBefore(other: Column) { public isBefore(other: Column) {
return this.gridX < other.gridX; return this.gridX < other.gridX;
} }
moveWindowUp(window: Window) { public moveWindowUp(window: Window) {
this.windows.moveBack(window); this.windows.moveBack(window);
this.grid.desktop.onLayoutChanged();
} }
moveWindowDown(window: Window) { public moveWindowDown(window: Window) {
this.windows.moveForward(window); this.windows.moveForward(window);
this.grid.desktop.onLayoutChanged();
} }
getWindowCount() { public getWindowCount() {
return this.windows.length(); return this.windows.length();
} }
isEmpty() { public isEmpty() {
return this.getWindowCount() === 0; return this.getWindowCount() === 0;
} }
getPrevWindow(window: Window) { public getPrevWindow(window: Window) {
return this.windows.getPrev(window); return this.windows.getPrev(window);
} }
getNextWindow(window: Window) { public getNextWindow(window: Window) {
return this.windows.getNext(window); return this.windows.getNext(window);
} }
getWidth() { public getWidth() {
return this.width; return this.width;
} }
getMaxWidth() { public getMinWidth() {
return this.grid.tilingArea.width; let maxMinWidth = Column.minWidth;
for (const window of this.windows.iterator()) {
const minWidth = window.client.kwinClient.minSize.width;
if (minWidth > maxMinWidth) {
maxMinWidth = minWidth;
}
}
return maxMinWidth;
} }
setWidth(width: number, setPreferred: boolean) { public getMaxWidth() {
width = Math.min(width, this.getMaxWidth()); return this.grid.desktop.tilingArea.width;
}
public setWidth(width: number, setPreferred: boolean) {
width = clamp(width, this.getMinWidth(), this.getMaxWidth());
const oldWidth = this.width; const oldWidth = this.width;
this.width = width; this.width = width;
if (setPreferred) { if (setPreferred) {
@@ -92,22 +104,34 @@ class Column {
} }
} }
adjustWidth(widthDelta: number, setPreferred: boolean) { public adjustWidth(widthDelta: number, setPreferred: boolean) {
this.setWidth(this.width + widthDelta, setPreferred); this.setWidth(this.width + widthDelta, setPreferred);
} }
expand() { public updateWidth() {
const maxWidth = this.getMaxWidth(); let minErr = Infinity;
const isAlreadyExpanded = this.width === maxWidth && this.widthBeforeExpand > 0; let closestPreferredWidth = this.width;
if (isAlreadyExpanded) { for (const window of this.windows.iterator()) {
this.setWidth(this.widthBeforeExpand, false); const err = Math.abs(window.client.preferredWidth - this.width);
} else { if (err < minErr) {
this.widthBeforeExpand = this.width; minErr = err;
this.setWidth(maxWidth, false); closestPreferredWidth = window.client.preferredWidth;
}
} }
this.setWidth(closestPreferredWidth, false);
} }
adjustWindowHeight(window: Window, heightDelta: number, top: boolean) { // returns x position of left edge in grid space
public getLeft() {
return this.gridX;
}
// returns x position of right edge in grid space
public getRight() {
return this.gridX + this.width;
}
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window); const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
if (otherWindow === null) { if (otherWindow === null) {
return; return;
@@ -115,18 +139,20 @@ class Column {
window.height += heightDelta; window.height += heightDelta;
otherWindow.height -= heightDelta; otherWindow.height -= heightDelta;
this.grid.desktop.onLayoutChanged();
} }
resizeWindows() { public resizeWindows() {
const nWindows = this.windows.length(); const nWindows = this.windows.length();
if (nWindows === 0) { if (nWindows === 0) {
return; return;
} }
if (nWindows === 1) { if (nWindows === 1) {
this.stacked = this.grid.world.config.stackColumnsByDefault; this.stacked = this.grid.config.stackColumnsByDefault;
} }
let remainingPixels = this.grid.tilingArea.height - (nWindows-1) * this.grid.world.config.gapsInnerVertical; let remainingPixels = this.grid.desktop.tilingArea.height - (nWindows-1) * this.grid.config.gapsInnerVertical;
let remainingWindows = nWindows; let remainingWindows = nWindows;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
const windowHeight = Math.round(remainingPixels / remainingWindows); const windowHeight = Math.round(remainingPixels / remainingWindows);
@@ -135,16 +161,18 @@ class Column {
remainingWindows--; remainingWindows--;
} }
// TODO: respect min height // TODO: respect min height
this.grid.desktop.onLayoutChanged();
} }
getFocusTaker() { public getFocusTaker() {
if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) { if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) {
return null; return null;
} }
return this.focusTaker; return this.focusTaker;
} }
focus() { public focus() {
const window = this.getFocusTaker() ?? this.windows.getFirst(); const window = this.getFocusTaker() ?? this.windows.getFirst();
if (window === null) { if (window === null) {
return; return;
@@ -152,20 +180,27 @@ class Column {
window.focus(); window.focus();
} }
arrange(x: number) { public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.stacked && this.windows.length() >= 2) { if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
}
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x); this.arrangeStacked(x);
return; return;
} }
let y = this.grid.tilingArea.y; let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.setShade(false); window.client.setShade(false);
window.arrange(x, y, this.width, window.height); window.arrange(x, y, this.width, window.height);
y += window.height + this.grid.world.config.gapsInnerVertical; y += window.height + this.grid.config.gapsInnerVertical;
} }
} }
arrangeStacked(x: number) { public arrangeStacked(x: number) {
const expandedWindow = this.getFocusTaker(); const expandedWindow = this.getFocusTaker();
let collapsedHeight; let collapsedHeight;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
@@ -178,28 +213,48 @@ class Column {
} }
const nCollapsed = this.getWindowCount() - 1; const nCollapsed = this.getWindowCount() - 1;
const expandedHeight = this.grid.tilingArea.height - nCollapsed * (collapsedHeight + this.grid.world.config.gapsInnerVertical); const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
let y = this.grid.tilingArea.y; let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
if (window === expandedWindow) { if (window === expandedWindow) {
window.arrange(x, y, this.width, expandedHeight); window.arrange(x, y, this.width, expandedHeight);
y += expandedHeight; y += expandedHeight;
} else { } else {
window.arrange(x, y, this.width, window.height); window.arrange(x, y, this.width, window.height);
y += collapsedHeight; y += collapsedHeight!;
} }
y += this.grid.world.config.gapsInnerVertical; y += this.grid.config.gapsInnerVertical;
} }
} }
toggleStacked() { public toggleStacked() {
if (this.windows.length() < 2) { if (this.windows.length() < 2) {
return; return;
} }
this.stacked = !this.stacked; this.stacked = !this.stacked;
this.grid.desktop.onLayoutChanged();
} }
onWindowAdded(window: Window) { private canStack() {
for (const window of this.windows.iterator()) {
if (!window.client.kwinClient.shadeable) {
return false;
}
}
return true;
}
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
} else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
}
}
public onWindowAdded(window: Window) {
this.windows.insertEnd(window); this.windows.insertEnd(window);
if (this.width === 0) { if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false); this.setWidth(window.client.preferredWidth, false);
@@ -211,9 +266,11 @@ class Column {
if (window.isFocused()) { if (window.isFocused()) {
this.onWindowFocused(window); this.onWindowFocused(window);
} }
this.grid.desktop.onLayoutChanged();
} }
onWindowRemoved(window: Window, passFocus: boolean) { public onWindowRemoved(window: Window, passFocus: boolean) {
const lastWindow = this.windows.length() === 1; const lastWindow = this.windows.length() === 1;
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window); const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
@@ -232,21 +289,23 @@ class Column {
windowToFocus.focus(); windowToFocus.focus();
} }
} }
this.grid.desktop.onLayoutChanged();
} }
onWindowFocused(window: Window) { public onWindowFocused(window: Window) {
this.grid.onColumnFocused(this); this.grid.onColumnFocused(this);
this.focusTaker = window; this.focusTaker = window;
} }
restoreToTiled() { public restoreToTiled() {
const lastFocusedWindow = this.getFocusTaker(); const lastFocusedWindow = this.getFocusTaker();
if (lastFocusedWindow !== null) { if (lastFocusedWindow !== null) {
lastFocusedWindow.restoreToTiled(); lastFocusedWindow.restoreToTiled();
} }
} }
destroy(passFocus: boolean) { private destroy(passFocus: boolean) {
this.grid.onColumnRemoved(this, passFocus); this.grid.onColumnRemoved(this, passFocus);
} }
} }

311
src/layout/Desktop.ts Normal file
View File

@@ -0,0 +1,311 @@
class Desktop {
public readonly grid: Grid;
public readonly desktopNumber: number;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.desktopNumber = desktopNumber;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
private static getClientArea(desktopNumber: number) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
}
private static getTilingArea(clientArea: QmlRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
left,
top,
right - left,
bottom - top,
)
}
public scrollIntoView(range: Desktop.Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
let targetScrollX: number;
if (left < initialVisibleRange.getLeft()) {
targetScrollX = left;
} else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width;
} else {
targetScrollX = initialVisibleRange.getLeft();
}
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Desktop.Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public scrollCenterVisible(focusedColumn: Column, prioritiseVisible: boolean) {
const columnRange = new Desktop.ColumnRange(focusedColumn);
const visibleRange = this.getCurrentVisibleRange();
if (prioritiseVisible) {
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal, column => column.isVisible(visibleRange, true));
}
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal, () => true);
this.scrollCenterRange(columnRange);
}
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null || focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(focusedColumn);
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
return this.getVisibleRange(this.scrollX);
}
private clampScrollX(x: number) {
return this.config.scroller.clampScrollX(this, x);
}
public setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
this.dirtyScroll = false;
}
public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
public equalizeVisibleColumnsWidths() {
const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
for (const minWidth of minWidths) {
if (minWidth > remainingWidth / remainingColumns) {
remainingWidth -= minWidth;
remainingColumns--;
}
}
const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
this.dirty = false;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}
}
namespace Desktop {
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
scroller: Desktop.Scroller,
};
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Desktop.Range, gap: number, condition: (column: Column) => boolean) {
const grid = this.left.grid;
const columnRange = this;
function canFit(column: Column) {
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth()
}
function isUsable(column: Column|null) {
return column !== null &&
canFit(column) &&
condition(column)
}
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
}
if (!isUsable(rightColumn)) {
rightColumn = null;
}
}
checkColumns();
while (leftColumn !== null || rightColumn !== null) {
const leftWidth = leftColumn === null ? -Infinity : leftColumn.getWidth();
const rightWidth = rightColumn === null ? -Infinity : rightColumn.getWidth();
if (leftWidth > rightWidth) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getPrevColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getNextColumn(rightColumn!);
}
checkColumns();
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
clampScrollX(desktop: Desktop, x: number): number;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
class ScrollerGrouped {
public scrollToColumn(desktop: Desktop, column: Column) {
desktop.scrollCenterVisible(column, true);
}
public clampScrollX(desktop: Desktop, x: number) {
return ScrollerCentered.clampScrollX(desktop, x);
}
}

View File

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

View File

@@ -2,7 +2,7 @@ class Window {
public column: Column; public column: Column;
public readonly client: ClientWrapper; public readonly client: ClientWrapper;
public height: number; public height: number;
public readonly focusedState: WindowState; public readonly focusedState: Window.State;
private skipArrange: boolean; private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) { constructor(client: ClientWrapper, column: Column) {
@@ -18,7 +18,7 @@ class Window {
column.onWindowAdded(this); column.onWindowAdded(this);
} }
moveToColumn(targetColumn: Column) { public moveToColumn(targetColumn: Column) {
if (targetColumn === this.column) { if (targetColumn === this.column) {
return; return;
} }
@@ -27,21 +27,31 @@ class Window {
targetColumn.onWindowAdded(this); targetColumn.onWindowAdded(this);
} }
arrange(x: number, y: number, width: number, height: number) { public arrange(x: number, y: number, width: number, height: number) {
if (this.skipArrange) { if (this.skipArrange) {
// window is being manually resized, prevent fighting with the user // window is maximized, fullscreen, or being manually resized, prevent fighting with the user
return; return;
} }
this.client.place(x, y, width, height);
if (this.isFocused()) { let maximized = false;
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement // do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all) // (otherwise placement may not happen at all)
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally); if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
this.client.setFullScreen(this.focusedState.fullScreen); this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
maximized = true;
}
if (this.focusedState.fullScreen) {
this.client.setFullScreen(true);
maximized = true;
}
}
if (!maximized) {
this.client.place(x, y, width, height);
} }
} }
focus() { public focus() {
if (this.client.isShaded()) { if (this.client.isShaded()) {
// workaround for KWin deactivating clients when unshading immediately after activation // workaround for KWin deactivating clients when unshading immediately after activation
this.client.setShade(false); this.client.setShade(false);
@@ -49,67 +59,93 @@ class Window {
this.client.focus(); this.client.focus();
} }
isFocused() { public isFocused() {
return this.client.isFocused(); return this.client.isFocused();
} }
onFocused() { public onFocused() {
this.column.onWindowFocused(this); this.column.onWindowFocused(this);
} }
restoreToTiled() { public restoreToTiled() {
if (this.isFocused()) { if (this.isFocused()) {
return; return;
} }
this.client.setMaximize(false, false);
this.client.setFullScreen(false); this.client.setFullScreen(false);
this.client.setMaximize(false, false);
this.column.grid.desktop.onLayoutChanged();
} }
onMaximizedChanged(horizontally: boolean, vertically: boolean) { public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
const maximized = horizontally || vertically; const maximized = horizontally || vertically;
this.skipArrange = maximized; this.skipArrange = maximized;
this.client.kwinClient.keepBelow = !maximized; if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = maximized;
}
if (this.isFocused()) { if (this.isFocused()) {
this.focusedState.maximizedHorizontally = horizontally; this.focusedState.maximizedHorizontally = horizontally;
this.focusedState.maximizedVertically = vertically; this.focusedState.maximizedVertically = vertically;
} }
this.column.grid.desktop.onLayoutChanged();
} }
onFullScreenChanged(fullScreen: boolean) { public onFullScreenChanged(fullScreen: boolean) {
this.skipArrange = fullScreen; this.skipArrange = fullScreen;
if (this.isFocused()) { if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !fullScreen; this.client.kwinClient.keepBelow = !fullScreen;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = fullScreen;
}
if (this.isFocused()) {
this.focusedState.fullScreen = fullScreen; this.focusedState.fullScreen = fullScreen;
} }
this.column.grid.desktop.onLayoutChanged();
} }
onUserResize(oldGeometry: QRect) { public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
const newGeometry = this.client.kwinClient.frameGeometry; const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width; const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height; const heightDelta = newGeometry.height - oldGeometry.height;
if (widthDelta !== 0) { if (widthDelta !== 0) {
this.column.adjustWidth(widthDelta, true); this.column.adjustWidth(widthDelta, true);
if (newGeometry.x !== oldGeometry.x) { let leftEdgeDelta = newGeometry.left - oldGeometry.left;
this.column.grid.adjustScroll(widthDelta, true); 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) { if (heightDelta !== 0) {
this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y); this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y);
} }
} }
onProgrammaticResize(oldGeometry: QRect) { public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry; const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true); this.column.setWidth(newGeometry.width, true);
this.column.grid.desktop.onLayoutChanged();
} }
destroy(passFocus: boolean) { public destroy(passFocus: boolean) {
this.column.onWindowRemoved(this, passFocus); this.column.onWindowRemoved(this, passFocus);
} }
} }
type WindowState = { namespace Window {
fullScreen: boolean, export type State = {
maximizedHorizontally: boolean, fullScreen: boolean,
maximizedVertically: boolean, maximizedHorizontally: boolean,
maximizedVertically: boolean,
}
} }

View File

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

View File

@@ -5,8 +5,8 @@ class ClientMatcher {
this.rules = rules; this.rules = rules;
} }
matches(kwinClient: AbstractClient) { public matches(kwinClient: KwinClient) {
const rule = this.rules.get(String(kwinClient.resourceClass)); const rule = this.rules.get(kwinClient.resourceClass);
if (rule === undefined) { if (rule === undefined) {
return false; return false;
} }

View File

@@ -2,4 +2,4 @@ type WindowRule = {
class: string, class: string,
caption: string, caption: string,
tile: boolean, tile: boolean,
} };

View File

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

View File

@@ -1,5 +1,5 @@
class Delayer { class Delayer {
private readonly timer: QQmlTimer; private readonly timer: QmlTimer;
constructor(delay: number, f: () => void) { constructor(delay: number, f: () => void) {
this.timer = initQmlTimer(); this.timer = initQmlTimer();
@@ -7,11 +7,11 @@ class Delayer {
this.timer.triggered.connect(f); this.timer.triggered.connect(f);
} }
run() { public run() {
this.timer.restart(); this.timer.restart();
} }
destroy() { public destroy() {
this.timer.destroy(); this.timer.destroy();
} }
} }

View File

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

View File

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

View File

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

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

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

View File

@@ -8,9 +8,7 @@ function clamp(value: number, min: number, max: number) {
return value; return value;
} }
function rectEqual(a: QRect, b: QRect) { function union<T>(array0: T[], array1: T[]) {
return a.x === b.x && const set = new Set([...array0, ...array1]);
a.y === b.y && return [...set];
a.width === b.width &&
a.height === b.height;
} }

View File

@@ -1,9 +1,8 @@
function initWorkspaceSignalHandlers(world: World) { function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: AbstractClient) => { manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => {
console.assert(!world.hasClient(kwinClient)); if (Clients.canTileEver(kwinClient)) {
if (canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities // never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) { if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop; kwinClient.desktop = workspace.currentDesktop;
@@ -12,48 +11,56 @@ function initWorkspaceSignalHandlers(world: World) {
kwinClient.activities = [workspace.currentActivity]; kwinClient.activities = [workspace.currentActivity];
} }
} }
world.addClient(kwinClient); world.do((clientManager, desktopManager) => {
}); clientManager.addClient(kwinClient)
manager.connect(workspace.clientRemoved, (kwinClient: AbstractClient) => {
console.assert(world.hasClient(kwinClient));
world.removeClient(kwinClient, true);
});
manager.connect(workspace.clientMinimized, (kwinClient: AbstractClient) => {
world.minimizeClient(kwinClient);
});
manager.connect(workspace.clientUnminimized, (kwinClient: AbstractClient) => {
world.unminimizeClient(kwinClient);
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: AbstractClient, horizontally: boolean, vertically: boolean) => {
world.doIfTiled(kwinClient, false, (window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
grid.arrange();
}); });
}); });
manager.connect(workspace.clientActivated, (kwinClient: AbstractClient) => { manager.connect(workspace.clientRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(workspace.clientMinimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: KwinClient, horizontally: boolean, vertically: boolean) => {
if ((horizontally || vertically) && kwinClient.tile !== null) {
kwinClient.tile = null;
}
world.doIfTiled(kwinClient, false, (clientManager, desktopManager, window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
});
});
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) { if (kwinClient === null) {
return; return;
} }
world.onClientFocused(kwinClient); world.do((clientManager, desktopManager) => {
world.doIfTiled(kwinClient, true, (window, column, grid) => { clientManager.onClientFocused(kwinClient);
window.onFocused();
grid.arrange();
}); });
}); });
manager.connect(workspace.clientFullScreenSet, (kwinClient: X11Client, fullScreen: boolean, user: boolean) => { manager.connect(workspace.currentDesktopChanged, () => {
world.doIfTiled(kwinClient, false, (window, column, grid) => { world.do(() => {}); // re-arrange desktop
window.onFullScreenChanged(fullScreen);
grid.arrange();
});
}); });
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfDesktops: number) => { manager.connect(workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfVirtualDesktops: number) => {
world.updateDesktops(); world.updateDesktops();
}); });

204
src/world/ClientManager.ts Normal file
View File

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

View File

@@ -1,23 +0,0 @@
class ClientStateDocked {
private readonly world: World;
private readonly signalManager: SignalManager;
constructor(world: World, kwinClient: AbstractClient) {
this.world = world;
this.signalManager = ClientStateDocked.initSignalManager(world, kwinClient);
world.onScreenResized();
}
destroy(passFocus: boolean) {
this.signalManager.destroy();
this.world.onScreenResized();
}
private static initSignalManager(world: World, kwinClient: AbstractClient) {
const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
world.onScreenResized();
});
return manager;
}
}

View File

@@ -1,30 +0,0 @@
class ClientStateManager {
private state: ClientState;
constructor(initialState: ClientState) {
this.state = initialState;
}
setState(newState: ClientState, passFocus: boolean) {
this.state.destroy(passFocus);
this.state = newState;
}
getState() {
return this.state;
}
destroy(passFocus: boolean) {
this.state.destroy(passFocus);
}
}
type ClientState = ClientStateTiled | ClientStateTiledMinimized | ClientStateFloating | ClientStateDocked;
class ClientStateTiledMinimized {
destroy(passFocus: boolean) {}
}
class ClientStateFloating {
destroy(passFocus: boolean) {}
}

View File

@@ -1,103 +0,0 @@
class ClientStateTiled {
readonly window: Window;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper) {
client.prepareForTiling();
const grid = world.getClientGrid(client.kwinClient);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column);
grid.arrange();
this.window = window;
this.signalManager = ClientStateTiled.initSignalManager(world, window);
}
destroy(passFocus: boolean) {
this.signalManager.destroy();
const window = this.window;
const grid = window.column.grid;
const clientWrapper = window.client;
window.destroy(passFocus);
grid.arrange();
clientWrapper.prepareForFloating(grid.clientArea);
}
static initSignalManager(world: World, window: Window) {
const client = window.client;
const kwinClient = client.kwinClient;
const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => {
if (kwinClient.desktop === -1) {
// windows on all desktops are not supported
world.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(world, window);
});
manager.connect(kwinClient.activitiesChanged, (kwinClient: AbstractClient) => {
if (kwinClient.activities.length !== 1) {
// windows on multiple activities are not supported
world.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(world, window);
})
let lastResize = false;
manager.connect(kwinClient.moveResizedChanged, () => {
if (kwinClient.move) {
world.untileClient(kwinClient);
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
console.assert(!kwinClient.move, "moved clients are removed in kwinClient.moveResizedChanged");
const grid = window.column.grid;
if (kwinClient.resize) {
window.onUserResize(oldGeometry);
grid.arrange();
} else {
const maximized = rectEqual(kwinClient.frameGeometry, grid.clientArea);
if (!client.isManipulatingGeometry() && !kwinClient.fullScreen && !maximized) {
window.onProgrammaticResize(oldGeometry);
grid.arrange();
}
}
});
return manager;
}
static moveWindowToCorrectGrid(world: World, window: Window) {
const kwinClient = window.client.kwinClient;
const oldGrid = window.column.grid;
const newGrid = world.getClientGrid(kwinClient);
if (oldGrid === newGrid) {
// window already on the correct grid
return;
}
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
window.moveToColumn(newColumn);
oldGrid.arrange();
newGrid.arrange();
}
}

View File

@@ -1,115 +1,104 @@
class ClientWrapper { class ClientWrapper {
public readonly kwinClient: AbstractClient; public readonly kwinClient: KwinClient;
public readonly stateManager: ClientStateManager; public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null; public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[]; private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null; private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number; public preferredWidth: number;
private readonly manipulatingGeometry: Doer; private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor( constructor(
kwinClient: AbstractClient, kwinClient: KwinClient,
initialState: ClientState, constructInitialState: (client: ClientWrapper) => ClientState.State,
transientFor: ClientWrapper | null, transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null, rulesSignalManager: SignalManager | null,
) { ) {
this.kwinClient = kwinClient; this.kwinClient = kwinClient;
this.stateManager = new ClientStateManager(initialState);
this.transientFor = transientFor; this.transientFor = transientFor;
this.transients = []; this.transients = [];
if (transientFor !== null) { if (transientFor !== null) {
transientFor.addTransient(this); transientFor.addTransient(this);
} }
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager; this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width; this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer(); this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
} }
place(x: number, y: number, width: number, height: number) { public place(x: number, y: number, width: number, height: number) {
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
if (this.kwinClient.resize) { if (this.kwinClient.resize) {
// window is being manually resized, prevent fighting with the user // window is being manually resized, prevent fighting with the user
return; return;
} }
this.kwinClient.frameGeometry = Qt.rect(x, y, width, height); this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement;
}); });
} }
private moveTransient(dx: number, dy: number) { private moveTransient(dx: number, dy: number, desktopNumber: number) {
// TODO: prevent moving off the grid if (this.stateManager.getState() instanceof ClientState.Floating) {
if (this.stateManager.getState() instanceof ClientStateFloating) { if (this.kwinClient.desktop === desktopNumber) {
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect( this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx, frame.x + dx,
frame.y + dy, frame.y + dy,
frame.width, frame.width,
frame.height, frame.height,
); );
}
for (const transient of this.transients) { for (const transient of this.transients) {
transient.moveTransient(dx, dy); transient.moveTransient(dx, dy, desktopNumber);
} }
} }
} }
focus() { public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktop);
}
}
public focus() {
workspace.activeClient = this.kwinClient; workspace.activeClient = this.kwinClient;
} }
isFocused() { public isFocused() {
return workspace.activeClient === this.kwinClient; return workspace.activeClient === this.kwinClient;
} }
setMaximize(horizontally: boolean, vertically: boolean) { public setMaximize(horizontally: boolean, vertically: boolean) {
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.setMaximize(vertically, horizontally); this.kwinClient.setMaximize(vertically, horizontally);
}); });
} }
setFullScreen(fullScreen: boolean) { public setFullScreen(fullScreen: boolean) {
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen; this.kwinClient.fullScreen = fullScreen;
}); });
} }
setShade(shade: boolean) { public setShade(shade: boolean) {
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.shade = shade; this.kwinClient.shade = shade;
}); });
} }
isShaded() { public isShaded() {
return this.kwinClient.shade; return this.kwinClient.shade;
} }
isManipulatingGeometry() { public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
return this.manipulatingGeometry.isDoing(); return this.manipulatingGeometry.isDoing();
} }
prepareForTiling() {
this.kwinClient.keepBelow = true;
this.setFullScreen(false);
this.setMaximize(false, false);
}
prepareForFloating(screenSize: QRect) {
this.kwinClient.keepBelow = false;
this.setShade(false);
this.setFullScreen(false);
this.setMaximize(false, false);
const clientRect = this.kwinClient.frameGeometry;
const width = this.preferredWidth;
this.place(
clamp(clientRect.x, screenSize.left, screenSize.right - width),
clientRect.y,
width,
Math.min(clientRect.height, Math.round(screenSize.height / 2)),
);
}
private addTransient(transient: ClientWrapper) { private addTransient(transient: ClientWrapper) {
this.transients.push(transient); this.transients.push(transient);
} }
@@ -119,9 +108,29 @@ class ClientWrapper {
this.transients.splice(i, 1); this.transients.splice(i, 1);
} }
destroy(passFocus: boolean) { public ensureTransientsVisible(screenSize: QmlRect) {
for (const transient of this.transients) {
if (transient.stateManager.getState() instanceof ClientState.Floating) {
transient.ensureVisible(screenSize);
transient.ensureTransientsVisible(screenSize);
}
}
}
public ensureVisible(screenSize: QmlRect) {
if (this.kwinClient.desktop !== workspace.currentDesktop) {
return;
}
const frame = this.kwinClient.frameGeometry;
if (frame.left < screenSize.left) {
frame.x = screenSize.left;
} else if (frame.right > screenSize.right) {
frame.x = screenSize.right - frame.width;
}
}
public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus); this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) { if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy(); this.rulesSignalManager.destroy();
} }
@@ -132,23 +141,4 @@ class ClientWrapper {
transient.transientFor = null; transient.transientFor = null;
} }
} }
static initSignalManager(client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
if (client.stateManager.getState() instanceof ClientStateTiled) {
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
const newCenterY = newGeometry.y + newGeometry.height/2;
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
for (const transient of client.transients) {
transient.moveTransient(dx, dy);
}
}
});
return manager;
}
} }

35
src/world/Clients.ts Normal file
View File

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

147
src/world/DesktopManager.ts Normal file
View File

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

View File

@@ -1,81 +0,0 @@
class GridManager {
private readonly world: World;
private readonly gridsPerActivity: Map<string, Grid[]>;
private nDesktops: number;
constructor(world: World, currentActivity: string, nDesktops: number) {
this.world = world;
this.gridsPerActivity = new Map();
this.nDesktops = 0;
this.setNDesktops(nDesktops);
this.addActivity(currentActivity);
}
get(activity: string, desktopNumber: number) {
const desktopIndex = desktopNumber - 1;
if (desktopIndex >= this.nDesktops || this.nDesktops < 0) {
throw new Error("invalid desktop number: " + String(desktopNumber));
}
if (!this.gridsPerActivity.has(activity)) {
this.addActivity(activity);
}
return this.gridsPerActivity.get(activity)![desktopIndex];
}
setNDesktops(nDesktops: number) {
if (nDesktops > this.nDesktops) {
this.addDesktopsToActivities(nDesktops - this.nDesktops);
} else if (nDesktops < this.nDesktops) {
this.removeDesktopsFromActivities(this.nDesktops - nDesktops);
}
this.nDesktops = nDesktops;
}
private addDesktopsToActivities(n: number) {
for (const grids of this.gridsPerActivity.values()) {
this.addDesktops(grids, n);
}
}
private addDesktops(grids: Grid[], n: number) {
const nStart = grids.length;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
grids.push(new Grid(this.world, desktopNumber));
}
}
private removeDesktopsFromActivities(n: number) {
const lastRemainingDesktopIndex = this.nDesktops - n - 1;
for (const grids of this.gridsPerActivity.values()) {
const targetGrid = grids[lastRemainingDesktopIndex];
for (let i = 0; i < n; i++) {
const removedGrid = grids.pop()!;
removedGrid.evacuate(targetGrid);
}
}
}
addActivity(activity: string) {
const grids: Grid[] = [];
this.addDesktops(grids, this.nDesktops);
this.gridsPerActivity.set(activity, grids);
}
removeActivity(activity: string) {
const removedGrids = this.gridsPerActivity.get(activity)!;
this.gridsPerActivity.delete(activity);
const targetActivityGrids = this.gridsPerActivity.values().next().value;
for (let i = 0; i < removedGrids.length; i++) {
removedGrids[i].evacuate(targetActivityGrids[i]);
}
}
*grids() {
for (const grids of this.gridsPerActivity.values()) {
for (const grid of grids) {
yield grid;
}
}
}
}

86
src/world/PinManager.ts Normal file
View File

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

View File

@@ -1,226 +1,108 @@
class World { class World {
public readonly config: Config; public readonly untileOnDrag: boolean;
private readonly gridManager: GridManager; private readonly desktopManager: DesktopManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>; public readonly clientManager: ClientManager;
private lastFocusedClient: AbstractClient|null; private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager; private readonly workspaceSignalManager: SignalManager;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
private readonly screenResizedDelayer: Delayer; private readonly screenResizedDelayer: Delayer;
constructor(config: Config) { constructor(config: Config) {
this.config = config; this.untileOnDrag = config.untileOnDrag;
this.clientMap = new Map();
this.lastFocusedClient = null;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this); this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
let parsedWindowRules: WindowRule[] = [];
try {
parsedWindowRules = JSON.parse(config.windowRules);
} catch (error: any) {
console.log("failed to parse windowRules:", error);
}
this.windowRuleEnforcer = new WindowRuleEnforcer(this, parsedWindowRules);
this.screenResizedDelayer = new Delayer(1000, () => { this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks get taken into account by `workspace.clientArea` // this delay ensures that docks get taken into account by `workspace.clientArea`
const gridManager = this.gridManager; // workaround for bug in Qt5's JS engine const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine
for (const grid of gridManager.grids()) { for (const desktop of desktopManager.desktops()) {
grid.arrange(); desktop.onLayoutChanged();
} }
this.update();
}); });
this.gridManager = new GridManager(this, workspace.currentActivity, workspace.desktops); this.pinManager = new PinManager();
this.addExistingClients();
}
updateDesktops() { const layoutConfig = {
this.gridManager.setNDesktops(workspace.desktops); gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
};
this.desktopManager = new DesktopManager(
this.pinManager,
{
marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight,
scroller: config.scrollingLazy ? new ScrollerLazy() :
config.scrollingCentered ? new ScrollerCentered() :
config.scrollingGrouped ? new ScrollerGrouped() :
console.assert(false),
},
layoutConfig,
workspace.currentActivity,
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
this.update();
} }
private addExistingClients() { private addExistingClients() {
const kwinClients = workspace.clientList(); const kwinClients = workspace.clientList();
for (let i = 0; i < kwinClients.length; i++) { for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i]; const kwinClient = kwinClients[i];
this.addClient(kwinClient); this.clientManager.addClient(kwinClient);
} }
} }
getGrid(activity: string, desktopNumber: number) { public updateDesktops() {
console.assert(desktopNumber > 0 && desktopNumber <= workspace.desktops); this.desktopManager.update();
return this.gridManager.get(activity, desktopNumber);
} }
getGridInCurrentActivity(desktopNumber: number) { private update() {
return this.getGrid(workspace.currentActivity, desktopNumber); this.desktopManager.getCurrentDesktop().arrange();
} }
getCurrentGrid() { public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
return this.getGrid(workspace.currentActivity, workspace.currentDesktop); f(this.clientManager, this.desktopManager);
this.update();
} }
getClientGrid(kwinClient: AbstractClient) { public doIfTiled(
console.assert(kwinClient.activities.length === 1); kwinClient: KwinClient,
return this.getGrid(kwinClient.activities[0], kwinClient.desktop); followTransient: boolean,
} f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
addClient(kwinClient: AbstractClient) { const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
const client = new ClientWrapper( if (window === null) {
kwinClient,
new ClientStateFloating(),
this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this, kwinClient),
);
this.clientMap.set(kwinClient, client);
if (kwinClient.dock) {
client.stateManager.setState(new ClientStateDocked(this, kwinClient), false);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
}
removeClient(kwinClient: AbstractClient, passFocus: boolean) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return; return;
} }
client.destroy(passFocus && kwinClient === this.lastFocusedClient); const column = window.column;
this.clientMap.delete(kwinClient); const grid = column.grid;
f(this.clientManager, this.desktopManager, window, column, grid);
this.update();
} }
findTransientFor(kwinClient: AbstractClient) { public doIfTiledFocused(
if (!kwinClient.transient) { followTransient: boolean,
return null; f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
} ) {
const transientFor = this.clientMap.get(kwinClient.transientFor);
if (transientFor === undefined) {
return null;
}
return transientFor;
}
minimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateTiledMinimized(), kwinClient === this.lastFocusedClient);
}
}
unminimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiledMinimized) {
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
}
tileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
return;
}
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
untileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
toggleFloatingClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateFloating && canTileEver(kwinClient)) {
makeTileable(kwinClient);
client.stateManager.setState(new ClientStateTiled(this, client), false);
} else if (clientState instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
hasClient(kwinClient: AbstractClient) {
return this.clientMap.has(kwinClient);
}
onClientFocused(kwinClient: AbstractClient) {
this.lastFocusedClient = kwinClient;
}
private doIfTiledInner(client: ClientWrapper, followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateTiled) {
const window = clientState.window;
const column = window.column;
const grid = column.grid;
f(window, column, grid);
} else if (followTransient && client.transientFor !== null) {
this.doIfTiledInner(client.transientFor, true, f);
}
}
doIfTiled(kwinClient: AbstractClient, followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
this.doIfTiledInner(client, followTransient, f);
}
doIfTiledFocused(followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
this.doIfTiled(workspace.activeClient, followTransient, f); this.doIfTiled(workspace.activeClient, followTransient, f);
} }
getFocusedWindow() { public destroy() {
const activeClient = workspace.activeClient;
if (activeClient === null) {
return null;
}
const client = this.clientMap.get(activeClient);
if (client === undefined) {
return null;
}
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateTiled) {
return clientState.window;
} else {
return null;
}
}
removeAllClients() {
for (const kwinClient of Array.from(this.clientMap.keys())) {
this.removeClient(kwinClient, false);
}
}
destroy() {
this.workspaceSignalManager.destroy(); this.workspaceSignalManager.destroy();
this.removeAllClients(); this.clientManager.destroy();
for (const grid of this.gridManager.grids()) { this.desktopManager.destroy();
grid.destroy();
}
} }
onScreenResized() { public onScreenResized() {
this.screenResizedDelayer.run(); this.screenResizedDelayer.run();
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,182 @@
namespace ClientState {
export class Tiled implements State {
public readonly window: Window;
private readonly defaultState: Tiled.WindowState;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, grid: Grid) {
this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
Tiled.prepareClientForTiling(client, grid.config);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column);
this.window = window;
this.signalManager = Tiled.initSignalManager(world, window);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
const window = this.window;
const grid = window.column.grid;
const client = window.client;
window.destroy(passFocus);
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window) {
const client = window.client;
const kwinClient = client.kwinClient;
const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.desktop === -1) {
// windows on all desktops are not supported
clientManager.untileClient(kwinClient);
return;
}
Tiled.moveWindowToCorrectGrid(desktopManager, window);
});
});
manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.activities.length !== 1) {
// windows on multiple activities are not supported
clientManager.untileClient(kwinClient);
return;
}
Tiled.moveWindowToCorrectGrid(desktopManager, window);
});
})
let lastResize = false;
manager.connect(kwinClient.moveResizedChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.move) {
if (world.untileOnDrag) {
clientManager.untileClient(kwinClient);
}
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
});
});
let cursorChangedAfterResizeStart = false;
manager.connect(kwinClient.moveResizeCursorChanged, () => {
cursorChangedAfterResizeStart = true;
});
manager.connect(kwinClient.clientStartUserMovedResized, () => {
cursorChangedAfterResizeStart = false;
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
return;
}
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
const newCenterY = newGeometry.y + newGeometry.height/2;
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
if (dx !== 0 || dy !== 0) {
client.moveTransients(dx, dy);
}
if (kwinClient.resize) {
world.do(() => window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart));
} else if (
!client.isManipulatingGeometry(newGeometry) &&
!Clients.isMaximizedGeometry(kwinClient) &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
world.do(() => window.onFrameGeometryChanged());
}
});
manager.connect(kwinClient.fullScreenChanged, () => {
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
});
manager.connect(kwinClient.tileChanged, () => {
// on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
});
return manager;
}
private static moveWindowToCorrectGrid(desktopManager: DesktopManager, window: Window) {
const kwinClient = window.client.kwinClient;
const oldGrid = window.column.grid;
const newGrid = desktopManager.getDesktopForClient(kwinClient).grid;
if (oldGrid === newGrid) {
// window already on the correct grid
return;
}
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
window.moveToColumn(newColumn);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = true;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
}
client.setMaximize(false, false);
}
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, defaultState: Tiled.WindowState, screenSize: QmlRect) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = defaultState.skipSwitcher;
}
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = false;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);
}
client.ensureVisible(screenSize);
}
}
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean,
}
}
}

View File

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