192 Commits
v0.4 ... v0.9.1

Author SHA1 Message Date
Peter Fajdiga
20a3ece4b5 bump version to 0.9.1 2024-04-06 08:36:51 +02:00
Peter Fajdiga
3cad8102ee qt.d.ts: remove QByteArray type 2024-04-05 14:49:36 +02:00
Peter Fajdiga
7fd45eed8f kwin.d.ts: mark cursorPos and minSize as immutable 2024-04-05 14:48:56 +02:00
Peter Fajdiga
7299341608 Tiled: use clientGeometry to determine border resize 2024-04-05 14:46:19 +02:00
Peter Fajdiga
842ec1ac63 WindowRuleEnforcer: fix bug in joinRegexes 2024-04-05 13:58:50 +02:00
Peter Fajdiga
0523465b84 WindowRuleEnforcer: remove debug logs 2024-04-01 19:26:50 +02:00
Peter Fajdiga
c7cfa261b9 bump version to 0.9 2024-03-30 12:46:59 +01:00
Peter Fajdiga
56955e4df3 src/config: don't tile kded windows 2024-03-30 12:45:42 +01:00
Peter Fajdiga
bb308cfbfb config: merge X11 and Wayland class regexes in window rules 2024-03-30 12:45:40 +01:00
Peter Fajdiga
6c00245943 config: escape . in window rules 2024-03-30 12:45:38 +01:00
Peter Fajdiga
2efdbe5a7b support regex for class selector in window rules (resolves #41) 2024-03-30 12:45:36 +01:00
Peter Fajdiga
092cbf3ff1 kwin.d.ts: remove unused signal maximizedChanged 2024-03-30 12:45:33 +01:00
Peter Fajdiga
f9ae299ce8 Tiled: restore opacity after un-tiling 2024-03-30 12:45:28 +01:00
Peter Fajdiga
695f5edf6a config: add option to disable layering (resolves #41) 2024-03-30 12:45:25 +01:00
Peter Fajdiga
9b80b535a1 readme: update QML dependencies 2024-03-19 10:58:51 +01:00
Peter Fajdiga
752df86db5 bump version to 0.8.1 2024-03-19 10:46:33 +01:00
Peter Fajdiga
f05eefe19b config: add plasmashell to window rules (fixes #38) 2024-03-19 10:17:37 +01:00
Peter Fajdiga
f550285778 bump version to 0.8 2024-03-18 19:56:14 +01:00
Peter Fajdiga
5247a6a0d3 don't tile immovable windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
2b114a63dc refactor TiledMinimized unminimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
63e4015f3a don't tile popup windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
02db31266b ClientWrapper: don't try to maximize/fullscreenify unsupporting windows 2024-03-18 19:31:22 +01:00
Peter Fajdiga
67d4d89700 World: remove workaround for Qt5 bug 2024-03-18 19:31:22 +01:00
Peter Fajdiga
755cf90b1a DesktopManager: destroy removed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
e6a01217a5 DesktopManager.getDesktopsForClient: call getDesktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
21d7bbd6c4 DesktopManager: don't yield previously unconstructed desktops 2024-03-18 19:31:22 +01:00
Peter Fajdiga
605215acdc workspace.ts: use Clients.makeTileable 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4b6808dba1 ClientWrapper: set maximizedMode in setMaximize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
f9749c6f56 Tiled: use interactiveMoveResizeStarted and interactiveMoveResizeFinished 2024-03-18 19:31:22 +01:00
Peter Fajdiga
9b40b2f777 Tiled: use cursorPos to distinguish between border and single-column resize 2024-03-18 19:31:22 +01:00
Peter Fajdiga
33470b4d7b move kwinClient.tile = null to ClientWrapper 2024-03-18 19:31:22 +01:00
Peter Fajdiga
8947719621 ClientWrapper: store maximized state 2024-03-18 19:31:22 +01:00
Peter Fajdiga
4bf4f8e8a1 Tiled: use maximizedAboutToChange instead of maximizedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
080de7cf97 kwin.d.ts: add signal maximizedAboutToChange 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c29902dc15 Pinned: change condition for un-tile on maximixedChanged 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1736b0a398 kwin.d.ts: update client signals 2024-03-18 19:31:22 +01:00
Peter Fajdiga
a1c44647ca TiledMinimized: support un-minimization 2024-03-18 19:31:22 +01:00
Peter Fajdiga
0ea75d6348 confirm all desktops == empty array 2024-03-18 19:31:22 +01:00
Peter Fajdiga
12901e45ce src/keyBindings: add TODO 2024-03-18 19:31:22 +01:00
Peter Fajdiga
29b4ccd1dd port key bindings to the kwin6 ShortcutHandler system 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7b547bc5b8 pass desktop to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
78a127111b pass output to Workspace.clientArea 2024-03-18 19:31:22 +01:00
Peter Fajdiga
333b7601b2 refactor desktops (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1927ae445d kwin.d.ts: add KwinDesktop (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
1f563dae01 kwin.d.ts: update KwinClient properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
6b82eedbfe kwin.d.ts: move removed Workspace signals to KwinClient (WIP) 2024-03-18 19:31:22 +01:00
Peter Fajdiga
b479735130 kwin.d.ts: update Workspace properties 2024-03-18 19:31:22 +01:00
Peter Fajdiga
c8f022d66f kwin.d.ts: rename Workspace 2024-03-18 19:31:22 +01:00
Peter Fajdiga
7f71750a8e use QtQuick 6.0 2024-03-18 19:31:22 +01:00
Peter Fajdiga
13ebf24732 Makefile: fix uninstall recipe 2024-03-18 19:31:22 +01:00
Peter Fajdiga
ec6b3247b7 Makefile: use kpackagetool6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
50681d3a07 update package structure for kde 6 2024-03-18 19:31:22 +01:00
Peter Fajdiga
af930a9b2f Tiled: check if user is resizing any window in the grid 2024-03-18 19:28:58 +01:00
Peter Fajdiga
489a1447e7 ClientWrapper: create a workaround for the problem with stuck off-screen windows on Wayland 2024-03-18 19:28:58 +01:00
Peter Fajdiga
b984f025ec Column: avoid setting preferredWidth of a window when another window's height in the same column is being resized by the user 2024-03-10 21:29:47 +01:00
Peter Fajdiga
4e1204f1bd DesktopManager: refactor getDesktopForClient 2024-03-10 21:02:24 +01:00
Peter Fajdiga
bbcf51783d World: fix grammar in comment 2024-03-10 15:44:17 +01:00
Peter Fajdiga
019da3766e kwin.d.ts: use unknown for Tile 2024-03-10 15:44:17 +01:00
Peter Fajdiga
1535c994b8 use === everywhere 2024-03-10 15:44:16 +01:00
Peter Fajdiga
296d0deca9 remove void keyword in QSignal params 2024-03-09 21:55:15 +01:00
Peter Fajdiga
17e7d5b46e kwin.d.ts: remove unneeded QSignal params 2024-03-09 21:50:27 +01:00
Peter Fajdiga
840a50d14d move ClientAreaOption to kwin.d.ts 2024-03-09 21:44:23 +01:00
Peter Fajdiga
4f99c4dd45 Actions: remove superfluous return values 2024-03-09 19:53:10 +01:00
Peter Fajdiga
030eddaf34 Floating: add missing ; 2024-03-09 19:34:54 +01:00
Peter Fajdiga
7246a7660e kwin.d.ts: remove superfluous comments 2024-03-09 19:19:39 +01:00
Peter Fajdiga
687256d1dd src/config: unjsonify 2024-03-09 19:19:38 +01:00
Peter Fajdiga
12bb7506cc src/keyBindings: unjsonify 2024-03-09 19:19:13 +01:00
Peter Fajdiga
1808ee0025 mark all signal properties as readonly 2024-03-09 19:07:12 +01:00
Peter Fajdiga
3021f61933 metadata.json: change description 2024-03-01 15:54:06 +01:00
Peter Fajdiga
e908138478 fix bug where resizing used manualScrollStep setting instead of manualResizeStep setting 2024-02-22 20:24:12 +01:00
Peter Fajdiga
99ad115370 rename scrollers 2024-02-18 22:11:34 +01:00
Peter Fajdiga
c5a4238f5f move scroller src files to src/behavior/scroller 2024-02-18 22:07:28 +01:00
Peter Fajdiga
0670d9c265 separate clampScrollX logic into different Clamper implementations 2024-02-18 22:06:14 +01:00
Peter Fajdiga
845874b0d0 separate increaseColumnWidth and columnWidthDecrease logic into different ColumnResizer implementations 2024-02-18 22:06:13 +01:00
Peter Fajdiga
a422a077f6 Makefile: add prerequisites to package target 2024-02-18 21:28:17 +01:00
Peter Fajdiga
2fe1be99cb Desktop.scrollCenterVisible: stop prioritizing visible columns (this is now done by ColumnRange.addNeighbors) 2024-02-18 20:21:34 +01:00
Peter Fajdiga
1a449c238d ColumnRange: prioritize nearer columns 2024-02-18 20:15:58 +01:00
Peter Fajdiga
9bda7d1a09 bump version to 0.7.1 2024-02-12 21:27:34 +01:00
Peter Fajdiga
2ce72bcee8 qt.d.ts: add console.trace 2024-02-12 21:17:37 +01:00
Peter Fajdiga
ff3f6c5d6b Desktop: fix ColumnRange when a column's width is still 0 2024-02-12 21:10:12 +01:00
Peter Fajdiga
3ab230b498 Makefile: append version number to package file name 2024-02-11 20:37:28 +01:00
Peter Fajdiga
ba9f362a1c .gitignore: ignore suffixed packages 2024-02-11 20:37:27 +01:00
Peter Fajdiga
ad6c3f1cae bump version to 0.7 2024-02-11 20:37:26 +01:00
Peter Fajdiga
ba4dd2a9c1 config.ui: relabel scrollingGrouped button 2024-02-11 20:37:25 +01:00
Peter Fajdiga
bb61853009 config.ui: reorder tabs 2024-02-11 20:37:22 +01:00
Peter Fajdiga
0cfd9b9e36 Desktop: scrollCenterRange: add parameter prioritiseVisible 2024-01-22 08:55:05 +01:00
Peter Fajdiga
43c4f7ef9a Actions: columnWidthIncrease: add steps for fully visible screen-edge columns 2024-01-22 08:55:05 +01:00
Peter Fajdiga
9cb3f33ecb Actions: extract function findNextStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
31b9e61ae3 config.ui: add manualResizeStep 2024-01-22 08:55:05 +01:00
Peter Fajdiga
668e6696ab Actions: column width increase/decrease: replace screen-relative steps with column-relative steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
e63959cfbf Actions: getWidthSteps: ignore screen-relative steps too close to existing steps 2024-01-22 08:55:05 +01:00
Peter Fajdiga
ef2650beb8 Actions: improve snapping in columnWidthDecrease 2024-01-22 08:55:05 +01:00
Peter Fajdiga
750c47c040 implement resize steps (resolves #25) 2024-01-22 08:55:05 +01:00
Peter Fajdiga
88ca0d02e1 generators/config/kcfg.ts: escape xml characters in default config values 2024-01-21 19:40:12 +01:00
Peter Fajdiga
aba786b754 Desktop: rename scrollIntoView 2024-01-21 18:39:53 +01:00
Peter Fajdiga
47aa625c99 Actions: column width increase/decrease: use getCurrentVisibleRange 2024-01-21 18:39:25 +01:00
Peter Fajdiga
03c7cc6503 Desktop.scrollToRange: simplify 2024-01-21 18:37:34 +01:00
Peter Fajdiga
9e9ff2b74f remove overscroll feature (resolves #23) 2024-01-21 18:26:18 +01:00
Peter Fajdiga
5674624e6f Desktop.equalizeVisibleColumnsWidths: simplify scroll at the end 2024-01-21 18:17:29 +01:00
Peter Fajdiga
44dd88ef7c Desktop.equalizeVisibleColumnsWidths: handle columns with limited min width 2024-01-21 18:17:29 +01:00
Peter Fajdiga
f800d6ecf0 Desktop: rewrite ColumnRange.addNeighbors 2024-01-21 18:17:29 +01:00
Peter Fajdiga
3477e17bb3 Desktop: scrollCenterRange: replace parameter requireVisible with condition 2024-01-21 18:17:29 +01:00
Peter Fajdiga
755c781646 rename parameters of doIfTiled passed functions 2024-01-21 18:17:29 +01:00
Peter Fajdiga
926345ba31 move column width increase/decrease code to Actions.ts 2024-01-21 18:17:29 +01:00
Peter Fajdiga
a2295ede43 Desktop: add method scrollCenterVisible (moved from ScrollerGrouped) 2024-01-21 18:17:29 +01:00
Peter Fajdiga
ca80a7ca28 Grid.onColumnWidthChanged: fix autoAdjustScroll call 2024-01-14 15:40:24 +01:00
Peter Fajdiga
64474b1677 readme: mention Niri 2024-01-14 09:55:57 +01:00
Peter Fajdiga
eca63cbc16 readme: update key bindings 2023-12-30 17:26:59 +01:00
Peter Fajdiga
3a8baf4cd7 bump version to 0.6 2023-12-30 17:18:44 +01:00
Peter Fajdiga
dc14171ae7 config: add Wayland window classes to window rules (resolves #24) 2023-12-27 19:45:07 +01:00
Peter Fajdiga
6dcf8979c2 config: add yakuake to window rules 2023-12-27 19:41:30 +01:00
Peter Fajdiga
fe5661c07f Grid: auto-scroll after removing a non-focused column 2023-12-24 14:58:44 +01:00
Peter Fajdiga
90b783b34b Grid: remove unused methods getLeftOffScreenColumn and getRightOffScreenColumn 2023-12-24 12:52:10 +01:00
Peter Fajdiga
fb40bd9592 Grid.decreaseColumnWidth: prevent scrolling away from focused column when reaching minimum width 2023-12-24 12:50:52 +01:00
Peter Fajdiga
768d95450d refactor arrange functions 2023-12-24 09:29:31 +01:00
Peter Fajdiga
e95a0e44c9 prevent translucent windows during resizing 2023-12-24 09:21:49 +01:00
Peter Fajdiga
c1b8d05919 defaultWindowRules: update Zoom entry 2023-12-24 09:21:44 +01:00
Peter Fajdiga
e98ce18105 add opacity settings for obscured windows 2023-12-16 12:10:26 +01:00
Peter Fajdiga
25a9efc8e4 config.ui: split up parameters and behavior tabs 2023-12-16 11:39:42 +01:00
Peter Fajdiga
db48644944 package/contents/ui: add footer spacer to take up excess space 2023-12-16 11:39:42 +01:00
Peter Fajdiga
950e0de076 Desktop: prevent scrolling when unnecessary (add dirtyScroll variable) 2023-12-16 10:07:02 +01:00
Peter Fajdiga
05ffe0895e Revert "Desktop.scrollToCenterRange: force scroll"
This reverts commit dda63d68cde58c7f4a7162b11a2fd614365d36ff.
2023-12-16 09:33:26 +01:00
Peter Fajdiga
61db5ca69f use different implementations of clampScrollX in different scrollers 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f7b5dd0b9c Desktop.equalizeVisibleColumnsWidths: use Desktop.RangeImpl.fromRanges 2023-12-16 09:33:26 +01:00
Peter Fajdiga
f83f60c98f Grid: reimplement increaseColumnWidth and decreaseColumnWidth 2023-12-16 09:33:26 +01:00
Peter Fajdiga
3e8734eefb Desktop.scrollToCenterRange: force scroll 2023-12-16 09:33:26 +01:00
Peter Fajdiga
bed0ea7ed8 Actions: make gridScrollFocused center focused column again 2023-12-16 09:33:26 +01:00
Peter Fajdiga
92c99f0b87 rename focusColumn -> scrollToColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
b2024bc8aa Actions: make scrolling actions use focusColumn 2023-12-16 09:33:26 +01:00
Peter Fajdiga
58f358313b config.ui: change description for kcfg_scrollingLazy 2023-12-16 09:33:26 +01:00
Peter Fajdiga
352a7061f6 config.ui: shorten description for kcfg_scrollingCentered 2023-12-16 09:33:26 +01:00
Peter Fajdiga
7314c0ee24 add ScrollerGrouped 2023-12-16 09:33:26 +01:00
Peter Fajdiga
c65361853c config.ui: use button groups 2023-12-16 09:32:26 +01:00
Peter Fajdiga
fa53e765b3 qt.d.ts: rename QmlTimer 2023-12-16 09:30:19 +01:00
Peter Fajdiga
4d35681ee2 qt.d.ts: rename QmlSize 2023-12-16 09:30:14 +01:00
Peter Fajdiga
1824bcdf85 fix usages of QmlRect.right and .bottom 2023-12-12 17:33:10 +01:00
Peter Fajdiga
ce1b402bf2 qt.d.ts: add comments to QmlRect fields 2023-12-10 21:08:00 +01:00
Peter Fajdiga
2df6d5d8e6 qt.d.ts: rename QmlRect 2023-12-10 21:06:03 +01:00
Peter Fajdiga
e7d33030ba read and use scrolling configuration 2023-12-05 22:15:49 +01:00
Peter Fajdiga
2bd000f0a6 config.ui: add scrolling configuration 2023-12-05 22:03:22 +01:00
Peter Fajdiga
6313d8f18e implement scrollers 2023-12-05 21:59:01 +01:00
Peter Fajdiga
464ec3bcb1 Grid.increaseColumnWidth: fix scroll adjustment after resize 2023-12-02 20:57:46 +01:00
Peter Fajdiga
fae793cb09 Desktop.equalizeVisibleColumnsWidths: adjust scroll after resizing 2023-12-02 20:50:13 +01:00
Peter Fajdiga
d7346a6fab Desktop.clampScrollX: simplify 2023-12-02 20:38:14 +01:00
Peter Fajdiga
0e5efd2be7 Desktop: receive Range in parameters instead of Column 2023-12-02 17:36:29 +01:00
Peter Fajdiga
a1a315790e turn Desktop.Range into an interface (well, into a type) 2023-12-02 14:12:41 +01:00
Peter Fajdiga
9d62499bf0 Grid.Range: make fields private 2023-12-02 14:09:23 +01:00
Peter Fajdiga
97cf61d1dd Grid: extract method calculateVisibleRange 2023-12-02 14:06:28 +01:00
Peter Fajdiga
5d83c6dd2c rename Grid.ScrollPos to Grid.Range 2023-12-02 13:55:39 +01:00
Peter Fajdiga
8915e8a9da Grid.getVisibleColumnsWidth: use getVisibleColumns 2023-12-02 12:44:57 +01:00
Peter Fajdiga
22e4c47189 add action columns-width-equalize (resolves #22) 2023-12-02 12:44:57 +01:00
Peter Fajdiga
552d2b851f readme: list QML dependencies 2023-11-12 10:02:33 +01:00
Peter Fajdiga
c4ce795359 config.ui: add stacked mode description to tooltip 2023-09-29 10:23:33 +02:00
Peter Fajdiga
1ac1fc3c2b readme: update key bindings 2023-09-29 10:21:47 +02:00
Peter Fajdiga
d0e041d16a keyBindings: add comment for column-toggle-stacked 2023-09-29 10:21:26 +02:00
Peter Fajdiga
8fc3fc976d Window: WindowState -> Window.State 2023-09-29 08:58:38 +02:00
Peter Fajdiga
e0eeace9dc ClientState.Manager: ; -> , 2023-09-29 08:58:37 +02:00
Peter Fajdiga
84e2a06b35 Tiled: define type WindowState 2023-09-29 08:56:45 +02:00
Peter Fajdiga
3373e02658 add skipSwitcher setting 2023-09-29 08:52:59 +02:00
Peter Fajdiga
43f425c868 config: set overscroll default to 0 2023-09-22 17:45:11 +02:00
Peter Fajdiga
5c5127f7ce bump version to 0.5 2023-09-22 17:42:16 +02:00
Peter Fajdiga
8664f05998 ClientManager: prevent glitchy placement when maximizing a pinned client 2023-09-22 17:36:54 +02:00
Peter Fajdiga
e4f6a32d42 make pinning work with kwin-tiled windows of any frameGeometry 2023-09-22 17:25:32 +02:00
Peter Fajdiga
2882bb8d5d base pinning on KwinClient.tile instead of frameGeometry 2023-09-22 14:40:25 +02:00
Peter Fajdiga
5ef71c92ce Column: prevent stacking columns with unshadeable windows 2023-09-22 11:15:35 +02:00
Peter Fajdiga
de59979a6e Tiled: restructure branches in moveResizedChanged handler 2023-09-22 10:54:12 +02:00
Peter Fajdiga
71d7d60837 ClientManager: use kwinClient parameters 2023-09-20 20:14:30 +02:00
Peter Fajdiga
7f44d23dd0 kwin.d.ts: merge AbstractClient and TopLevel 2023-09-20 20:13:23 +02:00
Peter Fajdiga
aceffae5f9 config.ui: add tooltips 2023-09-20 19:53:33 +02:00
Peter Fajdiga
09e5eaca88 add re-maximize setting 2023-09-19 21:15:01 +02:00
Peter Fajdiga
734dd8a4cc fix re-full-screen bug with Kate on Wayland 2023-09-19 21:05:59 +02:00
Peter Fajdiga
aeabb396f9 Revert "remove re-maximize and re-fullscreen functionality" (fixes #18)
This reverts commit 4f06f17b
2023-09-19 21:04:27 +02:00
Peter Fajdiga
05b9ebc325 handle grid changes during a full-screen window 2023-09-19 18:58:50 +02:00
Peter Fajdiga
4728afb5ea Grid: set preferred width in increaseColumnWidth and decreaseColumnWidth 2023-09-17 19:17:50 +02:00
Peter Fajdiga
653cb20e43 ClientWrapper: remove obsolete TODO 2023-09-17 19:17:50 +02:00
Peter Fajdiga
0cb2b68bf6 implement workaround that fixes #19 2023-09-17 19:17:50 +02:00
Peter Fajdiga
b8b8900754 Tiled: call World.do only if condition satisfied 2023-09-17 08:00:32 +02:00
Peter Fajdiga
14c006b5b1 Window.onFullScreenChanged: remove check for isFocused 2023-09-16 15:34:12 +02:00
Peter Fajdiga
9fe6be9b91 implement pinning (resolves #8) 2023-09-16 14:55:38 +02:00
Peter Fajdiga
97d1592318 add option for setting keepAbove on floating windows (fixes #15) 2023-09-16 14:51:46 +02:00
Peter Fajdiga
cc8cc04b05 WindowRuleEnforcer.shouldTile: don't tile transient windows 2023-09-10 18:18:48 +02:00
Peter Fajdiga
cc74d3610a WindowRuleEnforcer.shouldTile: explode condition 2023-09-10 18:17:25 +02:00
Peter Fajdiga
088725402e WindowRuleEnforcer: refactor function -> private static 2023-09-10 17:31:54 +02:00
Peter Fajdiga
25b7507b30 fix incorrect movement of transient windows (bug introduced in f4e9822f29) 2023-09-09 15:01:59 +02:00
Peter Fajdiga
9b32caafdc config/loader.ts: declare return type of loadConfig 2023-09-09 12:58:56 +02:00
Peter Fajdiga
668d579d63 prevent resizing new floating windows 2023-09-09 11:08:01 +02:00
Peter Fajdiga
f4e9822f29 ClientManager.addClient: avoid constructing state twice 2023-09-09 09:19:37 +02:00
Peter Fajdiga
bdb0a4aeb0 ClientWrapper: refactor constructor parameter initialState -> constructInitialState 2023-09-09 09:11:01 +02:00
Peter Fajdiga
dde1a12fce ClientManager.addClient: use variable kwinClient 2023-09-09 09:01:01 +02:00
Peter Fajdiga
c57c8391fb ClientWrapper: move tiling- and floating-specific functions to ClientState.Floating and ClientState.Tiled 2023-09-08 15:40:16 +02:00
Peter Fajdiga
ec64b47ceb Tiled: add comment regarding kwinClient.fullScreen 2023-09-06 22:53:36 +02:00
Peter Fajdiga
b055345e48 readme: update key bindings 2023-09-04 20:24:17 +02:00
49 changed files with 1971 additions and 1048 deletions

2
.gitignore vendored
View File

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

View File

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

View File

@@ -13,9 +13,16 @@ unprompted reflow of window content.
Windows are automatically centered when possible. And when running out of width, windows can be Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally. scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM) and Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
[Cardboard](https://gitlab.com/cardboardwm/cardboard). [Cardboard](https://gitlab.com/cardboardwm/cardboard).
## Dependencies
Karousel requires the following QML modules:
- QtQuick 6.0
- org.kde.kwin 3.0
- org.kde.notification 1.0
## Limitations ## Limitations
- Doesn't support multiple screens - Doesn't support multiple screens
- Doesn't support windows on all desktops - Doesn't support windows on all desktops
@@ -28,9 +35,9 @@ Here's the default ones:
| --- | --- | | --- | --- |
| Meta+Space | Toggle floating | | Meta+Space | Toggle floating |
| Meta+A | Move focus left | | Meta+A | Move focus left |
| Meta+D | Move focus right | | Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up | | Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down | | Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Home | Move focus to start | | Meta+Home | Move focus to start |
| Meta+End | Move focus to end | | Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) | | Meta+Shift+A | Move window left (Moves window out of and into columns) |
@@ -39,13 +46,14 @@ Here's the default ones:
| Meta+Shift+S | Move window down | | Meta+Shift+S | Move window down |
| Meta+Shift+Home | Move window to start | | Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end | | Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column | | Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+Ctrl+Shift+A | Move column left | | Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right | | Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start | | Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end | | Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width | | Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width | | Meta+Ctrl+- | Decrease column width |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) | | Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left | | Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right | | Meta+Alt+D | Scroll one column to the right |
@@ -53,7 +61,7 @@ Here's the default ones:
| Meta+Alt+PgDown | Scroll right | | Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start | | Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end | | Meta+Alt+End | Scroll to end |
| Meta+[N] | Move focus to column N | | Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) | | Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) | | Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |
| Meta+Ctrl+Shift+F[N] | Move column to desktop N | | Meta+Ctrl+Shift+F[N] | Move column to desktop N |

View File

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

View File

@@ -3,7 +3,6 @@ type KeyBinding = {
description: string; description: string;
comment?: string; comment?: string;
defaultKeySequence: string; defaultKeySequence: string;
action: string;
} }
type NumKeyBinding = { type NumKeyBinding = {
@@ -12,7 +11,6 @@ type NumKeyBinding = {
comment?: string; comment?: string;
defaultModifiers: string; defaultModifiers: string;
fKeys: boolean; fKeys: boolean;
action: string;
} }
function formatComment(comment: string | undefined) { function formatComment(comment: string | undefined) {
@@ -21,7 +19,7 @@ function formatComment(comment: string | undefined) {
function printCols(...columns: (string[] | string)[]) { function printCols(...columns: (string[] | string)[]) {
const nCols = columns.length; const nCols = columns.length;
if (nCols == 0) { if (nCols === 0) {
return; return;
} }
@@ -30,7 +28,7 @@ function printCols(...columns: (string[] | string)[]) {
).map( ).map(
(column: string[] | string) => column.length (column: string[] | string) => column.length
)); ));
if (nRows == Infinity) { if (nRows === Infinity) {
// we only have single string columns // we only have single string columns
nRows = 1; nRows = 1;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,29 +4,36 @@ const defaultWindowRules = `[
"tile": false "tile": false
}, },
{ {
"class": "kcalc", "class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false "tile": false
}, },
{ {
"class": "kfind", "class": "(org\\\\.kde\\\\.)?kded6",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kcalc",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kfind",
"tile": true "tile": true
}, },
{ {
"class": "kruler", "class": "(org\\\\.kde\\\\.)?kruler",
"tile": false "tile": false
}, },
{ {
"class": "krunner", "class": "(org\\\\.kde\\\\.)?krunner",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?yakuake",
"tile": false "tile": false
}, },
{ {
"class": "zoom", "class": "zoom",
"caption": "Zoom Cloud Meetings", "caption": "Zoom Cloud Meetings|zoom|zoom <2>",
"tile": false
},
{
"class": "zoom",
"caption": "zoom",
"tile": false "tile": false
}, },
{ {
@@ -53,63 +60,108 @@ const defaultWindowRules = `[
const configDef = [ const configDef = [
{ {
"name": "gapsOuterTop", name: "gapsOuterTop",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "gapsOuterBottom", name: "gapsOuterBottom",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "gapsOuterLeft", name: "gapsOuterLeft",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "gapsOuterRight", name: "gapsOuterRight",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "gapsInnerHorizontal", name: "gapsInnerHorizontal",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "gapsInnerVertical", name: "gapsInnerVertical",
"type": "UInt", type: "UInt",
"default": 18 default: 18,
}, },
{ {
"name": "overscroll", name: "manualScrollStep",
"type": "UInt", type: "UInt",
"default": 18 default: 200,
}, },
{ {
"name": "manualScrollStep", name: "manualResizeStep",
"type": "UInt", type: "UInt",
"default": 200 default: 600,
}, },
{ {
"name": "untileOnDrag", name: "offScreenOpacity",
"type": "Bool", type: "UInt",
"default": true default: 100,
}, },
{ {
"name": "stackColumnsByDefault", name: "untileOnDrag",
"type": "Bool", type: "Bool",
"default": false default: true,
}, },
{ {
"name": "resizeNeighborColumn", name: "stackColumnsByDefault",
"type": "Bool", type: "Bool",
"default": false default: false,
}, },
{ {
"name": "windowRules", name: "resizeNeighborColumn",
"type": "String", type: "Bool",
"default": defaultWindowRules default: false,
},
{
name: "reMaximize",
type: "Bool",
default: false,
},
{
name: "skipSwitcher",
type: "Bool",
default: false,
},
{
name: "scrollingLazy",
type: "Bool",
default: true,
},
{
name: "scrollingCentered",
type: "Bool",
default: false,
},
{
name: "scrollingGrouped",
type: "Bool",
default: false,
},
{
name: "tiledKeepBelow",
type: "Bool",
default: true,
},
{
name: "floatingKeepAbove",
type: "Bool",
default: false,
},
{
name: "noLayering",
type: "Bool",
default: false,
},
{
name: "windowRules",
type: "String",
default: defaultWindowRules,
} }
]; ];

View File

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

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

@@ -1,83 +1,102 @@
declare const KWin: { declare const KWin: {
// Functions
readConfig(key: string, defaultValue: any): any; readConfig(key: string, defaultValue: any): any;
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
}; };
declare const workspace: { declare const Workspace: {
// Read-write Properties readonly activities: string[];
readonly desktops: number; readonly desktops: KwinDesktop[];
readonly currentDesktop: number; readonly currentDesktop: KwinDesktop;
readonly currentActivity: string; readonly currentActivity: string;
readonly activeScreen: Output;
readonly windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
// Read-write Properties activeWindow: KwinClient;
activeClient: KwinClient;
// Signals readonly currentDesktopChanged: QSignal<[]>
currentDesktopChanged: QSignal<[oldDesktopNumber: number]> readonly windowAdded: QSignal<[KwinClient]>;
clientAdded: QSignal<[KwinClient]>; readonly windowRemoved: QSignal<[KwinClient]>;
clientRemoved: QSignal<[AbstractClient]>; readonly windowActivated: QSignal<[KwinClient]>;
clientMinimized: QSignal<[AbstractClient]>; readonly desktopsChanged: QSignal<[]>;
clientUnminimized: QSignal<[AbstractClient]>; readonly activitiesChanged: QSignal<[]>;
clientMaximizeSet: QSignal<[AbstractClient, horizontally: boolean, vertically: boolean]>; readonly currentActivityChanged: QSignal<[]>;
clientActivated: QSignal<[AbstractClient]>; readonly virtualScreenSizeChanged: QSignal<[]>;
numberDesktopsChanged: QSignal<[oldNumberOfVirtualDesktops: number]>;
currentActivityChanged: QSignal<[newActivity: string]>;
virtualScreenSizeChanged: QSignal<[void]>;
// Functions clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop);
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
clientList(): TopLevel[];
}; };
type Tile = any; const enum ClientAreaOption {
PlacementArea,
interface AbstractClient { MovementArea,
// Read-only Properties MaximizeArea,
readonly caption: string; MaximizeFullArea,
readonly minSize: QSize; FullScreenArea,
readonly transient: boolean; WorkArea,
readonly transientFor: AbstractClient; FullArea,
readonly move: boolean; ScreenArea,
readonly resize: boolean;
readonly resizeable: boolean;
// Read-write Properties
fullScreen: boolean;
activities: string[]; // empty array means all activities
keepBelow: boolean;
shade: boolean;
minimized: boolean;
tile: Tile;
// Signals
fullScreenChanged: QSignal<[void]>;
desktopChanged: QSignal<[void]>;
activitiesChanged: QSignal<[AbstractClient]>;
captionChanged: QSignal<[void]>;
tileChanged: QSignal<[Tile]>;
moveResizedChanged: QSignal<[void]>;
moveResizeCursorChanged: QSignal<[void]>;
clientStartUserMovedResized: QSignal<[void]>;
// Functions
setMaximize(vertically: boolean, horizontally: boolean): void;
} }
interface TopLevel extends AbstractClient { const enum MaximizedMode {
// Read-only Properties Unmaximized,
readonly screen: number; Vertically,
readonly resourceClass: QByteArray; Horizontally,
Maximized,
}
type Tile = unknown;
type Output = unknown;
interface KwinClient {
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
readonly moveable: boolean;
readonly resizeable: boolean;
readonly fullScreenable: boolean;
readonly maximizable: boolean;
readonly output: Output;
readonly resourceClass: string;
readonly dock: boolean; readonly dock: boolean;
readonly normalWindow: boolean; readonly normalWindow: boolean;
readonly managed: boolean; readonly managed: boolean;
readonly popupWindow: boolean;
// Read-write Properties fullScreen: boolean;
frameGeometry: QRect; activities: string[]; // empty array means all activities
desktop: number; // -1 means all desktops skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile;
opacity: number;
// Signals readonly fullScreenChanged: QSignal<[]>;
frameGeometryChanged: QSignal<[TopLevel, oldGeometry: QRect]>; readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
readonly interactiveMoveResizeFinished: QSignal<[]>;
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
} }
interface KwinClient extends TopLevel {} interface KwinDesktop {
readonly id: string;
}
type ShortcutHandler = {
readonly activated: QSignal<[]>;
destroy(): void;
};

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

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

View File

@@ -1,211 +1,185 @@
const keyBindings: KeyBinding[] = [ const keyBindings: KeyBinding[] = [
{ {
"name": "window-toggle-floating", name: "window-toggle-floating",
"description": "Toggle floating", description: "Toggle floating",
"defaultKeySequence": "Meta+Space", defaultKeySequence: "Meta+Space",
"action": "windowToggleFloating",
}, },
{ {
"name": "focus-left", name: "focus-left",
"description": "Move focus left", description: "Move focus left",
"defaultKeySequence": "Meta+A", defaultKeySequence: "Meta+A",
"action": "focusLeft",
}, },
{ {
"name": "focus-right", name: "focus-right",
"description": "Move focus right", description: "Move focus right",
"comment": "Clashes with default KDE shortcuts, may require manual remapping", comment: "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+D", defaultKeySequence: "Meta+D",
"action": "focusRight",
}, },
{ {
"name": "focus-up", name: "focus-up",
"description": "Move focus up", description: "Move focus up",
"comment": "Clashes with default KDE shortcuts, may require manual remapping", comment: "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+W", defaultKeySequence: "Meta+W",
"action": "focusUp",
}, },
{ {
"name": "focus-down", name: "focus-down",
"description": "Move focus down", description: "Move focus down",
"comment": "Clashes with default KDE shortcuts, may require manual remapping", comment: "Clashes with default KDE shortcuts, may require manual remapping",
"defaultKeySequence": "Meta+S", defaultKeySequence: "Meta+S",
"action": "focusDown",
}, },
{ {
"name": "focus-start", name: "focus-start",
"description": "Move focus to start", description: "Move focus to start",
"defaultKeySequence": "Meta+Home", defaultKeySequence: "Meta+Home",
"action": "focusStart",
}, },
{ {
"name": "focus-end", name: "focus-end",
"description": "Move focus to end", description: "Move focus to end",
"defaultKeySequence": "Meta+End", defaultKeySequence: "Meta+End",
"action": "focusEnd",
}, },
{ {
"name": "window-move-left", name: "window-move-left",
"description": "Move window left", description: "Move window left",
"comment": "Moves window out of and into columns", comment: "Moves window out of and into columns",
"defaultKeySequence": "Meta+Shift+A", defaultKeySequence: "Meta+Shift+A",
"action": "windowMoveLeft",
}, },
{ {
"name": "window-move-right", name: "window-move-right",
"description": "Move window right", description: "Move window right",
"comment": "Moves window out of and into columns", comment: "Moves window out of and into columns",
"defaultKeySequence": "Meta+Shift+D", defaultKeySequence: "Meta+Shift+D",
"action": "windowMoveRight",
}, },
{ {
"name": "window-move-up", name: "window-move-up",
"description": "Move window up", description: "Move window up",
"defaultKeySequence": "Meta+Shift+W", defaultKeySequence: "Meta+Shift+W",
"action": "windowMoveUp",
}, },
{ {
"name": "window-move-down", name: "window-move-down",
"description": "Move window down", description: "Move window down",
"defaultKeySequence": "Meta+Shift+S", defaultKeySequence: "Meta+Shift+S",
"action": "windowMoveDown",
}, },
{ {
"name": "window-move-start", name: "window-move-start",
"description": "Move window to start", description: "Move window to start",
"defaultKeySequence": "Meta+Shift+Home", defaultKeySequence: "Meta+Shift+Home",
"action": "windowMoveStart",
}, },
{ {
"name": "window-move-end", name: "window-move-end",
"description": "Move window to end", description: "Move window to end",
"defaultKeySequence": "Meta+Shift+End", defaultKeySequence: "Meta+Shift+End",
"action": "windowMoveEnd",
}, },
{ {
"name": "column-toggle-stacked", name: "column-toggle-stacked",
"description": "Toggle stacked layout for focused column", description: "Toggle stacked layout for focused column",
"defaultKeySequence": "Meta+X", comment: "One window in the column visible, others shaded; not supported on Wayland",
"action": "columnToggleStacked", defaultKeySequence: "Meta+X",
}, },
{ {
"name": "column-move-left", name: "column-move-left",
"description": "Move column left", description: "Move column left",
"defaultKeySequence": "Meta+Ctrl+Shift+A", defaultKeySequence: "Meta+Ctrl+Shift+A",
"action": "columnMoveLeft",
}, },
{ {
"name": "column-move-right", name: "column-move-right",
"description": "Move column right", description: "Move column right",
"defaultKeySequence": "Meta+Ctrl+Shift+D", defaultKeySequence: "Meta+Ctrl+Shift+D",
"action": "columnMoveRight",
}, },
{ {
"name": "column-move-start", name: "column-move-start",
"description": "Move column to start", description: "Move column to start",
"defaultKeySequence": "Meta+Ctrl+Shift+Home", defaultKeySequence: "Meta+Ctrl+Shift+Home",
"action": "columnMoveStart",
}, },
{ {
"name": "column-move-end", name: "column-move-end",
"description": "Move column to end", description: "Move column to end",
"defaultKeySequence": "Meta+Ctrl+Shift+End", defaultKeySequence: "Meta+Ctrl+Shift+End",
"action": "columnMoveEnd",
}, },
{ {
"name": "column-width-increase", name: "column-width-increase",
"description": "Increase column width", description: "Increase column width",
"defaultKeySequence": "Meta+Ctrl++", defaultKeySequence: "Meta+Ctrl++",
"action": "columnWidthIncrease",
}, },
{ {
"name": "column-width-decrease", name: "column-width-decrease",
"description": "Decrease column width", description: "Decrease column width",
"defaultKeySequence": "Meta+Ctrl+-", defaultKeySequence: "Meta+Ctrl+-",
"action": "columnWidthDecrease",
}, },
{ {
"name": "grid-scroll-focused", name: "columns-width-equalize",
"description": "Center focused window", description: "Equalize widths of visible columns",
"comment": "Scrolls so that the focused window is centered in the screen", defaultKeySequence: "Meta+Ctrl+X",
"defaultKeySequence": "Meta+Alt+Return",
"action": "gridScrollFocused",
}, },
{ {
"name": "grid-scroll-left-column", name: "grid-scroll-focused",
"description": "Scroll one column to the left", description: "Center focused window",
"defaultKeySequence": "Meta+Alt+A", comment: "Scrolls so that the focused window is centered in the screen",
"action": "gridScrollLeftColumn", defaultKeySequence: "Meta+Alt+Return",
}, },
{ {
"name": "grid-scroll-right-column", name: "grid-scroll-left-column",
"description": "Scroll one column to the right", description: "Scroll one column to the left",
"defaultKeySequence": "Meta+Alt+D", defaultKeySequence: "Meta+Alt+A",
"action": "gridScrollRightColumn",
}, },
{ {
"name": "grid-scroll-left", name: "grid-scroll-right-column",
"description": "Scroll left", description: "Scroll one column to the right",
"defaultKeySequence": "Meta+Alt+PgUp", defaultKeySequence: "Meta+Alt+D",
"action": "gridScrollLeft",
}, },
{ {
"name": "grid-scroll-right", name: "grid-scroll-left",
"description": "Scroll right", description: "Scroll left",
"defaultKeySequence": "Meta+Alt+PgDown", defaultKeySequence: "Meta+Alt+PgUp",
"action": "gridScrollRight",
}, },
{ {
"name": "grid-scroll-start", name: "grid-scroll-right",
"description": "Scroll to start", description: "Scroll right",
"defaultKeySequence": "Meta+Alt+Home", defaultKeySequence: "Meta+Alt+PgDown",
"action": "gridScrollStart",
}, },
{ {
"name": "grid-scroll-end", name: "grid-scroll-start",
"description": "Scroll to end", description: "Scroll to start",
"defaultKeySequence": "Meta+Alt+End", defaultKeySequence: "Meta+Alt+Home",
"action": "gridScrollEnd", },
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
}, },
]; ];
const numKeyBindings: NumKeyBinding[] = [ const numKeyBindings: NumKeyBinding[] = [
{ {
"name": "focus-", name: "focus-",
"description": "Move focus to column ", description: "Move focus to column ",
"comment": "Clashes with default KDE shortcuts, may require manual remapping", comment: "Clashes with default KDE shortcuts, may require manual remapping",
"defaultModifiers": "Meta", defaultModifiers: "Meta",
"fKeys": false, fKeys: false,
"action": "focusColumn",
}, },
{ {
"name": "window-move-to-column-", name: "window-move-to-column-",
"description": "Move window to column ", description: "Move window to column ",
"comment": "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!", comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
"defaultModifiers": "Meta+Shift", defaultModifiers: "Meta+Shift",
"fKeys": false, fKeys: false,
"action": "windowMoveToColumn",
}, },
{ {
"name": "column-move-to-column-", name: "column-move-to-column-",
"description": "Move column to position ", description: "Move column to position ",
"comment": "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!", comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
"defaultModifiers": "Meta+Ctrl+Shift", defaultModifiers: "Meta+Ctrl+Shift",
"fKeys": false, fKeys: false,
"action": "columnMoveToColumn",
}, },
{ {
"name": "column-move-to-desktop-", name: "column-move-to-desktop-",
"description": "Move column to desktop ", description: "Move column to desktop ",
"defaultModifiers": "Meta+Ctrl+Shift", defaultModifiers: "Meta+Ctrl+Shift",
"fKeys": true, fKeys: true,
"action": "columnMoveToDesktop",
}, },
{ {
"name": "tail-move-to-desktop-", name: "tail-move-to-desktop-",
"description": "Move this and all following columns to desktop ", description: "Move this and all following columns to desktop ",
"defaultModifiers": "Meta+Ctrl+Shift+Alt", defaultModifiers: "Meta+Ctrl+Shift+Alt",
"fKeys": true, fKeys: true,
"action": "tailMoveToDesktop",
}, },
]; ];

View File

@@ -3,7 +3,6 @@ type KeyBinding = {
description: string; description: string;
comment?: string; comment?: string;
defaultKeySequence: string; defaultKeySequence: string;
action: keyof ReturnType<typeof Actions.init>;
}; };
type NumKeyBinding = { type NumKeyBinding = {
@@ -12,7 +11,6 @@ type NumKeyBinding = {
comment?: string; comment?: string;
defaultModifiers: string; defaultModifiers: string;
fKeys: boolean; fKeys: boolean;
action: keyof ReturnType<typeof Actions.initNum>;
}; };
function catchWrap(f: () => void) { function catchWrap(f: () => void) {
@@ -26,40 +24,44 @@ function catchWrap(f: () => void) {
}; };
} }
function registerKeyBinding(name: string, description: string, keySequence: string, callback: () => void) { function registerKeyBinding(world: World, config: Actions.Config, shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
KWin.registerShortcut( shortcutActions.push(new ShortcutAction(
"karousel-" + name, keyBinding,
"Karousel: " + description, catchWrap(Actions.getAction(world, config, keyBinding.name)),
keySequence, ));
catchWrap(callback),
);
} }
function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) { function registerNumKeyBindings(world: World, shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = fKeys ? "F" : ""; const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = fKeys ? 12 : 9; const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) { for (let i = 0; i < 12; i++) {
const numKey = String(i + 1); const numKey = String(i + 1);
const keySequence = i < n ? const keySequence = i < n ?
modifiers + "+" + numPrefix + numKey : numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
""; "";
registerKeyBinding( const action = Actions.getNumAction(world, numKeyBinding.name);
name + numKey, shortcutActions.push(new ShortcutAction(
description + numKey, {
keySequence, name: numKeyBinding.name + numKey,
() => callback(i), description: numKeyBinding.description + numKey,
); defaultKeySequence: keySequence,
},
catchWrap(() => action(i)),
));
} }
} }
function registerKeyBindings(world: World, config: Config) { // TODO: refactor
const actions = Actions.init(world, config); function registerKeyBindings(world: World, config: Actions.Config) {
for (const binding of keyBindings) { const shortcutActions: ShortcutAction[] = [];
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
for (const keyBinding of keyBindings) {
registerKeyBinding(world, config, shortcutActions, keyBinding);
} }
const numActions = Actions.initNum(world); for (const numKeyBinding of numKeyBindings) {
for (const binding of numKeyBindings) { registerNumKeyBindings(world, shortcutActions, numKeyBinding);
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
} }
return shortcutActions;
} }

View File

@@ -25,7 +25,7 @@ class Column {
this.grid = targetGrid; this.grid = targetGrid;
targetGrid.onColumnAdded(this, prevColumn); targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber; window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
} }
} }
} }
@@ -92,16 +92,17 @@ class Column {
public setWidth(width: number, setPreferred: boolean) { public setWidth(width: number, setPreferred: boolean) {
width = clamp(width, this.getMinWidth(), this.getMaxWidth()); width = clamp(width, this.getMinWidth(), this.getMaxWidth());
const oldWidth = this.width; if (width === this.width) {
return;
}
this.width = width; this.width = width;
if (setPreferred) { if (setPreferred) {
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.preferredWidth = width; window.client.preferredWidth = width;
} }
} }
if (width !== oldWidth) { this.grid.onColumnWidthChanged(this);
this.grid.onColumnWidthChanged(this, oldWidth, width);
}
} }
public adjustWidth(widthDelta: number, setPreferred: boolean) { public adjustWidth(widthDelta: number, setPreferred: boolean) {
@@ -180,8 +181,15 @@ class Column {
window.focus(); window.focus();
} }
public arrange(x: number) { public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.stacked && this.windows.length() >= 2) { if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
}
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x); this.arrangeStacked(x);
return; return;
} }
@@ -228,13 +236,22 @@ class Column {
this.grid.desktop.onLayoutChanged(); this.grid.desktop.onLayoutChanged();
} }
public isVisible(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) { 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) { if (fullyVisible) {
return this.getLeft() >= scrollPos.getLeft() && return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= scrollPos.getRight(); this.getRight() <= visibleRange.getRight();
} else { } else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > scrollPos.getLeft() && return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < scrollPos.getRight(); this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
} }
} }

