190 Commits
v0.2.1 ... v0.6

Author SHA1 Message Date
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
53 changed files with 2633 additions and 1329 deletions

View File

@@ -1,6 +1,6 @@
.PHONY: *
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern.d.ts
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
config:
mkdir -p ./package/contents/config

View File

@@ -16,19 +16,27 @@ scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and
[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
- Doesn't support multiple screens
- Doesn't support windows on all desktops
- Doesn't support windows on multiple activities
## Key bindings
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
Here's the default ones:
| Shortcut | Action |
| --- | --- |
| Meta+Space | Toggle floating |
| Meta+A | Move focus left |
| Meta+D | Move focus right |
| Meta+W | Move focus up |
| Meta+S | Move focus down |
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Home | Move focus to start |
| Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
@@ -37,14 +45,13 @@ Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) an
| Meta+Shift+S | Move window down |
| Meta+Shift+Home | Move window to start |
| 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+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl+X | Expand column (Expands focused column horizontally to fill the screen) |
| Meta+Alt++ | Expand fully visible columns (Expands fully visible columns to fill the screen) |
| Meta+Alt+- | Shrink visible columns (Shrinks fully and partially visible columns, making them fully visible and filling the screen) |
| Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right |
@@ -52,7 +59,7 @@ Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) an
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| 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+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 |

View File

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

View File

@@ -1,5 +1,6 @@
import QtQuick 2.15
import org.kde.kwin 3.0
import org.kde.notification 1.0
import "./main.js" as Karousel
Item {
@@ -9,11 +10,19 @@ Item {
Component.onCompleted: {
qmlBase.karouselInstance = Karousel.init();
print("script started");
}
Component.onDestruction: {
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,16 @@
<layout class="QVBoxLayout" name="layout_main">
<item>
<widget class="QTabWidget" name="tabContainer">
<widget class="QWidget" name="tab_general">
<widget class="QWidget" name="tab_parameters">
<attribute name="title">
<string>General</string>
<string>Parameters</string>
</attribute>
<layout class="QGridLayout" name="layout_tab_general" columnstretch="0,1">
<layout class="QFormLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_gapsOuterTop">
<property name="text">
<string>Top margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
@@ -45,9 +42,6 @@
<property name="text">
<string>Bottom margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
@@ -69,9 +63,6 @@
<property name="text">
<string>Left margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
@@ -93,9 +84,6 @@
<property name="text">
<string>Right margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
@@ -117,9 +105,6 @@
<property name="text">
<string>Horizontal gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
@@ -141,9 +126,6 @@
<property name="text">
<string>Vertical gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="1">
@@ -165,9 +147,6 @@
<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">
@@ -189,9 +168,6 @@
<property name="text">
<string>Manual scroll step size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="7" column="1">
@@ -208,28 +184,164 @@
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<item row="8" column="0">
<widget class="QLabel" name="label_offScreenOpacity">
<property name="text">
<string>Stack columns by default</string>
<string>Obscured window opacity:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
<property name="suffix">
<string> %</string>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
<item row="9" column="0" colspan="2">
<spacer name="bottomSpacer_tab_general">
<widget class="QWidget" name="tab_behavior">
<attribute name="title">
<string>Behavior</string>
</attribute>
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox">
<property name="flat">
<bool>true</bool>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QCheckBox" name="kcfg_untileOnDrag">
<property name="text">
<string>Un-tile windows by dragging them</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_skipSwitcher">
<property name="text">
<string>Tiled windows skip switcher</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Scrolling mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_scrollingLazy">
<property name="text">
<string>Only scroll as necessary</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingCentered">
<property name="text">
<string>Center focused column</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_scrollingGrouped">
<property name="text">
<string>Prevent needlessly obscuring columns</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Layering mode</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="spacer_footer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_windowRules">
<attribute name="title">
<string>Window rules</string>
<string>Window Rules</string>
</attribute>
<layout class="QVBoxLayout" name="layout_tab_windowRules">
<layout class="QVBoxLayout">
<item>
<widget class="QPlainTextEdit" name="kcfg_windowRules">
<property name="tabChangesFocus">

View File

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

329
src/Actions.ts Normal file
View File

@@ -0,0 +1,329 @@
namespace Actions {
export function init(world: World, config: Config) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
},
focusRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
},
focusUp: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
},
focusDown: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
},
focusStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
},
focusEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
},
windowMoveLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
window.moveToColumn(prevColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
});
},
windowMoveRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
window.moveToColumn(nextColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
});
},
windowMoveUp: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
},
windowMoveDown: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
},
windowMoveStart: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
},
windowMoveEnd: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
},
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
},
columnMoveLeft: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
},
columnMoveRight: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
},
columnMoveStart: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
column.moveAfter(null);
});
},
columnMoveEnd: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
});
},
columnToggleStacked: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
column.toggleStacked();
});
},
columnWidthIncrease: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
grid.increaseColumnWidth(column);
});
},
columnWidthDecrease: () => {
world.doIfTiledFocused(false, (world, desktopManager, window, column, grid) => {
grid.decreaseColumnWidth(column);
});
},
columnsWidthEqualize: () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
},
gridScrollLeft: () => {
gridScroll(world, -config.manualScrollStep);
},
gridScrollRight: () => {
gridScroll(world, config.manualScrollStep);
},
gridScrollStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
});
},
gridScrollEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
});
},
gridScrollFocused: () => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
},
gridScrollLeftColumn: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.desktop.scrollToColumn(prevColumn);
});
},
gridScrollRightColumn: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.desktop.scrollToColumn(nextColumn);
});
},
};
}
export function initNum(world: World) {
return {
focusColumn: (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
targetColumn.focus();
});
},
windowMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll();
});
},
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (world, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return null;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
});
},
columnMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
},
tailMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
},
};
}
function gridScroll(world: World, amount: number) {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
grid.desktop.adjustScroll(amount, false);
});
}
export type Config = {
manualScrollStep: number,
};
}

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