View File

@@ -1,138 +1,162 @@
class Desktop { class Desktop {
public readonly grid: Grid; public readonly grid: Grid;
public readonly desktopNumber: number; public readonly kwinDesktop: KwinDesktop;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config; private readonly config: Desktop.Config;
private scrollX: number; private scrollX: number;
private dirty: boolean; private dirty: boolean;
public clientArea: QRect; private dirtyScroll: boolean;
public tilingArea: QRect; private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(desktopNumber: number, config: Desktop.Config, layoutConfig: LayoutConfig) { constructor(kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config; this.config = config;
this.scrollX = 0; this.scrollX = 0;
this.dirty = false; this.dirty = true;
this.desktopNumber = desktopNumber; this.dirtyScroll = true;
this.dirtyPins = true;
this.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig); this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber); this.clientArea = Desktop.getClientArea(kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, config); this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
} }
private updateArea() { private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber); const newClientArea = Desktop.getClientArea(this.kwinDesktop);
if (newClientArea === this.clientArea) { if (newClientArea === this.clientArea && !this.dirtyPins) {
return; return;
} }
this.clientArea = newClientArea; this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.config); this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
this.dirty = true; this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged(); this.grid.onScreenSizeChanged();
this.autoAdjustScroll(); this.autoAdjustScroll();
} }
private static getClientArea(desktopNumber: number) { private static getClientArea(kwinDesktop: KwinDesktop) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber); return Workspace.clientArea(ClientAreaOption.PlacementArea, Workspace.activeScreen, kwinDesktop);
} }
private static getTilingArea(clientArea: QRect, config: Desktop.Config) { private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(kwinDesktop, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect( return Qt.rect(
clientArea.x + config.marginLeft, left,
clientArea.y + config.marginTop, top,
clientArea.width - config.marginLeft - config.marginRight, right - left,
clientArea.height - config.marginTop - config.marginBottom, bottom - top,
) )
} }
// calculates Desktop.Pos that scrolls the column into view public scrollIntoView(range: Desktop.Range) {
public getScrollPosForColumn(column: Column) { const left = range.getLeft();
const left = column.getLeft(); const right = range.getRight();
const right = column.getRight(); const initialVisibleRange = this.getCurrentVisibleRange();
const initialScrollPos = this.getCurrentScrollPos();
let targetScrollX: number; let targetScrollX: number;
if (left < initialScrollPos.getLeft()) { if (left < initialVisibleRange.getLeft()) {
targetScrollX = left; targetScrollX = left;
} else if (right > initialScrollPos.getRight()) { } else if (right > initialVisibleRange.getRight()) {
targetScrollX = right - this.tilingArea.width; targetScrollX = right - this.tilingArea.width;
} else { } else {
return this.getScrollPos(this.clampScrollX(this.scrollX)); targetScrollX = initialVisibleRange.getLeft();
} }
const overscroll = this.getTargetOverscroll(targetScrollX, left < initialScrollPos.getLeft()); this.setScroll(targetScrollX, false);
return this.getScrollPos(this.clampScrollX(targetScrollX + overscroll));
} }
private getTargetOverscroll(targetScrollX: number, scrollLeft: boolean) { public scrollCenterRange(range: Desktop.Range) {
if (this.config.overscroll === 0) { const windowCenter = range.getLeft() + range.getWidth() / 2;
return 0;
}
const visibleColumnsWidth = this.grid.getVisibleColumnsWidth(this.getScrollPos(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 scrollToColumn(column: Column) {
this.setScroll(this.getScrollPosForColumn(column).x, true);
}
public scrollCenterColumn(column: Column) {
const windowCenter = column.getLeft() + column.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2; const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false); this.adjustScroll(Math.round(windowCenter - screenCenter), false);
} }
public scrollCenterVisible(focusedColumn: Column) {
const columnRange = new Desktop.ColumnRange(focusedColumn);
const visibleRange = this.getCurrentVisibleRange();
columnRange.addNeighbors(visibleRange, this.grid.config.gapsInnerHorizontal);
this.scrollCenterRange(columnRange);
}
public autoAdjustScroll() { public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn(); const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null) { if (focusedColumn === null || focusedColumn.grid !== this.grid) {
this.removeOverscroll();
return;
}
if (focusedColumn.grid !== this.grid) {
return; return;
} }
this.scrollToColumn(focusedColumn); this.scrollToColumn(focusedColumn);
} }
private getScrollPos(scrollX: number) { public scrollToColumn(column: Column) {
return new Desktop.ScrollPos(scrollX, this.tilingArea.width); if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
this.config.scroller.scrollToColumn(this, column);
}
} }
public getCurrentScrollPos() { private getVisibleRange(scrollX: number) {
return this.getScrollPos(this.scrollX); return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
return this.getVisibleRange(this.scrollX);
} }
private clampScrollX(x: number) { private clampScrollX(x: number) {
let minScroll = 0; return this.config.clamper.clampScrollX(this, x);
let maxScroll = this.grid.getWidth() - this.tilingArea.width;
if (maxScroll < 0) {
const centerScroll = Math.round(maxScroll / 2);
minScroll = centerScroll;
maxScroll = centerScroll;
}
return clamp(x, minScroll, maxScroll);
} }
private setScroll(x: number, force: boolean) { public setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX; const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x); this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) { if (this.scrollX !== oldScrollX) {
this.onLayoutChanged(); this.onLayoutChanged();
} }
} this.dirtyScroll = false;
private applyScrollPos(scrollPos: Desktop.ScrollPos) {
this.setScroll(scrollPos.x, true);
} }
public adjustScroll(dx: number, force: boolean) { public adjustScroll(dx: number, force: boolean) {
this.setScroll(this.scrollX + dx, force); this.setScroll(this.scrollX + dx, force);
} }
private removeOverscroll() { public equalizeVisibleColumnsWidths() {
this.setScroll(this.scrollX, false); const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
for (const minWidth of minWidths) {
if (minWidth > remainingWidth / remainingColumns) {
remainingWidth -= minWidth;
remainingColumns--;
}
}
const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
} }
public arrange() { public arrange() {
@@ -141,12 +165,19 @@ class Desktop {
if (!this.dirty) { if (!this.dirty) {
return; return;
} }
this.grid.arrange(this.tilingArea.x - this.scrollX); this.grid.arrange(this.tilingArea.x - this.scrollX, this.getCurrentVisibleRange());
this.dirty = false; this.dirty = false;
} }
public onLayoutChanged() { public onLayoutChanged() {
this.dirty = true; this.dirty = true;
this.dirtyScroll = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
} }
public destroy() { public destroy() {
@@ -160,12 +191,19 @@ namespace Desktop {
marginBottom: number, marginBottom: number,
marginLeft: number, marginLeft: number,
marginRight: number, marginRight: number,
overscroll: number, scroller: Desktop.Scroller,
clamper: Desktop.Clamper,
}; };
export class ScrollPos { export type Range = {
public readonly x: number; getLeft(): number;
public readonly width: number; getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) { constructor(x: number, width: number) {
this.x = x; this.x = x;
@@ -179,5 +217,95 @@ namespace Desktop {
public getRight() { public getRight() {
return this.x + this.width; return this.x + this.width;
} }
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange {
private left: Column;
private right: Column;
private width: number;
constructor(initialColumn: Column) {
this.left = initialColumn;
this.right = initialColumn;
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
function canFit(column: Column) {
return columnRange.width + gap + column.getWidth() <= visibleRange.getWidth();
}
function isUsable(column: Column|null) {
return column !== null && canFit(column);
}
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
}
if (!isUsable(rightColumn)) {
rightColumn = null;
}
}
checkColumns();
const visibleCenter = visibleRange.getLeft() + visibleRange.getWidth() / 2;
while (leftColumn !== null || rightColumn !== null) {
const leftToCenter = leftColumn === null ? Infinity : Math.abs(leftColumn.getLeft() - visibleCenter);
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getPrevColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getNextColumn(rightColumn!);
}
checkColumns();
}
}
public addLeft(column: Column, gap: number) {
this.left = column;
this.width += column.getWidth() + gap;
}
public addRight(column: Column, gap: number) {
this.right = column;
this.width += column.getWidth() + gap;
}
public getLeft() {
return this.left.getLeft();
}
public getRight() {
return this.right.getRight();
}
public getWidth() {
return this.width;
}
}
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
}
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
} }
} }

View File

@@ -1,3 +1,5 @@
import Range = Desktop.Range;
class Grid { class Grid {
public readonly desktop: Desktop; public readonly desktop: Desktop;
public readonly config: LayoutConfig; public readonly config: LayoutConfig;
@@ -41,6 +43,10 @@ class Grid {
return this.width; return this.width;
} }
public isUserResizing() {
return this.userResize;
}
public getPrevColumn(column: Column) { public getPrevColumn(column: Column) {
return this.columns.getPrev(column); return this.columns.getPrev(column);
} }
@@ -88,39 +94,41 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal; this.width = x - this.config.gapsInnerHorizontal;
} }
public getLeftmostVisibleColumn(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) { public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
const scrollX = scrollPos.getLeft();
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
const x = fullyVisible ? column.getLeft() : column.getRight() + (this.config.gapsInnerHorizontal - 1); if (column.isVisible(visibleRange, fullyVisible)) {
if (x >= scrollX) {
return column; return column;
} }
} }
return null; return null;
} }
public getRightmostVisibleColumn(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) { public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
const scrollX = scrollPos.getRight();
let last = null; let last = null;
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
const x = fullyVisible ? column.getRight() : column.getLeft() - (this.config.gapsInnerHorizontal - 1); if (column.isVisible(visibleRange, fullyVisible)) {
if (x <= scrollX) {
last = column; last = column;
} else { } else if (last !== null) {
break; break;
} }
} }
return last; return last;
} }
public getVisibleColumnsWidth(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) { public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) {
yield column;
}
}
}
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
let width = 0; let width = 0;
let nVisible = 0; let nVisible = 0;
for (const column of this.columns.iterator()) { for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
if (column.isVisible(scrollPos, fullyVisible)) { width += column.getWidth();
width += column.getWidth(); nVisible++;
nVisible++;
}
} }
if (nVisible > 0) { if (nVisible > 0) {
@@ -130,84 +138,9 @@ class Grid {
return width; return width;
} }
private getLeftOffScreenColumn(scrollPos: Desktop.ScrollPos) { public arrange(x: number, visibleRange: Range) {
const leftVisible = this.getLeftmostVisibleColumn(scrollPos, true);
if (leftVisible === null) {
return null;
}
return this.getPrevColumn(leftVisible);
}
private getRightOffScreenColumn(scrollPos: Desktop.ScrollPos) {
const rightVisible = this.getRightmostVisibleColumn(scrollPos, true);
if (rightVisible === null) {
return null;
}
return this.getNextColumn(rightVisible);
}
public increaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width < scrollPos.width) {
column.adjustWidth(scrollPos.width - this.width, false);
return;
}
let leftColumn = this.getLeftmostVisibleColumn(scrollPos, false);
if (leftColumn === column) {
leftColumn = null;
}
let rightColumn = this.getRightmostVisibleColumn(scrollPos, false);
if (rightColumn === column) {
rightColumn = null;
}
if (leftColumn === null && rightColumn === null) {
return;
}
const leftVisibleWidth = leftColumn === null ? Infinity : leftColumn.getRight() - scrollPos.getLeft();
const rightVisibleWidth = rightColumn === null ? Infinity : scrollPos.getRight() - rightColumn.getLeft();
const expandLeft = leftVisibleWidth < rightVisibleWidth;
const widthDelta = (expandLeft ? leftVisibleWidth : rightVisibleWidth) + this.config.gapsInnerHorizontal;
if (expandLeft) {
this.desktop.adjustScroll(widthDelta, false);
}
column.adjustWidth(widthDelta, true);
}
public decreaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width <= scrollPos.width) {
column.setWidth(Math.round(column.getWidth() / 2), false);
return;
}
let leftColumn = this.getLeftOffScreenColumn(scrollPos);
if (leftColumn === column) {
leftColumn = null;
}
let rightColumn = this.getRightOffScreenColumn(scrollPos);
if (rightColumn === column) {
rightColumn = null;
}
if (leftColumn === null && rightColumn === null) {
return;
}
const leftInvisibleWidth = leftColumn === null ? Infinity : scrollPos.getLeft() - leftColumn.getLeft();
const rightInvisibleWidth = rightColumn === null ? Infinity : rightColumn.getRight() - scrollPos.getRight();
const shrinkLeft = leftInvisibleWidth < rightInvisibleWidth;
const widthDelta = (shrinkLeft ? leftInvisibleWidth : rightInvisibleWidth);
if (shrinkLeft) {
const maxDelta = column.getWidth() - column.getMinWidth();
this.desktop.adjustScroll(-Math.min(widthDelta, maxDelta), false);
}
column.adjustWidth(-widthDelta, true);
}
public arrange(x: number) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
column.arrange(x); column.arrange(x, visibleRange, this.userResize);
x += column.getWidth() + this.config.gapsInnerHorizontal; x += column.getWidth() + this.config.gapsInnerHorizontal;
} }
@@ -239,12 +172,12 @@ class Grid {
this.columns.remove(column); this.columns.remove(column);
this.columnsSetX(nextColumn); this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (passFocus && columnToFocus !== null) { if (passFocus && columnToFocus !== null) {
columnToFocus.focus(); columnToFocus.focus();
} else { } else {
this.desktop.autoAdjustScroll(); this.desktop.autoAdjustScroll();
} }
this.desktop.onLayoutChanged();
} }
public onColumnMoved(column: Column, prevColumn: Column|null) { public onColumnMoved(column: Column, prevColumn: Column|null) {
@@ -256,13 +189,13 @@ class Grid {
this.desktop.autoAdjustScroll(); this.desktop.autoAdjustScroll();
} }
public onColumnWidthChanged(column: Column, oldWidth: number, width: number) { public onColumnWidthChanged(column: Column) {
const nextColumn = this.columns.getNext(column); const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn); this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) { if (!this.userResize) {
this.desktop.autoAdjustScroll(); this.desktop.autoAdjustScroll();
} }
this.desktop.onLayoutChanged();
} }
public onColumnFocused(column: Column) { public onColumnFocused(column: Column) {

View File

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

View File

@@ -2,11 +2,16 @@ class Window {
public column: Column; public column: Column;
public readonly client: ClientWrapper; public readonly client: ClientWrapper;
public height: number; public height: number;
public readonly focusedState: Window.State;
private skipArrange: boolean; private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) { constructor(client: ClientWrapper, column: Column) {
this.client = client; this.client = client;
this.height = client.kwinClient.frameGeometry.height; this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedMode: MaximizedMode.Unmaximized,
};
this.skipArrange = false; this.skipArrange = false;
this.column = column; this.column = column;
column.onWindowAdded(this); column.onWindowAdded(this);
@@ -26,7 +31,26 @@ class Window {
// window is maximized, fullscreen, or being manually resized, prevent fighting with the user // window is maximized, fullscreen, or being manually resized, prevent fighting with the user
return; return;
} }
this.client.place(x, y, width, height);
let maximized = false;
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedMode > MaximizedMode.Unmaximized) {
this.client.setMaximize(
this.focusedState.maximizedMode === MaximizedMode.Horizontally || this.focusedState.maximizedMode === MaximizedMode.Maximized,
this.focusedState.maximizedMode === MaximizedMode.Vertically || this.focusedState.maximizedMode === MaximizedMode.Maximized,
);
maximized = true;
}
if (this.focusedState.fullScreen) {
this.client.setFullScreen(true);
maximized = true;
}
}
if (!maximized) {
this.client.place(x, y, width, height);
}
} }
public focus() { public focus() {
@@ -54,20 +78,36 @@ class Window {
this.column.grid.desktop.onLayoutChanged(); this.column.grid.desktop.onLayoutChanged();
} }
public onMaximizedChanged(horizontally: boolean, vertically: boolean) { public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = horizontally || vertically; const maximized = maximizedMode > MaximizedMode.Unmaximized;
this.skipArrange = maximized; this.skipArrange = maximized;
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.maximizedMode = maximizedMode;
}
this.column.grid.desktop.onLayoutChanged();
} }
public onFullScreenChanged(fullScreen: boolean) { public onFullScreenChanged(fullScreen: boolean) {
this.skipArrange = fullScreen; this.skipArrange = fullScreen;
if (this.isFocused()) { if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !fullScreen; this.client.kwinClient.keepBelow = !fullScreen;
} }
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = fullScreen;
}
if (this.isFocused()) {
this.focusedState.fullScreen = fullScreen;
}
this.column.grid.desktop.onLayoutChanged();
} }
public onUserResize(oldGeometry: QRect, resizeNeighborColumn: boolean) { public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
const newGeometry = this.client.kwinClient.frameGeometry; const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width; const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height; const heightDelta = newGeometry.height - oldGeometry.height;
@@ -102,3 +142,10 @@ class Window {
this.column.onWindowRemoved(this, passFocus); this.column.onWindowRemoved(this, passFocus);
} }
} }
namespace Window {
export type State = {
fullScreen: boolean,
maximizedMode: MaximizedMode,
}
}