@@ -7,6 +7,16 @@ type Config = {
gapsInnerVertical: number,
overscroll: number,
manualScrollStep: number,
offScreenOpacity: number,
untileOnDrag: boolean,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
scrollingLazy: boolean,
scrollingCentered: boolean,
scrollingGrouped: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string,
}
};

View File

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

View File

@@ -1,4 +1,4 @@
function loadConfig() {
function loadConfig(): Config {
const config: any = {};
for (const entry of configDef) {
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;
}

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

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,232 @@
class Desktop {
public readonly grid: Grid;
public readonly desktopNumber: number;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.desktopNumber = desktopNumber;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
private static getClientArea(desktopNumber: number) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
}
private static getTilingArea(clientArea: QmlRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
left,
top,
right - left,
bottom - top,
)
}
// calculates a Range that scrolls the contained Range into view
public calculateVisibleRange(containedRange: Desktop.Range) {
const left = containedRange.getLeft();
const right = containedRange.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
let targetScrollX: number;
if (left < initialVisibleRange.getLeft()) {
targetScrollX = left;
} else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width;
} else {
return this.getVisibleRange(this.clampScrollX(this.scrollX));
}
const overscroll = this.getTargetOverscroll(targetScrollX, left < initialVisibleRange.getLeft());
return this.getVisibleRange(this.clampScrollX(targetScrollX + overscroll));
}
private getTargetOverscroll(targetScrollX: number, scrollLeft: boolean) {
if (this.config.overscroll === 0) {
return 0;
}
const visibleColumnsWidth = this.grid.getVisibleColumnsWidth(this.getVisibleRange(targetScrollX), true);
const remainingSpace = this.tilingArea.width - visibleColumnsWidth;
const overscrollX = Math.min(this.config.overscroll, Math.round(remainingSpace / 2));
const direction = scrollLeft ? -1 : 1;
return overscrollX * direction;
}
public scrollToRange(range: Desktop.Range) {
this.setScroll(this.calculateVisibleRange(range).getLeft(), true);
}
public scrollCenterRange(range: Desktop.Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null || focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(focusedColumn);
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
return this.getVisibleRange(this.scrollX);
}
private clampScrollX(x: number) {
return this.config.scroller.clampScrollX(this, x);
}
public setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
this.dirtyScroll = false;
}
public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force);
}
public equalizeVisibleColumnsWidths() {
const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
for (const column of visibleColumns) {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= columnWidth;
remainingColumns--;
}
const targetVisibleRange = Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
);
this.setScroll(this.calculateVisibleRange(targetVisibleRange).getLeft(), false);
}
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
this.dirty = false;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}
}
namespace Desktop {
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
overscroll: number,
scroller: Desktop.Scroller,
};
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
clampScrollX(desktop: Desktop, x: number): number;
}
}