View File

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

View File

@@ -1,15 +1,19 @@
class ClientMatcher { class ClientMatcher {
private readonly rules: Map<string, RegExp>; private readonly regex: RegExp;
constructor(rules: Map<string, RegExp>) { constructor(regex: RegExp) {
this.rules = rules; this.regex = regex;
} }
public matches(kwinClient: TopLevel) { public matches(kwinClient: KwinClient) {
const rule = this.rules.get(kwinClient.resourceClass); return this.regex.test(ClientMatcher.getClientString(kwinClient));
if (rule === undefined) { }
return false;
} public static getClientString(kwinClient: KwinClient) {
return rule.test(kwinClient.caption); return ClientMatcher.getRuleString(kwinClient.resourceClass, kwinClient.caption);
}
public static getRuleString(ruleClass: string, ruleCaption: string) {
return ruleClass + "\0" + ruleCaption;
} }
} }

View File

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

View File

@@ -1,24 +1,28 @@
class WindowRuleEnforcer { class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher; private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher; private readonly preferTiling: ClientMatcher;
private readonly followCaption: Set<string>; private readonly followCaption: RegExp;
constructor(windowRules: WindowRule[]) { constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = createWindowRuleMaps(windowRules); const [floatRegex, tileRegex, followCaptionRegex] = WindowRuleEnforcer.createWindowRuleRegexes(windowRules);
this.preferFloating = new ClientMatcher(mapFloat); this.preferFloating = new ClientMatcher(floatRegex);
this.preferTiling = new ClientMatcher(mapTile); this.preferTiling = new ClientMatcher(tileRegex);
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]); this.followCaption = followCaptionRegex;
} }
public shouldTile(kwinClient: TopLevel) { public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && ( return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow && kwinClient.managed && !this.preferFloating.matches(kwinClient) kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
); );
} }
public initClientSignalManager(world: World, kwinClient: TopLevel) { public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.has(kwinClient.resourceClass)) { if (!this.followCaption.test(kwinClient.resourceClass)) {
return null; return null;
} }
@@ -27,59 +31,61 @@ class WindowRuleEnforcer {
manager.connect(kwinClient.captionChanged, () => { manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient); const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (shouldTile) { const desktop = desktopManager.getDesktopForClient(kwinClient);
clientManager.tileClient(kwinClient); if (shouldTile && desktop !== undefined) {
clientManager.tileKwinClient(kwinClient, desktop.grid);
} else { } else {
clientManager.untileClient(kwinClient); clientManager.floatKwinClient(kwinClient);
} }
}); });
}); });
return manager; return manager;
} }
}
function createWindowRuleMaps(windowRules: WindowRule[]) { private static createWindowRuleRegexes(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>(); const floatRegexes: string[] = [];
const mapTile = new Map<string, string[]>(); const tileRegexes: string[] = [];
for (const windowRule of windowRules) { const followCaptionRegexes: string[] = [];
const map = windowRule.tile ? mapTile : mapFloat; for (const windowRule of windowRules) {
let captions = map.get(windowRule.class); const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
if (captions === undefined) { const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
captions = []; const ruleString = ClientMatcher.getRuleString(ruleClass, ruleCaption);
map.set(windowRule.class, captions);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
if (ruleCaption !== ".*") {
followCaptionRegexes.push(ruleClass);
}
} }
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption); return [
WindowRuleEnforcer.joinRegexes(floatRegexes),
WindowRuleEnforcer.joinRegexes(tileRegexes),
WindowRuleEnforcer.joinRegexes(followCaptionRegexes),
];
}
private static parseRegex(rawRule: string | undefined) {
if (rawRule === undefined || rawRule === "" || rawRule === ".*") {
return ".*";
} else {
return rawRule;
} }
} }
return [ private static joinRegexes(regexes: string[]) {
createWindowRuleRegexMap(mapFloat), if (regexes.length === 0) {
createWindowRuleRegexMap(mapTile), return new RegExp("a^"); // match nothing
]; }
}
function createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) { if (regexes.length === 1) {
const regexMap = new Map<string, RegExp>; return new RegExp("^(" + regexes[0] + ")$");
for (const [k, v] of windowRuleMap) { }
regexMap.set(k, joinRegexes(v));
}
return regexMap;
}
function joinRegexes(regexes: string[]) { const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
if (regexes.length == 0) { return new RegExp("^(" + joinedRegexes + ")$");
return new RegExp("");
} }
if (regexes.length == 1) { private static wrapParens(str: string) {
return new RegExp("^" + regexes[0] + "$"); return "(" + str + ")";
} }
const joinedRegexes = regexes.map(wrapParens).join("|");
return new RegExp("^" + joinedRegexes + "$");
}
function wrapParens(str: string) {
return "(" + str + ")";
} }

View File

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

View File

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

View File

@@ -7,3 +7,8 @@ function clamp(value: number, min: number, max: number) {
} }
return value; return value;
} }
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}

View File

@@ -1,49 +1,23 @@
function initWorkspaceSignalHandlers(world: World) { function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: KwinClient) => { manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) { if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities // never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) { Clients.makeTileable(kwinClient);
kwinClient.desktop = workspace.currentDesktop;
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity];
}
} }
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient) clientManager.addClient(kwinClient)
}); });
}); });
manager.connect(workspace.clientRemoved, (kwinClient: AbstractClient) => { manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true); clientManager.removeClient(kwinClient, true);
}); });
}); });
manager.connect(workspace.clientMinimized, (kwinClient: AbstractClient) => { manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: AbstractClient) => {
world.do((clientManager, desktopManager) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: AbstractClient, 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: AbstractClient) => {
if (kwinClient === null) { if (kwinClient === null) {
return; return;
} }
@@ -52,19 +26,27 @@ function initWorkspaceSignalHandlers(world: World) {
}); });
}); });
manager.connect(workspace.currentDesktopChanged, () => { manager.connect(Workspace.currentDesktopChanged, () => {
world.do(() => {}); // re-arrange desktop world.do(() => {}); // re-arrange desktop
}); });
manager.connect(workspace.currentActivityChanged, () => { manager.connect(Workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop world.do(() => {}); // re-arrange desktop
}); });
manager.connect(workspace.numberDesktopsChanged, (oldNumberOfVirtualDesktops: number) => { manager.connect(Workspace.desktopsChanged, () => {
world.updateDesktops(); world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
})
}); });
manager.connect(workspace.virtualScreenSizeChanged, () => { manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
})
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized(); world.onScreenResized();
}); });