View File

@@ -1,58 +1,37 @@
import Range = Desktop.Range;
class Grid {
public readonly world: World;
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
private readonly columns: LinkedList<Column>;
private lastFocusedColumn: Column|null;
private scrollX: number;
private width: number;
private userResize: boolean; // is any part of the grid being resized by the user
public clientArea: QRect;
public tilingArea: QRect;
public readonly desktop: number;
private readonly userResizeFinishedDelayer: Delayer;
constructor(world: World, desktop: number) {
this.world = world;
constructor(desktop: Desktop, config: LayoutConfig) {
this.desktop = desktop;
this.config = config;
this.columns = new LinkedList();
this.lastFocusedColumn = null;
this.scrollX = 0;
this.width = 0;
this.userResize = false;
this.desktop = desktop;
this.updateArea();
this.userResizeFinishedDelayer = new Delayer(50, () => {
// this delay prevents windows' contents from freezing after resizing
this.autoAdjustScroll();
this.arrange();
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
this.desktop.arrange();
});
}
updateArea() {
const newClientArea = workspace.clientArea(workspace.PlacementArea, 0, this.desktop);
if (newClientArea === this.clientArea) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Qt.rect(
newClientArea.x + this.world.config.gapsOuterLeft,
newClientArea.y + this.world.config.gapsOuterTop,
newClientArea.width - this.world.config.gapsOuterLeft - this.world.config.gapsOuterRight,
newClientArea.height - this.world.config.gapsOuterTop - this.world.config.gapsOuterBottom,
)
for (const column of this.columns.iterator()) {
column.resizeWindows();
}
this.autoAdjustScroll();
}
moveColumnLeft(column: Column) {
public moveColumnLeft(column: Column) {
this.columns.moveBack(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);
if (nextColumn === null) {
return;
@@ -60,179 +39,212 @@ class Grid {
this.moveColumnLeft(nextColumn);
}
getPrevColumn(column: Column) {
public getWidth() {
return this.width;
}
public getPrevColumn(column: Column) {
return this.columns.getPrev(column);
}
getNextColumn(column: Column) {
public getNextColumn(column: Column) {
return this.columns.getNext(column);
}
getFirstColumn() {
public getFirstColumn() {
return this.columns.getFirst();
}
getLastColumn() {
public getLastColumn() {
return this.columns.getLast();
}
getColumnAtIndex(i: number) {
public getColumnAtIndex(i: number) {
return this.columns.getItemAtIndex(i);
}
getLastFocusedColumn() {
public getLastFocusedColumn() {
if (this.lastFocusedColumn === null || this.lastFocusedColumn.grid !== this) {
return null;
}
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()) {
const left = column.gridX - this.scrollX; // in screen space
const right = left + column.width; // in screen space
const x = fullyVisible ? left : right;
if (x >= 0) {
if (column.isVisible(visibleRange, fullyVisible)) {
return column;
}
}
return null;
}
getRightmostVisibleColumn(fullyVisible: boolean) {
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
const left = column.gridX - this.scrollX; // in screen space
const right = left + column.width; // in screen space
const x = fullyVisible ? right : left;
if (x <= this.tilingArea.width) {
if (column.isVisible(visibleRange, fullyVisible)) {
last = column;
} else {
} else if (last !== null) {
break;
}
}
return last;
}
rescaleVisibleColumns(fullyVisible: boolean, allowScaleUp: 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;
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
column.arrange(x);
x += column.getWidth() + this.world.config.gapsInnerHorizontal;
if (column.isVisible(visibleRange, fullyVisible)) {
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 increaseColumnWidth(column: Column) {
const visibleRange = this.desktop.calculateVisibleRange(column);
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
return;
}
let leftVisibleColumn = this.getLeftmostVisibleColumn(visibleRange, true);
let rightVisibleColumn = this.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false);
return;
}
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
if (leftSpace + rightSpace > 0) {
column.adjustWidth(leftSpace + rightSpace, true);
} else {
// left and right columns are touching the screen's edges
const leftSpace = leftVisibleColumn === column ? Infinity : leftVisibleColumn.getWidth() + this.config.gapsInnerHorizontal;
const rightSpace = rightVisibleColumn === column ? Infinity : rightVisibleColumn.getWidth() + this.config.gapsInnerHorizontal;
if (leftSpace < rightSpace) {
column.adjustWidth(leftSpace, true);
leftVisibleColumn = this.getNextColumn(leftVisibleColumn)!;
} else {
column.adjustWidth(rightSpace, true);
rightVisibleColumn = this.getPrevColumn(rightVisibleColumn)!;
}
}
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
}
public decreaseColumnWidth(column: Column) {
const visibleRange = this.desktop.calculateVisibleRange(column);
if (!column.isVisible(visibleRange, true)) {
return;
}
if (this.width <= visibleRange.getWidth()) {
column.setWidth(Math.round(column.getWidth() / 2), true);
return;
}
const leftVisibleColumn = this.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = this.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false);
return;
}
let leftOffScreenColumn = this.getPrevColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = this.getNextColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
if (leftOffScreenColumn === null && rightOffScreenColumn === null) {
console.assert(false);
return;
}
const leftInvisibleWidth = leftOffScreenColumn === null ? Infinity : visibleRange.getLeft() - leftOffScreenColumn.getLeft();
const rightInvisibleWidth = rightOffScreenColumn === null ? Infinity : rightOffScreenColumn.getRight() - visibleRange.getRight();
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
if (leftInvisibleWidth < rightInvisibleWidth) {
const deltaWidth = rightSpace - leftInvisibleWidth;
column.adjustWidth(deltaWidth, true);
console.assert(leftOffScreenColumn !== null);
const newVisibleWidth = rightVisibleColumn.getRight() - leftOffScreenColumn!.getLeft();
const leftVisibleColumn = newVisibleWidth <= visibleRange.getWidth() ? leftOffScreenColumn! : this.getNextColumn(leftOffScreenColumn!)!;
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
} else {
const deltaWidth = leftSpace - rightInvisibleWidth;
column.adjustWidth(deltaWidth, true);
console.assert(rightOffScreenColumn !== null);
const newVisibleWidth = rightOffScreenColumn!.getRight() - leftVisibleColumn.getLeft();
const rightVisibleColumn = newVisibleWidth <= visibleRange.getWidth() ? rightOffScreenColumn! : this.getPrevColumn(rightOffScreenColumn!)!;
this.desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(leftVisibleColumn, rightVisibleColumn));
}
}
public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize);
x += column.getWidth() + this.config.gapsInnerHorizontal;
}
const focusedWindow = this.getLastFocusedWindow();
if (focusedWindow !== null) {
focusedWindow.client.ensureTransientsVisible(this.desktop.clientArea);
}
}
public onColumnAdded(column: Column, prevColumn: Column|null) {
if (prevColumn === null) {
this.columns.insertStart(column);
} else {
this.columns.insertAfter(column, prevColumn);
}
this.columnsSetX(column);
this.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 nextColumn = this.getNextColumn(column);
const columnToFocus = isLastColumn ? null : this.getPrevColumn(column) ?? nextColumn;
@@ -243,60 +255,70 @@ class Grid {
this.columns.remove(column);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} 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 firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
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);
this.columnsSetX(nextColumn);
if (!this.userResize) {
this.autoAdjustScroll();
this.desktop.autoAdjustScroll();
}
this.desktop.onLayoutChanged();
}
onColumnFocused(column: Column) {
public onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null) {
lastFocusedColumn.restoreToTiled();
}
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;
}
onUserResizeFinished() {
public onUserResizeFinished() {
this.userResize = false;
this.userResizeFinishedDelayer.run();
}
evacuateTail(targetGrid: Grid, startColumn: Column) {
public evacuateTail(targetGrid: Grid, startColumn: Column) {
for (const column of this.columns.iteratorFrom(startColumn)) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
evacuate(targetGrid: Grid) {
public evacuate(targetGrid: Grid) {
for (const column of this.columns.iterator()) {
column.moveToGrid(targetGrid, targetGrid.getLastColumn());
}
}
destroy() {
public 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,87 @@
class ScrollerGrouped {
private readonly layoutConfig: LayoutConfig;
constructor(layoutConfig: LayoutConfig) {
this.layoutConfig = layoutConfig;
}
public scrollToColumn(desktop: Desktop, column: Column) {
const columnRange = new ScrollerGrouped.ColumnRange(column);
const visibleRange = desktop.getCurrentVisibleRange();
columnRange.addNeighbors(visibleRange, this.layoutConfig.gapsInnerHorizontal, true);
columnRange.addNeighbors(visibleRange, this.layoutConfig.gapsInnerHorizontal, false);
desktop.scrollCenterRange(columnRange);
}
public clampScrollX(desktop: Desktop, x: number) {
return ScrollerCentered.clampScrollX(desktop, x);
}
}
namespace ScrollerGrouped {
import Range = Desktop.Range;
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Range, gap: number, requireVisible: boolean) {
const grid = this.left.grid;
let leftColumn: Column|null = this.left;
while (true) {
leftColumn = grid.getPrevColumn(leftColumn);
if (
leftColumn === null ||
requireVisible && !leftColumn.isVisible(visibleRange, true) ||
this.width + gap + leftColumn.getWidth() > visibleRange.getWidth()
) {
break;
}
this.addLeft(leftColumn, gap);
}
let rightColumn: Column|null = this.right;
while (true) {
rightColumn = grid.getNextColumn(rightColumn);
if (
rightColumn === null ||
requireVisible && !rightColumn.isVisible(visibleRange, true) ||
this.width + gap + rightColumn.getWidth() > visibleRange.getWidth()
) {
break;
}
this.addRight(rightColumn, gap);
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
}
function rectEqual(a: QRect, b: QRect) {
return a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height;
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}

View File

@@ -1,9 +1,8 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: AbstractClient) => {
console.assert(!world.hasClient(kwinClient));
if (canTileEver(kwinClient)) {
manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) {
kwinClient.desktop = workspace.currentDesktop;
@@ -12,48 +11,56 @@ function initWorkspaceSignalHandlers(world: World) {
kwinClient.activities = [workspace.currentActivity];
}
}
world.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();
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
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, (world, desktopManager, window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
});
});
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
world.onClientFocused(kwinClient);
world.doIfTiled(kwinClient, true, (window, column, grid) => {
window.onFocused();
grid.arrange();
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(workspace.clientFullScreenSet, (kwinClient: X11Client, fullScreen: boolean, user: boolean) => {
world.doIfTiled(kwinClient, false, (window, column, grid) => {
window.onFullScreenChanged(fullScreen);
grid.arrange();
});
manager.connect(workspace.currentDesktopChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfDesktops: number) => {
manager.connect(workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfVirtualDesktops: number) => {
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 {
public readonly kwinClient: AbstractClient;
public readonly stateManager: ClientStateManager;
public readonly kwinClient: KwinClient;
public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor(
kwinClient: AbstractClient,
initialState: ClientState,
kwinClient: KwinClient,
constructInitialState: (client: ClientWrapper) => ClientState.State,
transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null,
) {
this.kwinClient = kwinClient;
this.stateManager = new ClientStateManager(initialState);
this.transientFor = transientFor;
this.transients = [];
if (transientFor !== null) {
transientFor.addTransient(this);
}
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
}
place(x: number, y: number, width: number, height: number) {
public place(x: number, y: number, width: number, height: number) {
this.manipulatingGeometry.do(() => {
if (this.kwinClient.resize) {
// window is being manually resized, prevent fighting with the user
return;
}
this.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) {
// TODO: prevent moving off the grid
if (this.stateManager.getState() instanceof ClientStateFloating) {
const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx,
frame.y + dy,
frame.width,
frame.height,
);
private moveTransient(dx: number, dy: number, desktopNumber: number) {
if (this.stateManager.getState() instanceof ClientState.Floating) {
if (this.kwinClient.desktop === desktopNumber) {
const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx,
frame.y + dy,
frame.width,
frame.height,
);
}
for (const transient of this.transients) {
transient.moveTransient(dx, dy);
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;
}
isFocused() {
public isFocused() {
return workspace.activeClient === this.kwinClient;
}
setMaximize(horizontally: boolean, vertically: boolean) {
public setMaximize(horizontally: boolean, vertically: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.setMaximize(vertically, horizontally);
});
}
setFullScreen(fullScreen: boolean) {
public setFullScreen(fullScreen: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen;
});
}
setShade(shade: boolean) {
public setShade(shade: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.shade = shade;
});
}
isShaded() {
public isShaded() {
return this.kwinClient.shade;
}
isManipulatingGeometry() {
public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
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) {
this.transients.push(transient);
}
@@ -119,9 +108,29 @@ class ClientWrapper {
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.signalManager.destroy();
if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy();
}
@@ -132,23 +141,4 @@ class ClientWrapper {
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,109 @@
class World {
public readonly config: Config;
private readonly gridManager: GridManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>;
private lastFocusedClient: AbstractClient|null;
public readonly untileOnDrag: boolean;
private readonly desktopManager: DesktopManager;
public readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
private readonly screenResizedDelayer: Delayer;
constructor(config: Config) {
this.config = config;
this.clientMap = new Map();
this.lastFocusedClient = null;
this.untileOnDrag = config.untileOnDrag;
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 delay ensures that docks get taken into account by `workspace.clientArea`
const gridManager = this.gridManager; // workaround for bug in Qt5's JS engine
for (const grid of gridManager.grids()) {
grid.arrange();
const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine
for (const desktop of desktopManager.desktops()) {
desktop.onLayoutChanged();
}
this.update();
});
this.gridManager = new GridManager(this, workspace.currentActivity, workspace.desktops);
this.addExistingClients();
}
this.pinManager = new PinManager();
updateDesktops() {
this.gridManager.setNDesktops(workspace.desktops);
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
};
this.desktopManager = new DesktopManager(
this.pinManager,
{
marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight,
overscroll: config.overscroll,
scroller: config.scrollingLazy ? new ScrollerLazy() :
config.scrollingCentered ? new ScrollerCentered() :
config.scrollingGrouped ? new ScrollerGrouped(layoutConfig) :
console.assert(false),
},
layoutConfig,
workspace.currentActivity,
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
this.update();
}
private addExistingClients() {
const kwinClients = workspace.clientList();
for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i];
this.addClient(kwinClient);
this.clientManager.addClient(kwinClient);
}
}
getGrid(activity: string, desktopNumber: number) {
console.assert(desktopNumber > 0 && desktopNumber <= workspace.desktops);
return this.gridManager.get(activity, desktopNumber);
public updateDesktops() {
this.desktopManager.update();
}
getGridInCurrentActivity(desktopNumber: number) {
return this.getGrid(workspace.currentActivity, desktopNumber);
private update() {
this.desktopManager.getCurrentDesktop().arrange();
}
getCurrentGrid() {
return this.getGrid(workspace.currentActivity, workspace.currentDesktop);
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
f(this.clientManager, this.desktopManager);
this.update();
}
getClientGrid(kwinClient: AbstractClient) {
console.assert(kwinClient.activities.length === 1);
return this.getGrid(kwinClient.activities[0], kwinClient.desktop);
}
addClient(kwinClient: AbstractClient) {
const client = new ClientWrapper(
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) {
public doIfTiled(
kwinClient: KwinClient,
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
if (window === null) {
return;
}
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
this.clientMap.delete(kwinClient);
const column = window.column;
const grid = column.grid;
f(this.clientManager, this.desktopManager, window, column, grid);
this.update();
}
findTransientFor(kwinClient: AbstractClient) {
if (!kwinClient.transient) {
return null;
}
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) {
public doIfTiledFocused(
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
this.doIfTiled(workspace.activeClient, followTransient, f);
}
getFocusedWindow() {
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() {
public destroy() {
this.workspaceSignalManager.destroy();
this.removeAllClients();
for (const grid of this.gridManager.grids()) {
grid.destroy();
}
this.clientManager.destroy();
this.desktopManager.destroy();
}
onScreenResized() {
public onScreenResized() {
this.screenResizedDelayer.run();
}
}

View File

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