View File

@@ -1,13 +1,17 @@
class ClientManager { class ClientManager {
private readonly world: World; private readonly world: World;
private readonly config: ClientManager.Config;
private readonly desktopManager: DesktopManager; private readonly desktopManager: DesktopManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>; private readonly pinManager: PinManager;
private lastFocusedClient: AbstractClient|null; private readonly clientMap: Map<KwinClient, ClientWrapper>;
private lastFocusedClient: KwinClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer; private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(config: Config, world: World, desktopManager: DesktopManager) { constructor(config: Config, world: World, desktopManager: DesktopManager, pinManager: PinManager) {
this.world = world; this.world = world;
this.config = { keepAbove: config.floatingKeepAbove };
this.desktopManager = desktopManager; this.desktopManager = desktopManager;
this.pinManager = pinManager;
this.clientMap = new Map(); this.clientMap = new Map();
this.lastFocusedClient = null; this.lastFocusedClient = null;
@@ -21,25 +25,29 @@ class ClientManager {
this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules); this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules);
} }
public addClient(kwinClient: TopLevel) { public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient)); console.assert(!this.hasClient(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
}
const client = new ClientWrapper( const client = new ClientWrapper(
kwinClient, kwinClient,
new ClientState.Floating(null), constructState,
this.findTransientFor(kwinClient), this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient), this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient),
); );
this.clientMap.set(kwinClient, client); this.clientMap.set(kwinClient, client);
if (kwinClient.dock) {
client.stateManager.setState(() => new ClientState.Docked(this.world, kwinClient), false);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
} }
public removeClient(kwinClient: AbstractClient, passFocus: boolean) { public removeClient(kwinClient: KwinClient, passFocus: boolean) {
console.assert(this.hasClient(kwinClient)); console.assert(this.hasClient(kwinClient));
const client = this.clientMap.get(kwinClient); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
@@ -49,7 +57,7 @@ class ClientManager {
this.clientMap.delete(kwinClient); this.clientMap.delete(kwinClient);
} }
private findTransientFor(kwinClient: AbstractClient) { private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient) { if (!kwinClient.transient) {
return null; return null;
} }
@@ -62,70 +70,98 @@ class ClientManager {
return transientFor; return transientFor;
} }
public minimizeClient(kwinClient: AbstractClient) { public minimizeClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
return; return;
} }
if (client.stateManager.getState() instanceof ClientState.Tiled) { if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.TiledMinimized(), kwinClient === this.lastFocusedClient); client.stateManager.setState(
() => new ClientState.TiledMinimized(this.world, client),
kwinClient === this.lastFocusedClient,
);
} }
} }
public unminimizeClient(kwinClient: AbstractClient) { public tileClient(client: ClientWrapper, grid: Grid) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.TiledMinimized) {
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
}
public tileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) { if (client.stateManager.getState() instanceof ClientState.Tiled) {
return; return;
} }
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false); client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
} }
public untileClient(kwinClient: AbstractClient) { public floatClient(client: ClientWrapper) {
if (client.stateManager.getState() instanceof ClientState.Floating) {
return;
}
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
const client = this.clientMap.get(kwinClient); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
return; return;
} }
if (client.stateManager.getState() instanceof ClientState.Tiled) { this.tileClient(client, grid);
client.stateManager.setState(() => new ClientState.Floating(client), false); }
public floatKwinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
this.floatClient(client);
}
public pinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
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 toggleFloatingClient(kwinClient: TopLevel) { 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); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
return; return;
} }
const clientState = client.stateManager.getState(); const clientState = client.stateManager.getState();
if (clientState instanceof ClientState.Floating && Clients.canTileEver(kwinClient)) { if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient); Clients.makeTileable(kwinClient);
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid; const desktop = this.desktopManager.getDesktopForClient(kwinClient);
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false); if (desktop === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
} else if (clientState instanceof ClientState.Tiled) { } else if (clientState instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(client), false); client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
} }
} }
public hasClient(kwinClient: AbstractClient) { public hasClient(kwinClient: KwinClient) {
return this.clientMap.has(kwinClient); return this.clientMap.has(kwinClient);
} }
public onClientFocused(kwinClient: AbstractClient) { public onClientFocused(kwinClient: KwinClient) {
this.lastFocusedClient = kwinClient; this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient, true); const window = this.findTiledWindow(kwinClient, true);
if (window !== null) { if (window !== null) {
@@ -133,7 +169,7 @@ class ClientManager {
} }
} }
public findTiledWindow(kwinClient: AbstractClient, followTransient: boolean) { public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
const client = this.clientMap.get(kwinClient); const client = this.clientMap.get(kwinClient);
if (client === undefined) { if (client === undefined) {
return null; return null;
@@ -163,3 +199,9 @@ class ClientManager {
this.removeAllClients(); this.removeAllClients();
} }
} }
namespace ClientManager {
export type Config = {
keepAbove: boolean,
}
}

View File

@@ -1,21 +1,22 @@
class ClientWrapper { class ClientWrapper {
public readonly kwinClient: TopLevel; public readonly kwinClient: KwinClient;
public readonly stateManager: ClientState.Manager; public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null; public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[]; private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager; private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null; private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number; public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer; private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor( constructor(
kwinClient: TopLevel, kwinClient: KwinClient,
initialState: ClientState.State, constructInitialState: (client: ClientWrapper) => ClientState.State,
transientFor: ClientWrapper | null, transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null, rulesSignalManager: SignalManager | null,
) { ) {
this.kwinClient = kwinClient; this.kwinClient = kwinClient;
this.stateManager = new ClientState.Manager(initialState);
this.transientFor = transientFor; this.transientFor = transientFor;
this.transients = []; this.transients = [];
if (transientFor !== null) { if (transientFor !== null) {
@@ -25,6 +26,8 @@ class ClientWrapper {
this.rulesSignalManager = rulesSignalManager; this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width; this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer(); this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
} }
public place(x: number, y: number, width: number, height: number) { public place(x: number, y: number, width: number, height: number) {
@@ -33,14 +36,20 @@ class ClientWrapper {
// window is being manually resized, prevent fighting with the user // window is being manually resized, prevent fighting with the user
return; return;
} }
this.kwinClient.frameGeometry = Qt.rect(x, y, width, height); this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement;
if (this.kwinClient.frameGeometry !== this.lastPlacement) {
// frameGeometry assignment failed. This sometimes happens on Wayland
// when a window is off-screen, effectively making it stuck there.
this.kwinClient.frameGeometry.x = x; // This makes it unstuck.
this.kwinClient.frameGeometry = this.lastPlacement;
}
}); });
} }
private moveTransient(dx: number, dy: number, desktopNumber: number) { private moveTransient(dx: number, dy: number, kwinDesktops: KwinDesktop[]) {
// TODO: prevent moving off the grid
if (this.stateManager.getState() instanceof ClientState.Floating) { if (this.stateManager.getState() instanceof ClientState.Floating) {
if (this.kwinClient.desktop === desktopNumber) { if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect( this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx, frame.x + dx,
@@ -51,26 +60,52 @@ class ClientWrapper {
} }
for (const transient of this.transients) { for (const transient of this.transients) {
transient.moveTransient(dx, dy, desktopNumber); transient.moveTransient(dx, dy, kwinDesktops);
} }
} }
} }
public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktops);
}
}
public focus() { public focus() {
workspace.activeClient = this.kwinClient; Workspace.activeWindow = this.kwinClient;
} }
public isFocused() { public isFocused() {
return workspace.activeClient === this.kwinClient; return Workspace.activeWindow === this.kwinClient;
} }
public setMaximize(horizontally: boolean, vertically: boolean) { public setMaximize(horizontally: boolean, vertically: boolean) {
if (!this.kwinClient.maximizable) {
return;
}
if (this.maximizedMode === undefined) {
if (horizontally && vertically) {
this.maximizedMode = MaximizedMode.Maximized;
} else if (horizontally) {
this.maximizedMode = MaximizedMode.Horizontally;
} else if (vertically) {
this.maximizedMode = MaximizedMode.Vertically;
} else {
this.maximizedMode = MaximizedMode.Unmaximized;
}
}
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.setMaximize(vertically, horizontally); this.kwinClient.setMaximize(vertically, horizontally);
}); });
} }
public setFullScreen(fullScreen: boolean) { public setFullScreen(fullScreen: boolean) {
if (!this.kwinClient.fullScreenable) {
return;
}
this.manipulatingGeometry.do(() => { this.manipulatingGeometry.do(() => {
this.kwinClient.fullScreen = fullScreen; this.kwinClient.fullScreen = fullScreen;
}); });
@@ -86,41 +121,17 @@ class ClientWrapper {
return this.kwinClient.shade; return this.kwinClient.shade;
} }
public isManipulatingGeometry() { public getMaximizedMode() {
return this.maximizedMode;
}
public isManipulatingGeometry(newGeometry: QmlRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
return this.manipulatingGeometry.isDoing(); return this.manipulatingGeometry.isDoing();
} }
public prepareForTiling() {
this.kwinClient.keepBelow = true;
this.setFullScreen(false);
if (this.kwinClient.tile !== null) {
this.setMaximize(false, true); // disable quick tile mode
}
this.setMaximize(false, false);
}
public restoreAfterTiling(screenSize: QRect) {
this.kwinClient.keepBelow = false;
this.setShade(false);
this.setFullScreen(false);
if (this.kwinClient.tile === null) {
this.setMaximize(false, false);
}
this.ensureVisible(screenSize);
}
public prepareForFloating() {
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, this.kwinClient.screen, this.kwinClient.desktop);
const clientRect = this.kwinClient.frameGeometry;
const width = this.preferredWidth;
this.place(
clientRect.x,
clientRect.y,
width,
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
);
}
private addTransient(transient: ClientWrapper) { private addTransient(transient: ClientWrapper) {
this.transients.push(transient); this.transients.push(transient);
} }
@@ -130,7 +141,7 @@ class ClientWrapper {
this.transients.splice(i, 1); this.transients.splice(i, 1);
} }
public ensureTransientsVisible(screenSize: QRect) { public ensureTransientsVisible(screenSize: QmlRect) {
for (const transient of this.transients) { for (const transient of this.transients) {
if (transient.stateManager.getState() instanceof ClientState.Floating) { if (transient.stateManager.getState() instanceof ClientState.Floating) {
transient.ensureVisible(screenSize); transient.ensureVisible(screenSize);
@@ -139,15 +150,15 @@ class ClientWrapper {
} }
} }
public ensureVisible(screenSize: QRect) { public ensureVisible(screenSize: QmlRect) {
if (this.kwinClient.desktop !== workspace.currentDesktop) { if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return; return;
} }
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
if (frame.left < 0) { if (frame.left < screenSize.left) {
frame.x = 0; frame.x = screenSize.left;
} else if (frame.right > screenSize.width) { } else if (frame.right > screenSize.right) {
frame.x = screenSize.width - frame.width; frame.x = screenSize.right - frame.width;
} }
} }
@@ -167,20 +178,14 @@ class ClientWrapper {
private static initSignalManager(client: ClientWrapper) { private static initSignalManager(client: ClientWrapper) {
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(client.kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
if (client.stateManager.getState() instanceof ClientState.Tiled) { manager.connect(client.kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
const newGeometry = client.kwinClient.frameGeometry; if (maximizedMode > MaximizedMode.Unmaximized && client.kwinClient.tile !== null) {
const oldCenterX = oldGeometry.x + oldGeometry.width/2; client.kwinClient.tile = null;
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, client.kwinClient.desktop);
}
} }
client.maximizedMode = maximizedMode;
}); });
return manager; return manager;
} }
} }

View File

@@ -1,31 +1,49 @@
namespace Clients { namespace Clients {
export function canTileEver(kwinClient: AbstractClient) { export function canTileEver(kwinClient: KwinClient) {
return kwinClient.resizeable; return kwinClient.moveable && kwinClient.resizeable && !kwinClient.popupWindow;
} }
export function canTileNow(kwinClient: TopLevel) { export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1; return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktops.length === 1 && kwinClient.activities.length === 1;
} }
export function makeTileable(kwinClient: TopLevel) { export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) { if (kwinClient.minimized) {
kwinClient.minimized = false; kwinClient.minimized = false;
} }
if (kwinClient.desktop <= 0) { if (kwinClient.desktops.length !== 1) {
kwinClient.desktop = workspace.currentDesktop; kwinClient.desktops = [Workspace.currentDesktop];
} }
if (kwinClient.activities.length !== 1) { if (kwinClient.activities.length !== 1) {
kwinClient.activities = [workspace.currentActivity]; kwinClient.activities = [Workspace.currentActivity];
} }
} }
export function isMaximizedGeometry(kwinClient: TopLevel) { export function getKwinDesktopApprox(kwinClient: KwinClient) {
const maximizeArea = workspace.clientArea(ClientAreaOption.MaximizeArea, kwinClient.screen, kwinClient.desktop); switch (kwinClient.desktops.length) {
return kwinClient.frameGeometry === maximizeArea; case 0:
return Workspace.currentDesktop;
case 1:
return kwinClient.desktops[0];
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
return Workspace.currentDesktop;
} else {
return kwinClient.desktops[0];
}
}
} }
export function isFullScreenGeometry(kwinClient: TopLevel) { export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.screen, kwinClient.desktop); const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.frameGeometry === fullScreenArea; return kwinClient.frameGeometry === fullScreenArea;
} }
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.includes(kwinDesktop);
}
export function isOnOneOfVirtualDesktops(kwinClient: KwinClient, kwinDesktops: KwinDesktop[]) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.some(d => kwinDesktops.includes(d));
}
} }

View File

@@ -1,105 +1,126 @@
class DesktopManager { class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config; private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig; public readonly layoutConfig: LayoutConfig;
private readonly desktopsPerActivity: Map<string, Desktop[]>; private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private nVirtualDesktops: number; private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) { constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string, currentDesktop: KwinDesktop) {
this.pinManager = pinManager;
this.config = config; this.config = config;
this.layoutConfig = layoutConfig; this.layoutConfig = layoutConfig;
this.desktopsPerActivity = new Map(); this.desktops = new Map();
this.nVirtualDesktops = 0; this.kwinActivities = new Set(Workspace.activities);
this.update() this.kwinDesktops = new Set(Workspace.desktops);
this.addActivity(currentActivity); this.addDesktop(currentActivity, currentDesktop);
} }
public update() { public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
this.setNVirtualDesktops(workspace.desktops); const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
} const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
public getDesktop(activity: string, desktopNumber: number) { return desktop;
const desktopIndex = desktopNumber - 1; } else {
if (desktopIndex >= this.nVirtualDesktops || desktopIndex < 0) { return this.addDesktop(activity, kwinDesktop);
throw new Error("invalid desktop number: " + String(desktopNumber));
} }
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
return this.desktopsPerActivity.get(activity)![desktopIndex];
} }
public getCurrentDesktop() { public getCurrentDesktop() {
return this.getDesktop(workspace.currentActivity, workspace.currentDesktop); return this.getDesktop(Workspace.currentActivity, Workspace.currentDesktop);
} }
public getDesktopInCurrentActivity(desktopNumber: number) { public getDesktopInCurrentActivity(kwinDesktop: KwinDesktop) {
return this.getDesktop(workspace.currentActivity, desktopNumber); return this.getDesktop(Workspace.currentActivity, kwinDesktop);
} }
public getDesktopForClient(kwinClient: TopLevel) { public getDesktopForClient(kwinClient: KwinClient) {
console.assert(kwinClient.activities.length === 1); if (kwinClient.activities.length !== 1 || kwinClient.desktops.length !== 1) {
return this.getDesktop(kwinClient.activities[0], kwinClient.desktop); return undefined;
}
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; return this.getDesktop(kwinClient.activities[0], kwinClient.desktops[0]);
} }
private addDesktopsToActivities(n: number) { private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
for (const desktops of this.desktopsPerActivity.values()) { const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
this.addDesktops(desktops, n); const desktop = new Desktop(kwinDesktop, this.pinManager, this.config, this.layoutConfig);
} this.desktops.set(desktopKey, desktop);
return desktop;
} }
private addDesktops(desktops: Desktop[], n: number) { private static getDesktopKey(activity: string, kwinDesktop: KwinDesktop) {
const nStart = desktops.length; return activity + "|" + kwinDesktop.id;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
desktops.push(new Desktop(desktopNumber, this.config, this.layoutConfig));
}
} }
private removeDesktopsFromActivities(n: number) { public updateActivities() {
const lastRemainingDesktopIndex = this.nVirtualDesktops - n - 1; const newActivities = new Set(Workspace.activities);
for (const desktops of this.desktopsPerActivity.values()) { for (const activity of this.kwinActivities) {
const targetDesktop = desktops[lastRemainingDesktopIndex]; if (!newActivities.has(activity)) {
for (let i = 0; i < n; i++) { this.removeActivity(activity);
const removedDesktop = desktops.pop()!;
removedDesktop.grid.evacuate(targetDesktop.grid);
} }
} }
this.kwinActivities = newActivities;
} }
private addActivity(activity: string) { public updateDesktops() {
const desktops: Desktop[] = []; const newDesktops = new Set(Workspace.desktops);
this.addDesktops(desktops, this.nVirtualDesktops); for (const desktop of this.kwinDesktops) {
this.desktopsPerActivity.set(activity, desktops); if (!newDesktops.has(desktop)) {
this.removeKwinDesktop(desktop);
}
}
this.kwinDesktops = newDesktops;
} }
private removeActivity(activity: string) { private removeActivity(activity: string) {
const removedDesktops = this.desktopsPerActivity.get(activity)!; for (const kwinDesktop of this.kwinDesktops) {
this.desktopsPerActivity.delete(activity); this.destroyDesktop(activity, kwinDesktop);
const targetActivityDesktops = this.desktopsPerActivity.values().next().value; }
for (let i = 0; i < removedDesktops.length; i++) { }
removedDesktops[i].grid.evacuate(targetActivityDesktops[i]);
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
desktop.destroy();
this.desktops.delete(desktopKey);
} }
} }
public destroy() { public destroy() {
for (const desktop of this.desktops()) { for (const desktop of this.desktops.values()) {
desktop.destroy(); desktop.destroy();
} }
} }
public *desktops() { public *getAllDesktops() {
for (const desktops of this.desktopsPerActivity.values()) { for (const desktop of this.desktops.values()) {
for (const desktop of desktops) { yield desktop;
yield desktop; }
}
public getDesktopsForClient(kwinClient: KwinClient) {
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
return desktops;
}
// empty array means all
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
}
} }
} }
} }

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(kwinDesktop: KwinDesktop, screen: QmlRect) {
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
let lots = [baseLot];
for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, kwinDesktop)) {
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

@@ -2,50 +2,72 @@ class World {
public readonly untileOnDrag: boolean; public readonly untileOnDrag: boolean;
private readonly desktopManager: DesktopManager; private readonly desktopManager: DesktopManager;
public readonly clientManager: ClientManager; public readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager; private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer; private readonly screenResizedDelayer: Delayer;
constructor(config: Config) { constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag; this.untileOnDrag = config.untileOnDrag;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this); this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
this.shortcutActions = registerKeyBindings(this, {
manualScrollStep: config.manualScrollStep,
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
});
this.screenResizedDelayer = new Delayer(1000, () => { this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks get taken into account by `workspace.clientArea` // this delay ensures that docks are taken into account by `Workspace.clientArea`
const desktopManager = this.desktopManager; // workaround for bug in Qt5's JS engine for (const desktop of this.desktopManager.getAllDesktops()) {
for (const desktop of desktopManager.desktops()) {
desktop.onLayoutChanged(); desktop.onLayoutChanged();
} }
this.update(); this.update();
}); });
this.pinManager = new PinManager();
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
};
this.desktopManager = new DesktopManager( this.desktopManager = new DesktopManager(
this.pinManager,
{ {
marginTop: config.gapsOuterTop, marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom, marginBottom: config.gapsOuterBottom,
marginLeft: config.gapsOuterLeft, marginLeft: config.gapsOuterLeft,
marginRight: config.gapsOuterRight, marginRight: config.gapsOuterRight,
overscroll: config.overscroll, scroller: config.scrollingLazy ? new LazyScroller() :
config.scrollingCentered ? new CenteredScroller() :
config.scrollingGrouped ? new GroupedScroller() :
console.assert(false),
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
}, },
config, layoutConfig,
workspace.currentActivity, Workspace.currentActivity,
Workspace.currentDesktop,
); );
this.clientManager = new ClientManager(config, this, this.desktopManager); this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients(); this.addExistingClients();
this.update(); this.update();
} }
private addExistingClients() { private addExistingClients() {
const kwinClients = workspace.clientList(); const kwinClients = Workspace.windows;
for (let i = 0; i < kwinClients.length; i++) { for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i]; const kwinClient = kwinClients[i];
this.clientManager.addClient(kwinClient); this.clientManager.addClient(kwinClient);
} }
} }
public updateDesktops() {
this.desktopManager.update();
}
private update() { private update() {
this.desktopManager.getCurrentDesktop().arrange(); this.desktopManager.getCurrentDesktop().arrange();
} }
@@ -56,7 +78,7 @@ class World {
} }
public doIfTiled( public doIfTiled(
kwinClient: AbstractClient, kwinClient: KwinClient,
followTransient: boolean, followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void, f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) { ) {
@@ -74,11 +96,14 @@ class World {
followTransient: boolean, followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void, f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) { ) {
this.doIfTiled(workspace.activeClient, followTransient, f); this.doIfTiled(Workspace.activeWindow, followTransient, f);
} }
public destroy() { public destroy() {
this.workspaceSignalManager.destroy(); this.workspaceSignalManager.destroy();
for (const shortcutAction of this.shortcutActions) {
shortcutAction.destroy();
}
this.clientManager.destroy(); this.clientManager.destroy();
this.desktopManager.destroy(); this.desktopManager.destroy();
} }

View File

@@ -3,7 +3,7 @@ namespace ClientState {
private readonly world: World; private readonly world: World;
private readonly signalManager: SignalManager; private readonly signalManager: SignalManager;
constructor(world: World, kwinClient: TopLevel) { constructor(world: World, kwinClient: KwinClient) {
this.world = world; this.world = world;
this.signalManager = Docked.initSignalManager(world, kwinClient); this.signalManager = Docked.initSignalManager(world, kwinClient);
world.onScreenResized(); world.onScreenResized();
@@ -14,9 +14,9 @@ namespace ClientState {
this.world.onScreenResized(); this.world.onScreenResized();
} }
private static initSignalManager(world: World, kwinClient: TopLevel) { private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => { manager.connect(kwinClient.frameGeometryChanged, () => {
world.onScreenResized(); world.onScreenResized();
}); });
return manager; return manager;

View File

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

View File

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

View File

@@ -0,0 +1,86 @@
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 oldActivities = kwinClient.activities;
let oldDesktops = kwinClient.desktops;
manager.connect(kwinClient.tileChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
return;
}
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
})
});
manager.connect(kwinClient.desktopsChanged, () => {
const changedDesktops = oldDesktops.length === 0 || kwinClient.desktops.length === 0 ?
[] :
union(oldDesktops, kwinClient.desktops);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
desktop.onPinsChanged();
}
});
oldDesktops = kwinClient.desktops;
});
manager.connect(kwinClient.activitiesChanged, () => {
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
[] :
union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
desktop.onPinsChanged();
}
});
oldActivities = kwinClient.activities;
});
return manager;
}
}
}

View File

@@ -1,10 +1,12 @@
namespace ClientState { namespace ClientState {
export class Tiled implements State { export class Tiled implements State {
public readonly window: Window; public readonly window: Window;
private readonly defaultState: Tiled.WindowState;
private readonly signalManager: SignalManager; private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, grid: Grid) { constructor(world: World, client: ClientWrapper, grid: Grid) {
client.prepareForTiling(); this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
Tiled.prepareClientForTiling(client, grid.config);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn()); const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column); const window = new Window(client, column);
@@ -21,7 +23,7 @@ namespace ClientState {
const client = window.client; const client = window.client;
window.destroy(passFocus); window.destroy(passFocus);
client.restoreAfterTiling(grid.desktop.clientArea); Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
} }
private static initSignalManager(world: World, window: Window) { private static initSignalManager(world: World, window: Window) {
@@ -29,93 +31,164 @@ namespace ClientState {
const kwinClient = client.kwinClient; const kwinClient = client.kwinClient;
const manager = new SignalManager(); const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => { manager.connect(kwinClient.desktopsChanged, () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (kwinClient.desktop === -1) { const desktop = desktopManager.getDesktopForClient(kwinClient);
// windows on all desktops are not supported if (desktop === undefined) {
clientManager.untileClient(kwinClient); // windows on multiple desktops are not supported
clientManager.floatKwinClient(kwinClient);
return; return;
} }
Tiled.moveWindowToCorrectGrid(desktopManager, window); Tiled.moveWindowToGrid(window, desktop.grid);
}); });
}); });
manager.connect(kwinClient.activitiesChanged, (kwinClient: AbstractClient) => { manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (kwinClient.activities.length !== 1) { const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported // windows on multiple activities are not supported
clientManager.untileClient(kwinClient); clientManager.floatKwinClient(kwinClient);
return; return;
} }
Tiled.moveWindowToCorrectGrid(desktopManager, window); Tiled.moveWindowToGrid(window, desktop.grid);
}); });
}) })
let lastResize = false; manager.connect(kwinClient.minimizedChanged, () => {
manager.connect(kwinClient.moveResizedChanged, () => { console.assert(kwinClient.minimized);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
if (world.untileOnDrag && kwinClient.move) { clientManager.minimizeClient(kwinClient);
clientManager.untileClient(kwinClient);
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
}); });
}); });
let cursorChangedAfterResizeStart = false; manager.connect(kwinClient.maximizedAboutToChange, (maximizedMode: MaximizedMode) => {
manager.connect(kwinClient.moveResizeCursorChanged, () => { world.do(() => {
cursorChangedAfterResizeStart = true; window.onMaximizedChanged(maximizedMode);
}); });
manager.connect(kwinClient.clientStartUserMovedResized, () => {
cursorChangedAfterResizeStart = false;
}); });
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => { let resizing = false;
world.do((clientManager, desktopManager) => { let resizingBorder = false;
if (kwinClient.resize) { manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart); if (kwinClient.move) {
} else if (!client.isManipulatingGeometry() && !Clients.isMaximizedGeometry(kwinClient) && !Clients.isFullScreenGeometry(kwinClient)) { if (world.untileOnDrag) {
window.onFrameGeometryChanged(); world.do((clientManager, desktopManager) => {
clientManager.floatKwinClient(kwinClient);
});
} }
}); return;
}
if (kwinClient.resize) {
resizing = true;
resizingBorder = Workspace.cursorPos.x > kwinClient.clientGeometry.right ||
Workspace.cursorPos.x < kwinClient.clientGeometry.left;
window.column.grid.onUserResizeStarted();
}
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (resizing) {
resizing = false;
window.column.grid.onUserResizeFinished();
}
});
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
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, resizingBorder));
} else if (
!window.column.grid.isUserResizing() &&
!client.isManipulatingGeometry(newGeometry) &&
client.getMaximizedMode() === MaximizedMode.Unmaximized &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
world.do(() => window.onFrameGeometryChanged());
}
}); });
manager.connect(kwinClient.fullScreenChanged, () => { manager.connect(kwinClient.fullScreenChanged, () => {
window.onFullScreenChanged(kwinClient.fullScreen); world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
}); });
manager.connect(kwinClient.tileChanged, (tile: Tile) => { manager.connect(kwinClient.tileChanged, () => {
if (tile !== null) { // on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
clientManager.untileClient(kwinClient); clientManager.pinClient(kwinClient);
}) });
} }
}); });
return manager; return manager;
} }
private static moveWindowToCorrectGrid(desktopManager: DesktopManager, window: Window) { private static moveWindowToGrid(window: Window, grid: Grid) {
const kwinClient = window.client.kwinClient; if (grid === window.column.grid) {
// window already on the given grid
const oldGrid = window.column.grid;
const newGrid = desktopManager.getDesktopForClient(kwinClient).grid;
if (oldGrid === newGrid) {
// window already on the correct grid
return; return;
} }
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn()); const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn); window.moveToColumn(newColumn);
} }
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;
}
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);
}
client.ensureVisible(screenSize);
}
}
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean,
}
} }
} }

View File

@@ -1,5 +1,31 @@
namespace ClientState { namespace ClientState {
export class TiledMinimized implements State { export class TiledMinimized implements State {
public destroy(passFocus: boolean) {} private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper) {
this.signalManager = TiledMinimized.initSignalManager(world, client);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
}
private static initSignalManager(world: World, client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.minimizedChanged, () => {
console.assert(!client.kwinClient.minimized);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(client.kwinClient);
if (desktop !== undefined) {
clientManager.tileClient(client, desktop.grid);
} else {
clientManager.floatClient(client);
}
});
});
return manager;
}
} }
} }