Compare commits
113 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d4eab03b9 | ||
|
|
7070e59044 | ||
|
|
7f5745b2cf | ||
|
|
c7752bf20a | ||
|
|
8149100aac | ||
|
|
99bf71f0b9 | ||
|
|
2b882768d9 | ||
|
|
bb42e4d3ad | ||
|
|
c7effc8913 | ||
|
|
2c433867f3 | ||
|
|
e995555074 | ||
|
|
6a1e018df1 | ||
|
|
872a67e6e1 | ||
|
|
5a57ba76d8 | ||
|
|
55c6932338 | ||
|
|
b1d6193e48 | ||
|
|
456bbf30b4 | ||
|
|
24c1fa0a38 | ||
|
|
ac7566d2cf | ||
|
|
195f4e6d30 | ||
|
|
e8f2a50420 | ||
|
|
9910bc7041 | ||
|
|
1b592c5b4b | ||
|
|
75384d9fb4 | ||
|
|
dba92d3826 | ||
|
|
dbb95e0470 | ||
|
|
056149440d | ||
|
|
33be23c6dc | ||
|
|
e31669e499 | ||
|
|
caf2b5a146 | ||
|
|
b7f1876a84 | ||
|
|
f108c4a45e | ||
|
|
0aa5d8c3fa | ||
|
|
1674d14453 | ||
|
|
ca8b78ef04 | ||
|
|
877767cea3 | ||
|
|
f1a18b8276 | ||
|
|
8725bc84e0 | ||
|
|
8c23535e86 | ||
|
|
c0e7234fec | ||
|
|
03acbe1280 | ||
|
|
7ef0c60cb8 | ||
|
|
7e1517bbcb | ||
|
|
a23acd056b | ||
|
|
e908f7fe8a | ||
|
|
ba63c1d4e7 | ||
|
|
bf060cef17 | ||
|
|
0f21f94d02 | ||
|
|
6dd356dc53 | ||
|
|
c99cad96c3 | ||
|
|
b4fe71f91b | ||
|
|
099b9f5d6a | ||
|
|
92f6942eef | ||
|
|
9621c2a75b | ||
|
|
36bc1be8c0 | ||
|
|
68b659744c | ||
|
|
c715289282 | ||
|
|
465945429a | ||
|
|
1d7636508b | ||
|
|
47213a71f5 | ||
|
|
75a548977c | ||
|
|
d746b91a88 | ||
|
|
a0d9c49287 | ||
|
|
862cc445bd | ||
|
|
5019a5d702 | ||
|
|
36c7cab137 | ||
|
|
df3c1f4512 | ||
|
|
5f3eaf1eec | ||
|
|
4a680177f6 | ||
|
|
8d807c979b | ||
|
|
c8e37aeb87 | ||
|
|
ad0fe7472c | ||
|
|
a51e45667c | ||
|
|
6615fe6f93 | ||
|
|
6e69139b80 | ||
|
|
97430d5043 | ||
|
|
47f4bbd9b6 | ||
|
|
2d4ad73d16 | ||
|
|
bb4e4f8ebd | ||
|
|
0742975334 | ||
|
|
64457429d0 | ||
|
|
02154f2f5e | ||
|
|
0a2bb4f65d | ||
|
|
6f207e59c4 | ||
|
|
4c987b6c5b | ||
|
|
bca0158df9 | ||
|
|
9feeb0f23e | ||
|
|
0241846ea5 | ||
|
|
3bf3f16f49 | ||
|
|
782a6db56d | ||
|
|
93b6850ffd | ||
|
|
5f0c637d1a | ||
|
|
8829d0b291 | ||
|
|
d37b4bc5d1 | ||
|
|
ead29e5e69 | ||
|
|
ff75d931f6 | ||
|
|
d00d514d30 | ||
|
|
3b919909dc | ||
|
|
0004b6f921 | ||
|
|
24265c56f9 | ||
|
|
dcbc0a474d | ||
|
|
88f170f5c1 | ||
|
|
78ab48ee09 | ||
|
|
b2d81796f8 | ||
|
|
7d27331ce5 | ||
|
|
55e1037a7b | ||
|
|
7820c7d00e | ||
|
|
3d8ca0bc14 | ||
|
|
eaf68b87f9 | ||
|
|
b2dfad6042 | ||
|
|
054808cb38 | ||
|
|
97059fa4f7 | ||
|
|
5e7959c7f4 |
22
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/3-feature_request.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Request a feature
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
These sections are just guidelines, feel free to remove them.
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,4 +2,6 @@
|
|||||||
/package/contents/config/main.xml
|
/package/contents/config/main.xml
|
||||||
/karousel*.tar.gz
|
/karousel*.tar.gz
|
||||||
run-ts-tmp.js
|
run-ts-tmp.js
|
||||||
|
|
||||||
|
/node_modules
|
||||||
/.idea
|
/.idea
|
||||||
|
|||||||
17
Makefile
17
Makefile
@@ -1,15 +1,26 @@
|
|||||||
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
|
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
|
||||||
TESTS := true
|
CHECKS := true
|
||||||
|
|
||||||
.PHONY: *
|
.PHONY: *
|
||||||
|
|
||||||
build: tests
|
build: lint tests
|
||||||
tsc -p ./src/main --outFile ./package/contents/code/main.js
|
tsc -p ./src/main --outFile ./package/contents/code/main.js
|
||||||
mkdir -p ./package/contents/config
|
mkdir -p ./package/contents/config
|
||||||
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
|
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
|
||||||
|
|
||||||
|
npm-install:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
lint: npm-install
|
||||||
|
ifeq (${CHECKS}, true)
|
||||||
|
npx eslint ./src
|
||||||
|
endif
|
||||||
|
|
||||||
|
lint-fix: npm-install
|
||||||
|
npx eslint ./src --fix
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
ifeq (${TESTS}, true)
|
ifeq (${CHECKS}, true)
|
||||||
./run-ts.sh ./src/tests
|
./run-ts.sh ./src/tests
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -22,6 +22,13 @@ Karousel requires the following QML modules:
|
|||||||
- Doesn't support windows on all desktops
|
- Doesn't support windows on all desktops
|
||||||
- Doesn't support windows on multiple activities
|
- Doesn't support windows on multiple activities
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
|
||||||
|
|
||||||
|
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
|
||||||
|
|
||||||
|
Or clone the repo and run `make install` (requires npm, node, and tsc).
|
||||||
|
|
||||||
## Key bindings
|
## Key bindings
|
||||||
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
|
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
|
||||||
Here's the default ones:
|
Here's the default ones:
|
||||||
@@ -32,22 +39,30 @@ Here's the default ones:
|
|||||||
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
|
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||||
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
|
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||||
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
|
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||||
|
| (unassigned) | Move focus to the next window in grid |
|
||||||
|
| (unassigned) | Move focus to the previous window in grid |
|
||||||
| Meta+Home | Move focus to start |
|
| Meta+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) |
|
||||||
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
|
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
|
||||||
| Meta+Shift+W | Move window up |
|
| Meta+Shift+W | Move window up |
|
||||||
| Meta+Shift+S | Move window down |
|
| Meta+Shift+S | Move window down |
|
||||||
|
| (unassigned) | Move window to the next position in grid |
|
||||||
|
| (unassigned) | Move window to the previous position in grid |
|
||||||
| Meta+Shift+Home | Move window to start |
|
| Meta+Shift+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 (One window in the column visible, others shaded; not supported on Wayland) |
|
| Meta+X | Toggle stacked layout for focused column (Only the active window visible) |
|
||||||
| 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+R | Cycle through preset column widths |
|
||||||
|
| Meta+Shift+R | Cycle through preset column widths in reverse |
|
||||||
| Meta+Ctrl+X | Equalize widths of visible columns |
|
| Meta+Ctrl+X | Equalize widths of visible columns |
|
||||||
|
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
|
||||||
|
| Meta+Ctrl+D | Squeeze right column onto the screen |
|
||||||
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
|
| Meta+Alt+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 |
|
||||||
|
|||||||
15
eslint.config.mjs
Normal file
15
eslint.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
extends: [tseslint.configs.stylistic],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"semi": "error",
|
||||||
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
|
"indent": ["error", 4],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
1464
package-lock.json
generated
Normal file
1464
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
package.json
Normal file
6
package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^9.24.0",
|
||||||
|
"typescript-eslint": "^8.30.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,13 +29,23 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="kcfg_cursorFollowsFocus">
|
||||||
|
<property name="text">
|
||||||
|
<string>Cursor follows focus (experimental)</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>When a window gains focus, move the cursor to it</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
|
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Stack columns by default</string>
|
<string>Stack columns by default</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
|
<string>New columns start in stacked mode (only the active window visible)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@@ -101,6 +111,35 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Touchpad scrolling (Wayland only)</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="kcfg_gestureScroll">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable scrolling with touchpad gestures
|
||||||
|
(please don't forget to disable KDE's workspace switching gestures)</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Scroll with a three-finger horizontal swipe gesture</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="kcfg_gestureScrollInvert">
|
||||||
|
<property name="text">
|
||||||
|
<string>Invert scroll direction (Natural scrolling)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox">
|
<widget class="QGroupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@@ -280,14 +319,14 @@
|
|||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item row="6" column="0">
|
<item row="6" column="0">
|
||||||
<widget class="QLabel" name="label_manualScrollStep">
|
<widget class="QLabel" name="label_stackOffsetX">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Manual scroll step size:</string>
|
<string>Horizontal offset for stacked columns:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QSpinBox" name="kcfg_manualScrollStep">
|
<widget class="QSpinBox" name="kcfg_stackOffsetX">
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> px</string>
|
<string> px</string>
|
||||||
</property>
|
</property>
|
||||||
@@ -301,6 +340,78 @@
|
|||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item row="7" column="0">
|
<item row="7" column="0">
|
||||||
|
<widget class="QLabel" name="label_stackOffsetY">
|
||||||
|
<property name="text">
|
||||||
|
<string>Vertical offset for stacked columns:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="7" column="1">
|
||||||
|
<widget class="QSpinBox" name="kcfg_stackOffsetY">
|
||||||
|
<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_manualScrollStep">
|
||||||
|
<property name="text">
|
||||||
|
<string>Manual scroll step size:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="8" column="1">
|
||||||
|
<widget class="QSpinBox" name="kcfg_manualScrollStep">
|
||||||
|
<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="9" column="0">
|
||||||
|
<widget class="QLabel" name="label_gestureScrollStep">
|
||||||
|
<property name="text">
|
||||||
|
<string>Touchpad gesture scrolling speed:</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>The amount to scroll per edge-to-edge gesture</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="9" column="1">
|
||||||
|
<widget class="QSpinBox" name="kcfg_gestureScrollStep">
|
||||||
|
<property name="suffix">
|
||||||
|
<string> px</string>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000</number>
|
||||||
|
</property>
|
||||||
|
<property name="minimum">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="singleStep">
|
||||||
|
<number>100</number>
|
||||||
|
</property>
|
||||||
|
<property name="value">
|
||||||
|
<number>1920</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item row="10" column="0">
|
||||||
<widget class="QLabel" name="label_presetWidths">
|
<widget class="QLabel" name="label_presetWidths">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Preset widths:</string>
|
<string>Preset widths:</string>
|
||||||
@@ -310,7 +421,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="1">
|
<item row="10" column="1">
|
||||||
<widget class="QLineEdit" name="kcfg_presetWidths">
|
<widget class="QLineEdit" name="kcfg_presetWidths">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
|
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
|
||||||
@@ -318,14 +429,14 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item row="8" column="0">
|
<item row="11" column="0">
|
||||||
<widget class="QLabel" name="label_offScreenOpacity">
|
<widget class="QLabel" name="label_offScreenOpacity">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Obscured window opacity:</string>
|
<string>Obscured window opacity:</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="1">
|
<item row="11" column="1">
|
||||||
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
|
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> %</string>
|
<string> %</string>
|
||||||
@@ -338,6 +449,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
|
||||||
@@ -346,6 +458,27 @@
|
|||||||
<string>Window Rules</string>
|
<string>Window Rules</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_desktops">
|
||||||
|
<property name="text">
|
||||||
|
<string>Tiled desktops:</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="kcfg_tiledDesktops">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Regex string to match desktops by desktop name"</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPlainTextEdit" name="kcfg_windowRules">
|
<widget class="QPlainTextEdit" name="kcfg_windowRules">
|
||||||
<property name="tabChangesFocus">
|
<property name="tabChangesFocus">
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ Item {
|
|||||||
qmlBase.karouselInstance.destroy();
|
qmlBase.karouselInstance.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Notification {
|
||||||
|
id: notificationInvalidTiledDesktops
|
||||||
|
componentName: "plasma_workspace"
|
||||||
|
eventId: "notification"
|
||||||
|
title: "Karousel"
|
||||||
|
text: "Your Tiled Desktops regex is malformed, please review your Karousel configuration"
|
||||||
|
flags: Notification.Persistent
|
||||||
|
urgency: Notification.HighUrgency
|
||||||
|
}
|
||||||
|
|
||||||
Notification {
|
Notification {
|
||||||
id: notificationInvalidWindowRules
|
id: notificationInvalidWindowRules
|
||||||
componentName: "plasma_workspace"
|
componentName: "plasma_workspace"
|
||||||
@@ -35,4 +45,29 @@ Item {
|
|||||||
flags: Notification.Persistent
|
flags: Notification.Persistent
|
||||||
urgency: Notification.HighUrgency
|
urgency: Notification.HighUrgency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwipeGestureHandler {
|
||||||
|
direction: SwipeGestureHandler.Direction.Left
|
||||||
|
fingerCount: 3
|
||||||
|
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
|
||||||
|
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
|
||||||
|
onProgressChanged: qmlBase.karouselInstance.gestureScroll(-progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeGestureHandler {
|
||||||
|
direction: SwipeGestureHandler.Direction.Right
|
||||||
|
fingerCount: 3
|
||||||
|
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
|
||||||
|
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
|
||||||
|
onProgressChanged: qmlBase.karouselInstance.gestureScroll(progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
DBusCall {
|
||||||
|
id: moveCursorToFocus
|
||||||
|
|
||||||
|
service: "org.kde.kglobalaccel"
|
||||||
|
path: "/component/kwin"
|
||||||
|
method: "invokeShortcut"
|
||||||
|
arguments: ["MoveMouseToFocus"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"Name": "Peter Fajdiga"
|
"Name": "Peter Fajdiga"
|
||||||
}],
|
}],
|
||||||
"Id": "karousel",
|
"Id": "karousel",
|
||||||
"Version": "0.10",
|
"Version": "0.15",
|
||||||
"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"
|
||||||
|
|||||||
2
src/extern/global.d.ts
vendored
2
src/extern/global.d.ts
vendored
@@ -2,5 +2,7 @@ declare const Qt: Qt;
|
|||||||
declare const KWin: KWin;
|
declare const KWin: KWin;
|
||||||
declare const Workspace: Workspace;
|
declare const Workspace: Workspace;
|
||||||
declare const qmlBase: QmlObject;
|
declare const qmlBase: QmlObject;
|
||||||
|
declare const notificationInvalidTiledDesktops: Notification;
|
||||||
declare const notificationInvalidWindowRules: Notification;
|
declare const notificationInvalidWindowRules: Notification;
|
||||||
declare const notificationInvalidPresetWidths: Notification;
|
declare const notificationInvalidPresetWidths: Notification;
|
||||||
|
declare const moveCursorToFocus: DBusCall;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
type DocsKeyBinding = {
|
interface DocsKeyBinding {
|
||||||
description: string;
|
description: string;
|
||||||
keySequence: string;
|
keySequence: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
function formatDescription(item: {description: string, comment?: string}) {
|
function formatDescription(item: {description: string, comment?: string}) {
|
||||||
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
|
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
|
||||||
@@ -15,9 +15,9 @@ function printCols(...columns: (string[] | string)[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nRows = Math.min(...columns.filter(
|
let nRows = Math.min(...columns.filter(
|
||||||
(column: string[] | string) => column instanceof Array
|
(column: string[] | string) => column instanceof Array,
|
||||||
).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
|
||||||
@@ -28,12 +28,12 @@ function printCols(...columns: (string[] | string)[]) {
|
|||||||
(column: string[] | string) => {
|
(column: string[] | string) => {
|
||||||
if (column instanceof Array) {
|
if (column instanceof Array) {
|
||||||
return Math.max(...column.map(
|
return Math.max(...column.map(
|
||||||
(cell: string) => cell.length
|
(cell: string) => cell.length,
|
||||||
))
|
));
|
||||||
} else {
|
} else {
|
||||||
return column.length;
|
return column.length;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
function getCell(col: number, row: number) {
|
function getCell(col: number, row: number) {
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ class PresetWidths {
|
|||||||
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
|
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public prev(currentWidth: number, minWidth: number, maxWidth: number) {
|
||||||
|
const widths = this.getWidths(minWidth, maxWidth).reverse();
|
||||||
|
const nextIndex = widths.findIndex(width => width < currentWidth);
|
||||||
|
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
|
||||||
|
}
|
||||||
|
|
||||||
public getWidths(minWidth: number, maxWidth: number) {
|
public getWidths(minWidth: number, maxWidth: number) {
|
||||||
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
|
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
|
||||||
widths.sort((a, b) => a - b);
|
widths.sort((a, b) => a - b);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ class ContextualResizer {
|
|||||||
const visibleRange = desktop.getCurrentVisibleRange();
|
const visibleRange = desktop.getCurrentVisibleRange();
|
||||||
const minWidth = column.getMinWidth();
|
const minWidth = column.getMinWidth();
|
||||||
const maxWidth = column.getMaxWidth();
|
const maxWidth = column.getMaxWidth();
|
||||||
if(!column.isVisible(visibleRange, true) || column.getWidth() >= maxWidth) {
|
if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
|
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
|
||||||
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
|
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
|
||||||
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
if (leftVisibleColumn === null || rightVisibleColumn === null) {
|
||||||
console.assert(false); // should at least see self
|
console.assert(false); // should at least see self
|
||||||
return;
|
return;
|
||||||
@@ -31,7 +31,7 @@ class ContextualResizer {
|
|||||||
...this.presetWidths.getWidths(minWidth, maxWidth),
|
...this.presetWidths.getWidths(minWidth, maxWidth),
|
||||||
],
|
],
|
||||||
width => width - column.getWidth(),
|
width => width - column.getWidth(),
|
||||||
)
|
);
|
||||||
if (newWidth === undefined) {
|
if (newWidth === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ class ContextualResizer {
|
|||||||
const visibleRange = desktop.getCurrentVisibleRange();
|
const visibleRange = desktop.getCurrentVisibleRange();
|
||||||
const minWidth = column.getMinWidth();
|
const minWidth = column.getMinWidth();
|
||||||
const maxWidth = column.getMaxWidth();
|
const maxWidth = column.getMaxWidth();
|
||||||
if(!column.isVisible(visibleRange, true) || column.getWidth() <= minWidth) {
|
if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ class ContextualResizer {
|
|||||||
...this.presetWidths.getWidths(minWidth, maxWidth),
|
...this.presetWidths.getWidths(minWidth, maxWidth),
|
||||||
],
|
],
|
||||||
width => column.getWidth() - width,
|
width => column.getWidth() - width,
|
||||||
)
|
);
|
||||||
if (newWidth === undefined) {
|
if (newWidth === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ class CenterClamper {
|
|||||||
}
|
}
|
||||||
const lastColumn = desktop.grid.getLastColumn()!;
|
const lastColumn = desktop.grid.getLastColumn()!;
|
||||||
|
|
||||||
let minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
|
const minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
|
||||||
let maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
|
const maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
|
||||||
return clamp(x, minScroll, maxScroll);
|
return clamp(x, minScroll, maxScroll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class EdgeClamper {
|
class EdgeClamper {
|
||||||
public clampScrollX(desktop: Desktop, x: number) {
|
public clampScrollX(desktop: Desktop, x: number) {
|
||||||
let minScroll = 0;
|
const minScroll = 0;
|
||||||
let maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
|
const maxScroll = desktop.grid.getWidth() - desktop.tilingArea.width;
|
||||||
if (maxScroll < 0) {
|
if (maxScroll < 0) {
|
||||||
return Math.round(maxScroll / 2);
|
return Math.round(maxScroll / 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
type Config = {
|
interface Config {
|
||||||
gapsOuterTop: number;
|
gapsOuterTop: number;
|
||||||
gapsOuterBottom: number;
|
gapsOuterBottom: number;
|
||||||
gapsOuterLeft: number;
|
gapsOuterLeft: number;
|
||||||
gapsOuterRight: number;
|
gapsOuterRight: number;
|
||||||
gapsInnerHorizontal: number;
|
gapsInnerHorizontal: number;
|
||||||
gapsInnerVertical: number;
|
gapsInnerVertical: number;
|
||||||
|
stackOffsetX: number;
|
||||||
|
stackOffsetY: number;
|
||||||
manualScrollStep: number;
|
manualScrollStep: number;
|
||||||
presetWidths: string;
|
presetWidths: string;
|
||||||
offScreenOpacity: number;
|
offScreenOpacity: number;
|
||||||
untileOnDrag: boolean;
|
untileOnDrag: boolean;
|
||||||
|
cursorFollowsFocus: boolean;
|
||||||
stackColumnsByDefault: boolean;
|
stackColumnsByDefault: boolean;
|
||||||
resizeNeighborColumn: boolean;
|
resizeNeighborColumn: boolean;
|
||||||
reMaximize: boolean;
|
reMaximize: boolean;
|
||||||
@@ -16,7 +19,11 @@ type Config = {
|
|||||||
scrollingLazy: boolean;
|
scrollingLazy: boolean;
|
||||||
scrollingCentered: boolean;
|
scrollingCentered: boolean;
|
||||||
scrollingGrouped: boolean;
|
scrollingGrouped: boolean;
|
||||||
|
gestureScroll: boolean;
|
||||||
|
gestureScrollInvert: boolean;
|
||||||
|
gestureScrollStep: number;
|
||||||
tiledKeepBelow: boolean;
|
tiledKeepBelow: boolean;
|
||||||
floatingKeepAbove: boolean;
|
floatingKeepAbove: boolean;
|
||||||
windowRules: string;
|
windowRules: string;
|
||||||
};
|
tiledDesktops: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ const defaultWindowRules = `[
|
|||||||
"class": "(org\\\\.kde\\\\.)?plasmashell",
|
"class": "(org\\\\.kde\\\\.)?plasmashell",
|
||||||
"tile": false
|
"tile": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"class": "(org\\\\.kde\\\\.)?polkit-kde-authentication-agent-1",
|
||||||
|
"tile": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"class": "(org\\\\.kde\\\\.)?kded6",
|
"class": "(org\\\\.kde\\\\.)?kded6",
|
||||||
"tile": false
|
"tile": false
|
||||||
@@ -80,6 +84,16 @@ const configDef = [
|
|||||||
type: "UInt",
|
type: "UInt",
|
||||||
default: 8,
|
default: 8,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "stackOffsetX",
|
||||||
|
type: "UInt",
|
||||||
|
default: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stackOffsetY",
|
||||||
|
type: "UInt",
|
||||||
|
default: 32,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "manualScrollStep",
|
name: "manualScrollStep",
|
||||||
type: "UInt",
|
type: "UInt",
|
||||||
@@ -100,6 +114,11 @@ const configDef = [
|
|||||||
type: "Bool",
|
type: "Bool",
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "cursorFollowsFocus",
|
||||||
|
type: "Bool",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "stackColumnsByDefault",
|
name: "stackColumnsByDefault",
|
||||||
type: "Bool",
|
type: "Bool",
|
||||||
@@ -135,6 +154,21 @@ const configDef = [
|
|||||||
type: "Bool",
|
type: "Bool",
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "gestureScroll",
|
||||||
|
type: "Bool",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gestureScrollInvert",
|
||||||
|
type: "Bool",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "gestureScrollStep",
|
||||||
|
type: "UInt",
|
||||||
|
default: 1920,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "tiledKeepBelow",
|
name: "tiledKeepBelow",
|
||||||
type: "Bool",
|
type: "Bool",
|
||||||
@@ -154,5 +188,10 @@ const configDef = [
|
|||||||
name: "windowRules",
|
name: "windowRules",
|
||||||
type: "String",
|
type: "String",
|
||||||
default: defaultWindowRules,
|
default: defaultWindowRules,
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
name: "tiledDesktops",
|
||||||
|
type: "String",
|
||||||
|
default: ".*",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
3
src/lib/extern/dbuscall.ts
vendored
Normal file
3
src/lib/extern/dbuscall.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
interface DBusCall extends QmlObject {
|
||||||
|
call(): void;
|
||||||
|
}
|
||||||
27
src/lib/extern/kwin.ts
vendored
27
src/lib/extern/kwin.ts
vendored
@@ -1,10 +1,10 @@
|
|||||||
type KWin = {
|
interface KWin {
|
||||||
__brand: "KWin";
|
__brand: "KWin";
|
||||||
|
|
||||||
readConfig(key: string, defaultValue: any): any;
|
readConfig(key: string, defaultValue: any): any;
|
||||||
};
|
}
|
||||||
|
|
||||||
type Workspace = {
|
interface Workspace {
|
||||||
__brand: "Workspace";
|
__brand: "Workspace";
|
||||||
|
|
||||||
readonly activities: string[];
|
readonly activities: string[];
|
||||||
@@ -28,7 +28,7 @@ type Workspace = {
|
|||||||
readonly virtualScreenSizeChanged: QSignal<[]>;
|
readonly virtualScreenSizeChanged: QSignal<[]>;
|
||||||
|
|
||||||
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
|
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
|
||||||
};
|
}
|
||||||
|
|
||||||
const enum ClientAreaOption {
|
const enum ClientAreaOption {
|
||||||
PlacementArea,
|
PlacementArea,
|
||||||
@@ -48,13 +48,12 @@ const enum MaximizedMode {
|
|||||||
Maximized,
|
Maximized,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tile = { __brand: "Tile" };
|
interface Tile { __brand: "Tile" }
|
||||||
type Output = { __brand: "Output" };
|
interface Output { __brand: "Output" }
|
||||||
|
|
||||||
type KwinClient = {
|
interface KwinClient {
|
||||||
__brand: "KwinClient";
|
__brand: "KwinClient";
|
||||||
|
|
||||||
readonly shadeable: boolean;
|
|
||||||
readonly caption: string;
|
readonly caption: string;
|
||||||
readonly minSize: Readonly<QmlSize>;
|
readonly minSize: Readonly<QmlSize>;
|
||||||
readonly transient: boolean;
|
readonly transient: boolean;
|
||||||
@@ -79,7 +78,6 @@ type KwinClient = {
|
|||||||
skipSwitcher: boolean;
|
skipSwitcher: boolean;
|
||||||
keepAbove: boolean;
|
keepAbove: boolean;
|
||||||
keepBelow: boolean;
|
keepBelow: boolean;
|
||||||
shade: boolean;
|
|
||||||
minimized: boolean;
|
minimized: boolean;
|
||||||
frameGeometry: QmlRect;
|
frameGeometry: QmlRect;
|
||||||
desktops: KwinDesktop[]; // empty array means all desktops
|
desktops: KwinDesktop[]; // empty array means all desktops
|
||||||
@@ -98,15 +96,16 @@ type KwinClient = {
|
|||||||
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
|
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
|
||||||
|
|
||||||
setMaximize(vertically: boolean, horizontally: boolean): void;
|
setMaximize(vertically: boolean, horizontally: boolean): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
type KwinDesktop = {
|
interface KwinDesktop {
|
||||||
__brand: "KwinDesktop";
|
__brand: "KwinDesktop";
|
||||||
|
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
};
|
readonly name: string;
|
||||||
|
}
|
||||||
|
|
||||||
type ShortcutHandler = QmlObject & {
|
interface ShortcutHandler extends QmlObject {
|
||||||
readonly activated: QSignal<[]>;
|
readonly activated: QSignal<[]>;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
};
|
}
|
||||||
|
|||||||
4
src/lib/extern/notification.ts
vendored
4
src/lib/extern/notification.ts
vendored
@@ -1,3 +1,3 @@
|
|||||||
type Notification = QmlObject & {
|
interface Notification extends QmlObject {
|
||||||
sendEvent(): void;
|
sendEvent(): void;
|
||||||
};
|
}
|
||||||
|
|||||||
30
src/lib/extern/qt.ts
vendored
30
src/lib/extern/qt.ts
vendored
@@ -1,27 +1,27 @@
|
|||||||
type Console = {
|
interface Console {
|
||||||
__brand: "Console";
|
__brand: "Console";
|
||||||
|
|
||||||
log(...args: any[]): void;
|
log(...args: any[]): void;
|
||||||
assert(assertion: boolean, message?: string): void;
|
assert(assertion: boolean, message?: string): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
type Qt = {
|
interface Qt {
|
||||||
__brand: "Qt";
|
__brand: "Qt";
|
||||||
|
|
||||||
rect(x: number, y: number, width: number, height: number): QmlRect;
|
rect(x: number, y: number, width: number, height: number): QmlRect;
|
||||||
createQmlObject(qml: string, parent: QmlObject): QmlObject;
|
createQmlObject(qml: string, parent: QmlObject): QmlObject;
|
||||||
};
|
}
|
||||||
|
|
||||||
type QmlObject = { __brand: "QmlObject" };
|
interface QmlObject { __brand: "QmlObject" }
|
||||||
|
|
||||||
type QmlPoint = {
|
interface QmlPoint {
|
||||||
__brand: "QmlPoint";
|
__brand: "QmlPoint";
|
||||||
|
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
type QmlRect = {
|
interface QmlRect {
|
||||||
__brand: "QmlRect";
|
__brand: "QmlRect";
|
||||||
|
|
||||||
x: number;
|
x: number;
|
||||||
@@ -32,25 +32,25 @@ type QmlRect = {
|
|||||||
readonly bottom: number; // top + height
|
readonly bottom: number; // top + height
|
||||||
readonly left: number;
|
readonly left: number;
|
||||||
readonly right: number; // left + width
|
readonly right: number; // left + width
|
||||||
};
|
}
|
||||||
|
|
||||||
type QmlSize = {
|
interface QmlSize {
|
||||||
__brand: "QmlSize";
|
__brand: "QmlSize";
|
||||||
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
type QSignal<T extends unknown[]> = {
|
interface QSignal<T extends unknown[]> {
|
||||||
__brand: "QSignal";
|
__brand: "QSignal";
|
||||||
|
|
||||||
connect(handler: (...args: [...T]) => void): void;
|
connect(handler: (...args: [...T]) => void): void;
|
||||||
disconnect(handler: (...args: [...T]) => void): void;
|
disconnect(handler: (...args: [...T]) => void): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
type QmlTimer = QmlObject & {
|
interface QmlTimer extends QmlObject {
|
||||||
interval: number;
|
interval: number;
|
||||||
readonly triggered: QSignal<[]>;
|
readonly triggered: QSignal<[]>;
|
||||||
restart(): void;
|
restart(): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ class Actions {
|
|||||||
if (leftColumn === null) {
|
if (leftColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
leftColumn.focus();
|
leftColumn.getWindowToFocus().focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const rightColumn = grid.getRightColumn(column);
|
const rightColumn = grid.getRightColumn(column);
|
||||||
if (rightColumn === null) {
|
if (rightColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rightColumn.focus();
|
rightColumn.getWindowToFocus().focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly focusUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const aboveWindow = column.getAboveWindow(window);
|
const aboveWindow = column.getAboveWindow(window);
|
||||||
@@ -25,7 +25,7 @@ class Actions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
aboveWindow.focus();
|
aboveWindow.focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly focusDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const belowWindow = column.getBelowWindow(window);
|
const belowWindow = column.getBelowWindow(window);
|
||||||
@@ -33,7 +33,7 @@ class Actions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
belowWindow.focus();
|
belowWindow.focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly focusNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const belowWindow = column.getBelowWindow(window);
|
const belowWindow = column.getBelowWindow(window);
|
||||||
@@ -46,7 +46,7 @@ class Actions {
|
|||||||
}
|
}
|
||||||
rightColumn.getFirstWindow().focus();
|
rightColumn.getFirstWindow().focus();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusPrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly focusPrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const aboveWindow = column.getAboveWindow(window);
|
const aboveWindow = column.getAboveWindow(window);
|
||||||
@@ -59,25 +59,33 @@ class Actions {
|
|||||||
}
|
}
|
||||||
leftColumn.getLastWindow().focus();
|
leftColumn.getLastWindow().focus();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const firstColumn = grid.getFirstColumn();
|
const firstColumn = grid.getFirstColumn();
|
||||||
if (firstColumn === null) {
|
if (firstColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
firstColumn.focus();
|
firstColumn.getWindowToFocus().focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const lastColumn = grid.getLastColumn();
|
const lastColumn = grid.getLastColumn();
|
||||||
if (lastColumn === null) {
|
if (lastColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastColumn.focus();
|
lastColumn.getWindowToFocus().focus();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
if (column.getWindowCount() === 1) {
|
if (column.getWindowCount() === 1) {
|
||||||
@@ -86,40 +94,40 @@ class Actions {
|
|||||||
if (leftColumn === null) {
|
if (leftColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.moveToColumn(leftColumn, true);
|
window.moveToColumn(leftColumn, true, FocusPassing.Type.None);
|
||||||
grid.desktop.autoAdjustScroll();
|
grid.desktop.autoAdjustScroll();
|
||||||
} else {
|
} else {
|
||||||
// move from shared column into own column
|
// move from shared column into own column
|
||||||
const newColumn = new Column(grid, grid.getLeftColumn(column));
|
const newColumn = new Column(grid, grid.getLeftColumn(column));
|
||||||
window.moveToColumn(newColumn, true);
|
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom: boolean = true) => {
|
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom = true) => {
|
||||||
if (column.getWindowCount() === 1) {
|
if (column.getWindowCount() === 1) {
|
||||||
// move from own column into existing column
|
// move from own column into existing column
|
||||||
const rightColumn = grid.getRightColumn(column);
|
const rightColumn = grid.getRightColumn(column);
|
||||||
if (rightColumn === null) {
|
if (rightColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.moveToColumn(rightColumn, bottom);
|
window.moveToColumn(rightColumn, bottom, FocusPassing.Type.None);
|
||||||
grid.desktop.autoAdjustScroll();
|
grid.desktop.autoAdjustScroll();
|
||||||
} else {
|
} else {
|
||||||
// move from shared column into own column
|
// move from shared column into own column
|
||||||
const newColumn = new Column(grid, column);
|
const newColumn = new Column(grid, column);
|
||||||
window.moveToColumn(newColumn, true);
|
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO (optimization): only arrange moved windows
|
// TODO (optimization): only arrange moved windows
|
||||||
public readonly windowMoveUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
column.moveWindowUp(window);
|
column.moveWindowUp(window);
|
||||||
}
|
};
|
||||||
|
|
||||||
// TODO (optimization): only arrange moved windows
|
// TODO (optimization): only arrange moved windows
|
||||||
public readonly windowMoveDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
column.moveWindowDown(window);
|
column.moveWindowDown(window);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMoveNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const canMoveDown = window !== column.getLastWindow();
|
const canMoveDown = window !== column.getLastWindow();
|
||||||
@@ -128,7 +136,7 @@ class Actions {
|
|||||||
} else {
|
} else {
|
||||||
this.windowMoveRight(cm, dm, window, column, grid, false);
|
this.windowMoveRight(cm, dm, window, column, grid, false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMovePrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMovePrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const canMoveUp = window !== column.getFirstWindow();
|
const canMoveUp = window !== column.getFirstWindow();
|
||||||
@@ -137,60 +145,68 @@ class Actions {
|
|||||||
} else {
|
} else {
|
||||||
this.windowMoveLeft(cm, dm, window, column, grid);
|
this.windowMoveLeft(cm, dm, window, column, grid);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const newColumn = new Column(grid, null);
|
const newColumn = new Column(grid, null);
|
||||||
window.moveToColumn(newColumn, true);
|
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const newColumn = new Column(grid, grid.getLastColumn());
|
const newColumn = new Column(grid, grid.getLastColumn());
|
||||||
window.moveToColumn(newColumn, true);
|
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly windowToggleFloating = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly windowToggleFloating = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
if (Workspace.activeWindow === null) {
|
if (Workspace.activeWindow === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cm.toggleFloatingClient(Workspace.activeWindow);
|
cm.toggleFloatingClient(Workspace.activeWindow);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
grid.moveColumnLeft(column);
|
grid.moveColumnLeft(column);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
grid.moveColumnRight(column);
|
grid.moveColumnRight(column);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
grid.moveColumn(column, null);
|
grid.moveColumn(column, null);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
grid.moveColumn(column, grid.getLastColumn());
|
grid.moveColumn(column, grid.getLastColumn());
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnToggleStacked = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnToggleStacked = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
column.toggleStacked();
|
column.toggleStacked();
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnWidthIncrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnWidthIncrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
this.config.columnResizer.increaseWidth(column);
|
this.config.columnResizer.increaseWidth(column);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnWidthDecrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly columnWidthDecrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
this.config.columnResizer.decreaseWidth(column);
|
this.config.columnResizer.decreaseWidth(column);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
|
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
|
||||||
column.setWidth(nextWidth, true);
|
column.setWidth(nextWidth, true);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
public readonly cyclePresetWidthsReverse = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
|
const nextWidth = this.config.presetWidths.prev(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
|
||||||
|
column.setWidth(nextWidth, true);
|
||||||
|
};
|
||||||
|
|
||||||
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const desktop = dm.getCurrentDesktop();
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const visibleRange = desktop.getCurrentVisibleRange();
|
const visibleRange = desktop.getCurrentVisibleRange();
|
||||||
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
|
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
|
||||||
|
|
||||||
@@ -202,15 +218,15 @@ class Actions {
|
|||||||
);
|
);
|
||||||
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
|
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
|
||||||
|
|
||||||
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(
|
desktop.scrollCenterRange(Range.fromRanges(
|
||||||
visibleColumns[0],
|
visibleColumns[0],
|
||||||
visibleColumns[visibleColumns.length - 1],
|
visibleColumns[visibleColumns.length - 1],
|
||||||
));
|
));
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
||||||
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
||||||
if (!focusedColumn.isVisible(visibleRange, true)) {
|
if (!Range.contains(visibleRange, focusedColumn)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,11 +249,11 @@ class Actions {
|
|||||||
break; // don't scroll past the currently focused column
|
break; // don't scroll past the currently focused column
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
|
||||||
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
const visibleRange = grid.desktop.getCurrentVisibleRange();
|
||||||
if (!focusedColumn.isVisible(visibleRange, true)) {
|
if (!Range.contains(visibleRange, focusedColumn)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +276,7 @@ class Actions {
|
|||||||
break; // don't scroll past the currently focused column
|
break; // don't scroll past the currently focused column
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private readonly squeezeColumns = (columns: Column[]) => {
|
private readonly squeezeColumns = (columns: Column[]) => {
|
||||||
const firstColumn = columns[0];
|
const firstColumn = columns[0];
|
||||||
@@ -279,47 +295,66 @@ class Actions {
|
|||||||
|
|
||||||
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
|
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
|
||||||
columns.forEach((column, index) => column.setWidth(widths[index], true));
|
columns.forEach((column, index) => column.setWidth(widths[index], true));
|
||||||
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(firstColumn, lastColumn));
|
desktop.scrollCenterRange(Range.fromRanges(firstColumn, lastColumn));
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
this.gridScroll(dm, -this.config.manualScrollStep);
|
this.gridScroll(dm, -this.config.manualScrollStep);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
this.gridScroll(dm, this.config.manualScrollStep);
|
this.gridScroll(dm, this.config.manualScrollStep);
|
||||||
}
|
};
|
||||||
|
|
||||||
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
|
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
|
||||||
const grid = desktopManager.getCurrentDesktop().grid;
|
const desktop = desktopManager.getCurrentDesktop();
|
||||||
grid.desktop.adjustScroll(amount, false);
|
if (desktop !== undefined) {
|
||||||
}
|
desktop.adjustScroll(amount, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const firstColumn = grid.getFirstColumn();
|
const firstColumn = grid.getFirstColumn();
|
||||||
if (firstColumn === null) {
|
if (firstColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
grid.desktop.scrollToColumn(firstColumn);
|
grid.desktop.scrollToColumn(firstColumn, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const lastColumn = grid.getLastColumn();
|
const lastColumn = grid.getLastColumn();
|
||||||
if (lastColumn === null) {
|
if (lastColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
grid.desktop.scrollToColumn(lastColumn);
|
grid.desktop.scrollToColumn(lastColumn, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
grid.desktop.scrollCenterRange(column);
|
const scrollAmount = Range.minus(column, grid.desktop.getCurrentVisibleRange());
|
||||||
}
|
if (scrollAmount !== 0) {
|
||||||
|
grid.desktop.adjustScroll(scrollAmount, true);
|
||||||
|
} else {
|
||||||
|
grid.desktop.scrollToColumn(column, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||||
if (column === null) {
|
if (column === null) {
|
||||||
return;
|
return;
|
||||||
@@ -330,11 +365,15 @@ class Actions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.desktop.scrollToColumn(leftColumn);
|
grid.desktop.scrollToColumn(leftColumn, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
|
||||||
if (column === null) {
|
if (column === null) {
|
||||||
return;
|
return;
|
||||||
@@ -345,20 +384,24 @@ class Actions {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
grid.desktop.scrollToColumn(rightColumn);
|
grid.desktop.scrollToColumn(rightColumn, false);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
|
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
|
||||||
dm.selectScreen(Workspace.activeScreen);
|
dm.selectScreen(Workspace.activeScreen);
|
||||||
}
|
};
|
||||||
|
|
||||||
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
|
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
|
||||||
const grid = dm.getCurrentDesktop().grid;
|
const desktop = dm.getCurrentDesktop();
|
||||||
|
if (desktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const grid = desktop.grid;
|
||||||
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
const targetColumn = grid.getColumnAtIndex(columnIndex);
|
||||||
if (targetColumn === null) {
|
if (targetColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
targetColumn.focus();
|
targetColumn.getWindowToFocus().focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
public readonly windowMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
public readonly windowMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
|
||||||
@@ -366,7 +409,7 @@ class Actions {
|
|||||||
if (targetColumn === null) {
|
if (targetColumn === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.moveToColumn(targetColumn, true);
|
window.moveToColumn(targetColumn, true, FocusPassing.Type.None);
|
||||||
grid.desktop.autoAdjustScroll();
|
grid.desktop.autoAdjustScroll();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -387,7 +430,11 @@ class Actions {
|
|||||||
if (kwinDesktop === undefined) {
|
if (kwinDesktop === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
|
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
|
||||||
|
if (newDesktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newGrid = newDesktop.grid;
|
||||||
if (newGrid === null || newGrid === oldGrid) {
|
if (newGrid === null || newGrid === oldGrid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -399,7 +446,11 @@ class Actions {
|
|||||||
if (kwinDesktop === undefined) {
|
if (kwinDesktop === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
|
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
|
||||||
|
if (newDesktop === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newGrid = newDesktop.grid;
|
||||||
if (newGrid === null || newGrid === oldGrid) {
|
if (newGrid === null || newGrid === oldGrid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -408,14 +459,17 @@ class Actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace Actions {
|
namespace Actions {
|
||||||
export type Config = {
|
export interface Config {
|
||||||
manualScrollStep: number;
|
manualScrollStep: number;
|
||||||
presetWidths: { next: (currentWidth: number, minWidth: number, maxWidth: number) => number };
|
presetWidths: {
|
||||||
|
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
|
||||||
|
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
|
||||||
|
};
|
||||||
columnResizer: ColumnResizer;
|
columnResizer: ColumnResizer;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ColumnResizer = {
|
export interface ColumnResizer {
|
||||||
increaseWidth(column: Column): void;
|
increaseWidth(column: Column): void;
|
||||||
decreaseWidth(column: Column): void;
|
decreaseWidth(column: Column): void;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
|
|||||||
{
|
{
|
||||||
name: "column-toggle-stacked",
|
name: "column-toggle-stacked",
|
||||||
description: "Toggle stacked layout for focused column",
|
description: "Toggle stacked layout for focused column",
|
||||||
comment: "One window in the column visible, others shaded; not supported on Wayland",
|
comment: "Only the active window visible",
|
||||||
defaultKeySequence: "Meta+X",
|
defaultKeySequence: "Meta+X",
|
||||||
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
|
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
|
||||||
},
|
},
|
||||||
@@ -152,6 +152,12 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
|
|||||||
defaultKeySequence: "Meta+R",
|
defaultKeySequence: "Meta+R",
|
||||||
action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
|
action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "cycle-preset-widths-reverse",
|
||||||
|
description: "Cycle through preset column widths in reverse",
|
||||||
|
defaultKeySequence: "Meta+Shift+R",
|
||||||
|
action: () => world.doIfTiledFocused(actions.cyclePresetWidthsReverse),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "columns-width-equalize",
|
name: "columns-width-equalize",
|
||||||
description: "Equalize widths of visible columns",
|
description: "Equalize widths of visible columns",
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
type KeyBinding = {
|
interface KeyBinding {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
defaultKeySequence?: string;
|
defaultKeySequence?: string;
|
||||||
action: () => void;
|
action: () => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
type NumKeyBinding = {
|
interface NumKeyBinding {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
defaultModifiers: string;
|
defaultModifiers: string;
|
||||||
fKeys: boolean;
|
fKeys: boolean;
|
||||||
action: (i: number) => void;
|
action: (i: number) => void;
|
||||||
};
|
}
|
||||||
|
|
||||||
function catchWrap(f: () => void) {
|
function catchWrap(f: () => void) {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class Column {
|
|||||||
if (targetGrid === this.grid) {
|
if (targetGrid === this.grid) {
|
||||||
this.grid.moveColumn(this, leftColumn);
|
this.grid.moveColumn(this, leftColumn);
|
||||||
} else {
|
} else {
|
||||||
this.grid.onColumnRemoved(this, false);
|
this.grid.onColumnRemoved(this, this.isFocused() ? FocusPassing.Type.Immediate : FocusPassing.Type.None);
|
||||||
this.grid = targetGrid;
|
this.grid = targetGrid;
|
||||||
targetGrid.onColumnAdded(this, leftColumn);
|
targetGrid.onColumnAdded(this, leftColumn);
|
||||||
for (const window of this.windows.iterator()) {
|
for (const window of this.windows.iterator()) {
|
||||||
@@ -195,58 +195,48 @@ class Column {
|
|||||||
return this.focusTaker;
|
return this.focusTaker;
|
||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public getWindowToFocus() {
|
||||||
const window = this.getFocusTaker() ?? this.windows.getFirst();
|
return this.getFocusTaker() ?? this.windows.getFirst()!;
|
||||||
if (window === null) {
|
}
|
||||||
return;
|
|
||||||
|
public isFocused() {
|
||||||
|
const lastFocusedWindow = this.grid.getLastFocusedWindow();
|
||||||
|
if (lastFocusedWindow === null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
window.focus();
|
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
|
||||||
}
|
}
|
||||||
|
|
||||||
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
|
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
|
||||||
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
|
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
|
||||||
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
|
const opacity = Range.contains(visibleRange, this) ? 100 : this.grid.config.offScreenOpacity;
|
||||||
for (const window of this.windows.iterator()) {
|
for (const window of this.windows.iterator()) {
|
||||||
window.client.kwinClient.opacity = opacity;
|
window.client.kwinClient.opacity = opacity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
|
if (this.stacked && this.windows.length() >= 2) {
|
||||||
this.arrangeStacked(x);
|
this.arrangeStacked(x);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let y = this.grid.desktop.tilingArea.y;
|
let y = this.grid.desktop.tilingArea.y;
|
||||||
for (const window of this.windows.iterator()) {
|
for (const window of this.windows.iterator()) {
|
||||||
window.client.setShade(false);
|
|
||||||
window.arrange(x, y, this.width, window.height);
|
window.arrange(x, y, this.width, window.height);
|
||||||
y += window.height + this.grid.config.gapsInnerVertical;
|
y += window.height + this.grid.config.gapsInnerVertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public arrangeStacked(x: number) {
|
public arrangeStacked(x: number) {
|
||||||
const expandedWindow = this.getFocusTaker();
|
const nWindows = this.windows.length();
|
||||||
let collapsedHeight;
|
const windowWidth = this.width - (nWindows - 1) * this.grid.config.stackOffsetX;
|
||||||
for (const window of this.windows.iterator()) {
|
const windowHeight = this.grid.desktop.tilingArea.height - (nWindows - 1) * this.grid.config.stackOffsetY;
|
||||||
if (window === expandedWindow) {
|
|
||||||
window.client.setShade(false);
|
|
||||||
} else {
|
|
||||||
window.client.setShade(true);
|
|
||||||
collapsedHeight = window.client.kwinClient.frameGeometry.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nCollapsed = this.getWindowCount() - 1;
|
let windowX = x;
|
||||||
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
|
let windowY = this.grid.desktop.tilingArea.y;
|
||||||
let y = this.grid.desktop.tilingArea.y;
|
|
||||||
for (const window of this.windows.iterator()) {
|
for (const window of this.windows.iterator()) {
|
||||||
if (window === expandedWindow) {
|
window.arrange(windowX, windowY, windowWidth, windowHeight);
|
||||||
window.arrange(x, y, this.width, expandedHeight);
|
windowX += this.grid.config.stackOffsetX;
|
||||||
y += expandedHeight;
|
windowY += this.grid.config.stackOffsetY;
|
||||||
} else {
|
|
||||||
window.arrange(x, y, this.width, window.height);
|
|
||||||
y += collapsedHeight!;
|
|
||||||
}
|
|
||||||
y += this.grid.config.gapsInnerVertical;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,25 +248,6 @@ class Column {
|
|||||||
this.grid.desktop.onLayoutChanged();
|
this.grid.desktop.onLayoutChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private canStack() {
|
|
||||||
for (const window of this.windows.iterator()) {
|
|
||||||
if (!window.client.kwinClient.shadeable) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
|
||||||
if (fullyVisible) {
|
|
||||||
return this.getLeft() >= visibleRange.getLeft() &&
|
|
||||||
this.getRight() <= visibleRange.getRight();
|
|
||||||
} else {
|
|
||||||
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
|
|
||||||
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public onWindowAdded(window: Window, bottom: boolean) {
|
public onWindowAdded(window: Window, bottom: boolean) {
|
||||||
if (bottom) {
|
if (bottom) {
|
||||||
this.windows.insertEnd(window);
|
this.windows.insertEnd(window);
|
||||||
@@ -298,7 +269,7 @@ class Column {
|
|||||||
this.grid.desktop.onLayoutChanged();
|
this.grid.desktop.onLayoutChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onWindowRemoved(window: Window, passFocus: boolean) {
|
public onWindowRemoved(window: Window, passFocus: FocusPassing.Type) {
|
||||||
const lastWindow = this.windows.length() === 1;
|
const lastWindow = this.windows.length() === 1;
|
||||||
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
|
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
|
||||||
|
|
||||||
@@ -313,8 +284,15 @@ class Column {
|
|||||||
this.destroy(passFocus);
|
this.destroy(passFocus);
|
||||||
} else {
|
} else {
|
||||||
this.resizeWindows();
|
this.resizeWindows();
|
||||||
if (passFocus && windowToFocus !== null) {
|
if (windowToFocus !== null) {
|
||||||
windowToFocus.focus();
|
switch (passFocus) {
|
||||||
|
case FocusPassing.Type.Immediate:
|
||||||
|
windowToFocus.focus();
|
||||||
|
break;
|
||||||
|
case FocusPassing.Type.OnUnfocus:
|
||||||
|
this.grid.focusPasser.request(windowToFocus.client.kwinClient);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,18 +300,18 @@ class Column {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onWindowFocused(window: Window) {
|
public onWindowFocused(window: Window) {
|
||||||
this.grid.onColumnFocused(this);
|
this.grid.onColumnFocused(this, window);
|
||||||
this.focusTaker = window;
|
this.focusTaker = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
public restoreToTiled() {
|
public restoreToTiled(focusedWindow: Window) {
|
||||||
const lastFocusedWindow = this.getFocusTaker();
|
const lastFocusedWindow = this.getFocusTaker();
|
||||||
if (lastFocusedWindow !== null) {
|
if (lastFocusedWindow !== null && lastFocusedWindow !== focusedWindow) {
|
||||||
lastFocusedWindow.restoreToTiled();
|
lastFocusedWindow.restoreToTiled();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private destroy(passFocus: boolean) {
|
private destroy(passFocus: FocusPassing.Type) {
|
||||||
this.grid.onColumnRemoved(this, passFocus);
|
this.grid.onColumnRemoved(this, passFocus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
class Desktop {
|
class Desktop {
|
||||||
public readonly grid: Grid;
|
public readonly grid: Grid;
|
||||||
private scrollX: number;
|
private scrollX: number;
|
||||||
|
private gestureScrollXInitial: number | null;
|
||||||
private dirty: boolean;
|
private dirty: boolean;
|
||||||
private dirtyScroll: boolean;
|
private dirtyScroll: boolean;
|
||||||
private dirtyPins: boolean;
|
private dirtyPins: boolean;
|
||||||
@@ -13,12 +14,14 @@ class Desktop {
|
|||||||
private readonly config: Desktop.Config,
|
private readonly config: Desktop.Config,
|
||||||
private readonly getScreen: () => Output,
|
private readonly getScreen: () => Output,
|
||||||
layoutConfig: LayoutConfig,
|
layoutConfig: LayoutConfig,
|
||||||
|
focusPasser: FocusPassing.Passer,
|
||||||
) {
|
) {
|
||||||
this.scrollX = 0;
|
this.scrollX = 0;
|
||||||
|
this.gestureScrollXInitial = null;
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.dirtyScroll = true;
|
this.dirtyScroll = true;
|
||||||
this.dirtyPins = true;
|
this.dirtyPins = true;
|
||||||
this.grid = new Grid(this, layoutConfig);
|
this.grid = new Grid(this, layoutConfig, focusPasser);
|
||||||
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
|
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
|
||||||
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
|
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
|
||||||
}
|
}
|
||||||
@@ -52,10 +55,10 @@ class Desktop {
|
|||||||
top,
|
top,
|
||||||
right - left,
|
right - left,
|
||||||
bottom - top,
|
bottom - top,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollIntoView(range: Desktop.Range) {
|
public scrollIntoView(range: Range) {
|
||||||
const left = range.getLeft();
|
const left = range.getLeft();
|
||||||
const right = range.getRight();
|
const right = range.getRight();
|
||||||
const initialVisibleRange = this.getCurrentVisibleRange();
|
const initialVisibleRange = this.getCurrentVisibleRange();
|
||||||
@@ -72,10 +75,9 @@ class Desktop {
|
|||||||
this.setScroll(targetScrollX, false);
|
this.setScroll(targetScrollX, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollCenterRange(range: Desktop.Range) {
|
public scrollCenterRange(range: Range) {
|
||||||
const windowCenter = range.getLeft() + range.getWidth() / 2;
|
const scrollAmount = Range.minus(range, this.getCurrentVisibleRange());
|
||||||
const screenCenter = this.scrollX + this.tilingArea.width / 2;
|
this.adjustScroll(scrollAmount, true);
|
||||||
this.adjustScroll(Math.round(windowCenter - screenCenter), true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollCenterVisible(focusedColumn: Column) {
|
public scrollCenterVisible(focusedColumn: Column) {
|
||||||
@@ -91,17 +93,17 @@ class Desktop {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollToColumn(focusedColumn);
|
this.scrollToColumn(focusedColumn, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public scrollToColumn(column: Column) {
|
public scrollToColumn(column: Column, force: boolean) {
|
||||||
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
|
if (force || this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
|
||||||
this.config.scroller.scrollToColumn(this, column);
|
this.config.scroller.scrollToColumn(this, column);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getVisibleRange(scrollX: number) {
|
private getVisibleRange(scrollX: number) {
|
||||||
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
|
return Range.create(scrollX, this.tilingArea.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentVisibleRange() {
|
public getCurrentVisibleRange() {
|
||||||
@@ -125,6 +127,24 @@ class Desktop {
|
|||||||
this.setScroll(this.scrollX + dx, force);
|
this.setScroll(this.scrollX + dx, force);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public gestureScroll(amount: number) {
|
||||||
|
if (!this.config.gestureScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.gestureScrollXInitial === null) {
|
||||||
|
this.gestureScrollXInitial = this.scrollX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.gestureScrollInvert) {
|
||||||
|
amount = -amount;
|
||||||
|
}
|
||||||
|
this.setScroll(this.gestureScrollXInitial + this.config.gestureScrollStep * amount, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public gestureScrollFinish() {
|
||||||
|
this.gestureScrollXInitial = null;
|
||||||
|
}
|
||||||
|
|
||||||
public arrange() {
|
public arrange() {
|
||||||
// TODO (optimization): only arrange visible windows
|
// TODO (optimization): only arrange visible windows
|
||||||
this.updateArea();
|
this.updateArea();
|
||||||
@@ -135,6 +155,10 @@ class Desktop {
|
|||||||
this.dirty = false;
|
this.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public forceArrange() {
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
public onLayoutChanged() {
|
public onLayoutChanged() {
|
||||||
this.dirty = true;
|
this.dirty = true;
|
||||||
this.dirtyScroll = true;
|
this.dirtyScroll = true;
|
||||||
@@ -152,47 +176,16 @@ class Desktop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace Desktop {
|
namespace Desktop {
|
||||||
export type Config = {
|
export interface Config {
|
||||||
marginTop: number;
|
marginTop: number;
|
||||||
marginBottom: number;
|
marginBottom: number;
|
||||||
marginLeft: number;
|
marginLeft: number;
|
||||||
marginRight: number;
|
marginRight: number;
|
||||||
|
gestureScroll: boolean;
|
||||||
|
gestureScrollInvert: boolean;
|
||||||
|
gestureScrollStep: number;
|
||||||
scroller: Desktop.Scroller;
|
scroller: Desktop.Scroller;
|
||||||
clamper: Desktop.Clamper;
|
clamper: Desktop.Clamper;
|
||||||
};
|
|
||||||
|
|
||||||
export type Range = {
|
|
||||||
getLeft(): number;
|
|
||||||
getRight(): number;
|
|
||||||
getWidth(): number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class RangeImpl {
|
|
||||||
private readonly x: number;
|
|
||||||
private readonly width: number;
|
|
||||||
|
|
||||||
constructor(x: number, width: number) {
|
|
||||||
this.x = x;
|
|
||||||
this.width = width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLeft() {
|
|
||||||
return this.x;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRight() {
|
|
||||||
return this.x + this.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getWidth() {
|
|
||||||
return this.width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static fromRanges(leftRange: Range, rightRange: Range) {
|
|
||||||
const left = leftRange.getLeft();
|
|
||||||
const right = rightRange.getRight();
|
|
||||||
return new RangeImpl(left, right - left);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ColumnRange {
|
export class ColumnRange {
|
||||||
@@ -206,7 +199,7 @@ namespace Desktop {
|
|||||||
this.width = initialColumn.getWidth();
|
this.width = initialColumn.getWidth();
|
||||||
}
|
}
|
||||||
|
|
||||||
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
|
public addNeighbors(visibleRange: Range, gap: number) {
|
||||||
const grid = this.left.grid;
|
const grid = this.left.grid;
|
||||||
|
|
||||||
const columnRange = this;
|
const columnRange = this;
|
||||||
@@ -267,11 +260,11 @@ namespace Desktop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Scroller = {
|
export interface Scroller {
|
||||||
scrollToColumn(desktop: Desktop, column: Column): void;
|
scrollToColumn(desktop: Desktop, column: Column): void;
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Clamper = {
|
export interface Clamper {
|
||||||
clampScrollX(desktop: Desktop, x: number): number;
|
clampScrollX(desktop: Desktop, x: number): number;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import Range = Desktop.Range;
|
|
||||||
|
|
||||||
class Grid {
|
class Grid {
|
||||||
public readonly desktop: Desktop;
|
public readonly desktop: Desktop;
|
||||||
public readonly config: LayoutConfig;
|
public readonly config: LayoutConfig;
|
||||||
|
public readonly focusPasser: FocusPassing.Passer;
|
||||||
private readonly columns: LinkedList<Column>;
|
private readonly columns: LinkedList<Column>;
|
||||||
private lastFocusedColumn: Column|null;
|
private lastFocusedColumn: Column|null;
|
||||||
private width: number;
|
private width: number;
|
||||||
private userResize: boolean; // is any part of the grid being resized by the user
|
private userResize: boolean; // is any part of the grid being resized by the user
|
||||||
private readonly userResizeFinishedDelayer: Delayer;
|
private readonly userResizeFinishedDelayer: Delayer;
|
||||||
|
|
||||||
constructor(desktop: Desktop, config: LayoutConfig) {
|
constructor(desktop: Desktop, config: LayoutConfig, focusPasser: FocusPassing.Passer) {
|
||||||
this.desktop = desktop;
|
this.desktop = desktop;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.focusPasser = focusPasser;
|
||||||
this.columns = new LinkedList();
|
this.columns = new LinkedList();
|
||||||
this.lastFocusedColumn = null;
|
this.lastFocusedColumn = null;
|
||||||
this.width = 0;
|
this.width = 0;
|
||||||
@@ -106,19 +106,19 @@ class Grid {
|
|||||||
this.width = x - this.config.gapsInnerHorizontal;
|
this.width = x - this.config.gapsInnerHorizontal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
|
||||||
for (const column of this.columns.iterator()) {
|
for (const column of this.columns.iterator()) {
|
||||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
if (Range.contains(visibleRange, column)) {
|
||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
|
||||||
let last = null;
|
let last = null;
|
||||||
for (const column of this.columns.iterator()) {
|
for (const column of this.columns.iterator()) {
|
||||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
if (Range.contains(visibleRange, column)) {
|
||||||
last = column;
|
last = column;
|
||||||
} else if (last !== null) {
|
} else if (last !== null) {
|
||||||
break;
|
break;
|
||||||
@@ -127,29 +127,14 @@ class Grid {
|
|||||||
return last;
|
return last;
|
||||||
}
|
}
|
||||||
|
|
||||||
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
|
||||||
for (const column of this.columns.iterator()) {
|
for (const column of this.columns.iterator()) {
|
||||||
if (column.isVisible(visibleRange, fullyVisible)) {
|
if (Range.contains(visibleRange, column)) {
|
||||||
yield column;
|
yield column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
|
|
||||||
let width = 0;
|
|
||||||
let nVisible = 0;
|
|
||||||
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
|
|
||||||
width += column.getWidth();
|
|
||||||
nVisible++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nVisible > 0) {
|
|
||||||
width += (nVisible-1) * this.config.gapsInnerHorizontal;
|
|
||||||
}
|
|
||||||
|
|
||||||
return width;
|
|
||||||
}
|
|
||||||
|
|
||||||
public arrange(x: number, visibleRange: Range) {
|
public arrange(x: number, visibleRange: Range) {
|
||||||
for (const column of this.columns.iterator()) {
|
for (const column of this.columns.iterator()) {
|
||||||
column.arrange(x, visibleRange, this.userResize);
|
column.arrange(x, visibleRange, this.userResize);
|
||||||
@@ -173,7 +158,7 @@ class Grid {
|
|||||||
this.desktop.autoAdjustScroll();
|
this.desktop.autoAdjustScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onColumnRemoved(column: Column, passFocus: boolean) {
|
public onColumnRemoved(column: Column, passFocus: FocusPassing.Type) {
|
||||||
const isLastColumn = this.columns.length() === 1;
|
const isLastColumn = this.columns.length() === 1;
|
||||||
const rightColumn = this.getRightColumn(column);
|
const rightColumn = this.getRightColumn(column);
|
||||||
const columnToFocus = isLastColumn ? null : this.getLeftColumn(column) ?? rightColumn;
|
const columnToFocus = isLastColumn ? null : this.getLeftColumn(column) ?? rightColumn;
|
||||||
@@ -185,11 +170,17 @@ class Grid {
|
|||||||
this.columnsSetX(rightColumn);
|
this.columnsSetX(rightColumn);
|
||||||
|
|
||||||
this.desktop.onLayoutChanged();
|
this.desktop.onLayoutChanged();
|
||||||
if (passFocus && columnToFocus !== null) {
|
if (columnToFocus !== null) {
|
||||||
columnToFocus.focus();
|
switch (passFocus) {
|
||||||
} else {
|
case FocusPassing.Type.Immediate:
|
||||||
this.desktop.autoAdjustScroll();
|
columnToFocus.getWindowToFocus().focus();
|
||||||
|
return;
|
||||||
|
case FocusPassing.Type.OnUnfocus:
|
||||||
|
this.focusPasser.request(columnToFocus.getWindowToFocus().client.kwinClient);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this.desktop.autoAdjustScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onColumnWidthChanged(column: Column) {
|
public onColumnWidthChanged(column: Column) {
|
||||||
@@ -201,13 +192,13 @@ class Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onColumnFocused(column: Column) {
|
public onColumnFocused(column: Column, window: Window) {
|
||||||
const lastFocusedColumn = this.getLastFocusedColumn();
|
const lastFocusedColumn = this.getLastFocusedColumn();
|
||||||
if (lastFocusedColumn !== null && lastFocusedColumn !== column) {
|
if (lastFocusedColumn !== null) {
|
||||||
lastFocusedColumn.restoreToTiled();
|
lastFocusedColumn.restoreToTiled(window);
|
||||||
}
|
}
|
||||||
this.lastFocusedColumn = column;
|
this.lastFocusedColumn = column;
|
||||||
this.desktop.scrollToColumn(column);
|
this.desktop.scrollToColumn(column, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onScreenSizeChanged() {
|
public onScreenSizeChanged() {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
type LayoutConfig = {
|
interface LayoutConfig {
|
||||||
gapsInnerHorizontal: number;
|
gapsInnerHorizontal: number;
|
||||||
gapsInnerVertical: number;
|
gapsInnerVertical: number;
|
||||||
|
stackOffsetX: number;
|
||||||
|
stackOffsetY: number;
|
||||||
offScreenOpacity: number;
|
offScreenOpacity: number;
|
||||||
stackColumnsByDefault: boolean;
|
stackColumnsByDefault: boolean;
|
||||||
resizeNeighborColumn: boolean;
|
resizeNeighborColumn: boolean;
|
||||||
@@ -9,4 +11,4 @@ type LayoutConfig = {
|
|||||||
tiledKeepBelow: boolean;
|
tiledKeepBelow: boolean;
|
||||||
maximizedKeepAbove: boolean;
|
maximizedKeepAbove: boolean;
|
||||||
untileOnDrag: boolean;
|
untileOnDrag: boolean;
|
||||||
};
|
}
|
||||||
|
|||||||
47
src/lib/layout/Range.ts
Normal file
47
src/lib/layout/Range.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
interface Range {
|
||||||
|
getLeft(): number;
|
||||||
|
getRight(): number;
|
||||||
|
getWidth(): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Range {
|
||||||
|
export function create(x: number, width: number) {
|
||||||
|
return new Basic(x, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromRanges(leftRange: Range, rightRange: Range) {
|
||||||
|
const left = leftRange.getLeft();
|
||||||
|
const right = rightRange.getRight();
|
||||||
|
return new Basic(left, right - left);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function contains(parent: Range, child: Range) {
|
||||||
|
return child.getLeft() >= parent.getLeft() &&
|
||||||
|
child.getRight() <= parent.getRight();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function minus(a: Range, b: Range) {
|
||||||
|
const aCenter = a.getLeft() + a.getWidth() / 2;
|
||||||
|
const bCenter = b.getLeft() + b.getWidth() / 2;
|
||||||
|
return Math.round(aCenter - bCenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Basic {
|
||||||
|
constructor(
|
||||||
|
private readonly x: number,
|
||||||
|
private readonly width: number,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getLeft() {
|
||||||
|
return this.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRight() {
|
||||||
|
return this.x + this.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getWidth() {
|
||||||
|
return this.width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,20 +8,26 @@ class Window {
|
|||||||
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;
|
||||||
|
|
||||||
|
let maximizedMode = this.client.getMaximizedMode();
|
||||||
|
if (maximizedMode === undefined) {
|
||||||
|
maximizedMode = MaximizedMode.Unmaximized; // defaulting to unmaximized, as this is set in Tiled.prepareClientForTiling
|
||||||
|
}
|
||||||
this.focusedState = {
|
this.focusedState = {
|
||||||
fullScreen: false,
|
fullScreen: this.client.kwinClient.fullScreen,
|
||||||
maximizedMode: MaximizedMode.Unmaximized,
|
maximizedMode: maximizedMode,
|
||||||
};
|
};
|
||||||
this.skipArrange = false;
|
|
||||||
|
this.skipArrange = this.client.kwinClient.fullScreen || maximizedMode !== MaximizedMode.Unmaximized;
|
||||||
this.column = column;
|
this.column = column;
|
||||||
column.onWindowAdded(this, true);
|
column.onWindowAdded(this, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public moveToColumn(targetColumn: Column, bottom: boolean) {
|
public moveToColumn(targetColumn: Column, bottom: boolean, passFocus: FocusPassing.Type) {
|
||||||
if (targetColumn === this.column) {
|
if (targetColumn === this.column) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.column.onWindowRemoved(this, false);
|
this.column.onWindowRemoved(this, passFocus);
|
||||||
this.column = targetColumn;
|
this.column = targetColumn;
|
||||||
targetColumn.onWindowAdded(this, bottom);
|
targetColumn.onWindowAdded(this, bottom);
|
||||||
}
|
}
|
||||||
@@ -54,11 +60,12 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public focus() {
|
public focus() {
|
||||||
if (this.client.isShaded()) {
|
|
||||||
// workaround for KWin deactivating clients when unshading immediately after activation
|
|
||||||
this.client.setShade(false);
|
|
||||||
}
|
|
||||||
this.client.focus();
|
this.client.focus();
|
||||||
|
const kwinClient = this.client.kwinClient;
|
||||||
|
if (!this.isFocused()) {
|
||||||
|
// in some situations focus assignment just doesn't work, let's do it later
|
||||||
|
this.column.grid.focusPasser.request(kwinClient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isFocused() {
|
public isFocused() {
|
||||||
@@ -66,6 +73,14 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onFocused() {
|
public onFocused() {
|
||||||
|
if (this.column.grid.config.reMaximize && (
|
||||||
|
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
|
||||||
|
this.focusedState.fullScreen
|
||||||
|
)) {
|
||||||
|
// We need to maximize/fullscreen this window, but we can't do it here.
|
||||||
|
// We need to do it in `arrange` to ensure it happens after placement.
|
||||||
|
this.column.grid.desktop.forceArrange();
|
||||||
|
}
|
||||||
this.column.onWindowFocused(this);
|
this.column.onWindowFocused(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +90,6 @@ class Window {
|
|||||||
}
|
}
|
||||||
this.client.setFullScreen(false);
|
this.client.setFullScreen(false);
|
||||||
this.client.setMaximize(false, false);
|
this.client.setMaximize(false, false);
|
||||||
this.column.grid.desktop.onLayoutChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onMaximizedChanged(maximizedMode: MaximizedMode) {
|
public onMaximizedChanged(maximizedMode: MaximizedMode) {
|
||||||
@@ -113,14 +127,14 @@ class Window {
|
|||||||
this.column.grid.desktop.onLayoutChanged();
|
this.column.grid.desktop.onLayoutChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.column.onWindowRemoved(this, passFocus);
|
this.column.onWindowRemoved(this, passFocus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Window {
|
namespace Window {
|
||||||
export type State = {
|
export interface State {
|
||||||
fullScreen: boolean;
|
fullScreen: boolean;
|
||||||
maximizedMode: MaximizedMode;
|
maximizedMode: MaximizedMode;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/lib/rules/DesktopFilter.ts
Normal file
30
src/lib/rules/DesktopFilter.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
class DesktopFilter {
|
||||||
|
private readonly desktopRegex: RegExp | null; // null means all desktops
|
||||||
|
|
||||||
|
constructor(desktopsConfig: string) {
|
||||||
|
this.desktopRegex = DesktopFilter.parseDesktopConfig(desktopsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
public shouldWorkOnDesktop(kwinDesktop: KwinDesktop): boolean {
|
||||||
|
if (this.desktopRegex === null) {
|
||||||
|
return true; // Work on all desktops
|
||||||
|
}
|
||||||
|
return this.desktopRegex.test(kwinDesktop.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseDesktopConfig(config: string): RegExp | null {
|
||||||
|
const trimmed = config.trim();
|
||||||
|
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return null; // Empty config means work on all desktops
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new RegExp(`^${trimmed}$`);
|
||||||
|
} catch (e) {
|
||||||
|
notificationInvalidTiledDesktops.sendEvent();
|
||||||
|
log(`Invalid regex pattern in tiledDesktops config: ${trimmed}. Working on all desktops.`);
|
||||||
|
return null; // Invalid regex means work on all desktops as fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
type WindowRule = {
|
interface WindowRule {
|
||||||
class: string | undefined;
|
class: string | undefined;
|
||||||
caption: string | undefined;
|
caption: string | undefined;
|
||||||
tile: boolean;
|
tile: boolean;
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ class WindowRuleEnforcer {
|
|||||||
!kwinClient.transient &&
|
!kwinClient.transient &&
|
||||||
kwinClient.managed &&
|
kwinClient.managed &&
|
||||||
kwinClient.pid > -1 &&
|
kwinClient.pid > -1 &&
|
||||||
|
!kwinClient.fullScreen &&
|
||||||
|
!Clients.isFullScreenGeometry(kwinClient) &&
|
||||||
!this.preferFloating.matches(kwinClient)
|
!this.preferFloating.matches(kwinClient)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,7 +52,7 @@ class WindowRuleEnforcer {
|
|||||||
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
|
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
|
||||||
const ruleString = ClientMatcher.getRuleString(
|
const ruleString = ClientMatcher.getRuleString(
|
||||||
WindowRuleEnforcer.wrapParens(ruleClass),
|
WindowRuleEnforcer.wrapParens(ruleClass),
|
||||||
WindowRuleEnforcer.wrapParens(ruleCaption)
|
WindowRuleEnforcer.wrapParens(ruleCaption),
|
||||||
);
|
);
|
||||||
|
|
||||||
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
|
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ class Delayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initQmlTimer() {
|
function initQmlTimer() {
|
||||||
return <QmlTimer>Qt.createQmlObject(
|
return Qt.createQmlObject(
|
||||||
`import QtQuick 6.0
|
`import QtQuick 6.0
|
||||||
Timer {}`,
|
Timer {}`,
|
||||||
qmlBase
|
qmlBase,
|
||||||
);
|
) as QmlTimer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class ShortcutAction {
|
|||||||
` :
|
` :
|
||||||
"";
|
"";
|
||||||
|
|
||||||
return <ShortcutHandler>Qt.createQmlObject(
|
return Qt.createQmlObject(
|
||||||
`import QtQuick 6.0
|
`import QtQuick 6.0
|
||||||
import org.kde.kwin 3.0
|
import org.kde.kwin 3.0
|
||||||
ShortcutHandler {
|
ShortcutHandler {
|
||||||
@@ -24,14 +24,14 @@ ShortcutHandler {
|
|||||||
text: "Karousel: ${keyBinding.description}";
|
text: "Karousel: ${keyBinding.description}";
|
||||||
${sequenceLine}}`,
|
${sequenceLine}}`,
|
||||||
qmlBase,
|
qmlBase,
|
||||||
);
|
) as ShortcutHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace ShortcutAction {
|
namespace ShortcutAction {
|
||||||
export type KeyBinding = {
|
export interface KeyBinding {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
defaultKeySequence?: string;
|
defaultKeySequence?: string;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,38 +26,38 @@ function fillSpace(availableSpace: number, items: { min: number, max: number }[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildRanges(items: { min: number, max: number }[]) {
|
function buildRanges(items: { min: number, max: number }[]) {
|
||||||
const landmarks = buildLandmarks(items);
|
const fenceposts = extractFenceposts(items);
|
||||||
if (landmarks.length === 1) {
|
if (fenceposts.length === 1) {
|
||||||
return [{
|
return [{
|
||||||
start: landmarks[0].value,
|
start: fenceposts[0].value,
|
||||||
end: landmarks[0].value,
|
end: fenceposts[0].value,
|
||||||
n: items.length,
|
n: items.length,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
const ranges: Range[] = [];
|
const ranges: Range[] = [];
|
||||||
let n = 0;
|
let n = 0;
|
||||||
for (let i = 1; i < landmarks.length; i++) {
|
for (let i = 1; i < fenceposts.length; i++) {
|
||||||
const startLandmark = landmarks[i-1];
|
const startFencepost = fenceposts[i-1];
|
||||||
const endLandmark = landmarks[i];
|
const endFencepost = fenceposts[i];
|
||||||
n = n - startLandmark.nMax + startLandmark.nMin;
|
n = n - startFencepost.nMax + startFencepost.nMin;
|
||||||
ranges.push({
|
ranges.push({
|
||||||
start: startLandmark.value,
|
start: startFencepost.value,
|
||||||
end: endLandmark.value,
|
end: endFencepost.value,
|
||||||
n: n,
|
n: n,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return ranges;
|
return ranges;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLandmarks(items: { min: number, max: number }[]) {
|
function extractFenceposts(items: { min: number, max: number }[]) {
|
||||||
const landmarks = new Map<number, Landmark>();
|
const fenceposts = new Map<number, Fencepost>();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
mapGetOrInit(landmarks, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
|
mapGetOrInit(fenceposts, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
|
||||||
mapGetOrInit(landmarks, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
|
mapGetOrInit(fenceposts, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const array = Array.from(landmarks.values());
|
const array = Array.from(fenceposts.values());
|
||||||
array.sort((a, b) => a.value - b.value);
|
array.sort((a, b) => a.value - b.value);
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
@@ -84,13 +84,13 @@ function fillSpace(availableSpace: number, items: { min: number, max: number }[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Range = {
|
interface Range {
|
||||||
start: number,
|
start: number,
|
||||||
end: number,
|
end: number,
|
||||||
n: number,
|
n: number,
|
||||||
};
|
}
|
||||||
|
|
||||||
type Landmark = {
|
interface Fencepost {
|
||||||
value: number,
|
value: number,
|
||||||
nMin: number,
|
nMin: number,
|
||||||
nMax: number,
|
nMax: number,
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ interface Function {
|
|||||||
|
|
||||||
Function.prototype.partial = function<H extends any[], T extends any[]>(...head: H) {
|
Function.prototype.partial = function<H extends any[], T extends any[]>(...head: H) {
|
||||||
return (...tail: T) => this(...head, ...tail);
|
return (...tail: T) => this(...head, ...tail);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -18,3 +18,15 @@ function rectEquals(a: QmlRect, b: QmlRect) {
|
|||||||
a.width === b.width &&
|
a.width === b.width &&
|
||||||
a.height === b.height;
|
a.height === b.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pointEquals(a: QmlPoint, b: QmlPoint) {
|
||||||
|
return a.x === b.x &&
|
||||||
|
a.y === b.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rectContainsPoint(rect: QmlRect, point: QmlPoint) {
|
||||||
|
return rect.left <= point.x &&
|
||||||
|
rect.right >= point.x &&
|
||||||
|
rect.top <= point.y &&
|
||||||
|
rect.bottom >= point.y;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
function initWorkspaceSignalHandlers(world: World) {
|
function initWorkspaceSignalHandlers(world: World, focusPasser: FocusPassing.Passer) {
|
||||||
const manager = new SignalManager();
|
const manager = new SignalManager();
|
||||||
|
|
||||||
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
|
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
|
||||||
world.do((clientManager, desktopManager) => {
|
world.do((clientManager, desktopManager) => {
|
||||||
clientManager.addClient(kwinClient)
|
clientManager.addClient(kwinClient);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
|
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
|
||||||
world.do((clientManager, desktopManager) => {
|
world.do((clientManager, desktopManager) => {
|
||||||
clientManager.removeClient(kwinClient, true);
|
clientManager.removeClient(kwinClient, FocusPassing.Type.Immediate);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
|
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
|
||||||
if (kwinClient === null) {
|
if (kwinClient === null) {
|
||||||
return;
|
focusPasser.activate();
|
||||||
|
} else {
|
||||||
|
focusPasser.clearIfDifferent(kwinClient);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
clientManager.onClientFocused(kwinClient);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
world.do((clientManager, desktopManager) => {
|
|
||||||
clientManager.onClientFocused(kwinClient);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(Workspace.currentDesktopChanged, () => {
|
manager.connect(Workspace.currentDesktopChanged, () => {
|
||||||
|
|||||||
@@ -31,18 +31,16 @@ class ClientManager {
|
|||||||
console.assert(!this.hasClient(kwinClient));
|
console.assert(!this.hasClient(kwinClient));
|
||||||
|
|
||||||
let constructState: (client: ClientWrapper) => ClientState.State;
|
let constructState: (client: ClientWrapper) => ClientState.State;
|
||||||
|
let desktop: Desktop | undefined;
|
||||||
if (kwinClient.dock) {
|
if (kwinClient.dock) {
|
||||||
constructState = () => new ClientState.Docked(this.world, kwinClient);
|
constructState = () => new ClientState.Docked(this.world, kwinClient);
|
||||||
} else if (
|
} else if (
|
||||||
Clients.canTileEver(kwinClient) &&
|
Clients.canTileEver(kwinClient) &&
|
||||||
!kwinClient.fullScreen &&
|
this.windowRuleEnforcer.shouldTile(kwinClient) &&
|
||||||
!Clients.isFullScreenGeometry(kwinClient) &&
|
(desktop = this.desktopManager.getDesktopForClient(kwinClient)) !== undefined
|
||||||
this.windowRuleEnforcer.shouldTile(kwinClient)
|
|
||||||
) {
|
) {
|
||||||
Clients.makeTileable(kwinClient);
|
Clients.makeTileable(kwinClient);
|
||||||
console.assert(Clients.canTileNow(kwinClient));
|
console.assert(Clients.canTileNow(kwinClient));
|
||||||
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
|
|
||||||
console.assert(desktop !== undefined);
|
|
||||||
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
|
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
|
||||||
} else {
|
} else {
|
||||||
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
|
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
|
||||||
@@ -57,13 +55,16 @@ class ClientManager {
|
|||||||
this.clientMap.set(kwinClient, client);
|
this.clientMap.set(kwinClient, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
|
public removeClient(kwinClient: KwinClient, passFocus: FocusPassing.Type) {
|
||||||
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) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
|
if (kwinClient !== this.lastFocusedClient) {
|
||||||
|
passFocus = FocusPassing.Type.None;
|
||||||
|
}
|
||||||
|
client.destroy(passFocus);
|
||||||
this.clientMap.delete(kwinClient);
|
this.clientMap.delete(kwinClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +87,10 @@ class ClientManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||||
|
const passFocus = kwinClient === this.lastFocusedClient ? FocusPassing.Type.Immediate : FocusPassing.Type.None;
|
||||||
client.stateManager.setState(
|
client.stateManager.setState(
|
||||||
() => new ClientState.TiledMinimized(this.world, client),
|
() => new ClientState.TiledMinimized(this.world, client),
|
||||||
kwinClient === this.lastFocusedClient,
|
passFocus,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,14 +99,14 @@ class ClientManager {
|
|||||||
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
if (client.stateManager.getState() instanceof ClientState.Tiled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
|
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public floatClient(client: ClientWrapper) {
|
public floatClient(client: ClientWrapper) {
|
||||||
if (client.stateManager.getState() instanceof ClientState.Floating) {
|
if (client.stateManager.getState() instanceof ClientState.Floating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
|
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
|
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
|
||||||
@@ -133,7 +135,7 @@ class ClientManager {
|
|||||||
kwinClient.tile = null;
|
kwinClient.tile = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
|
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), FocusPassing.Type.None);
|
||||||
this.pinManager.addClient(kwinClient);
|
this.pinManager.addClient(kwinClient);
|
||||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||||
desktop.onPinsChanged();
|
desktop.onPinsChanged();
|
||||||
@@ -146,7 +148,7 @@ class ClientManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
|
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
|
||||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
|
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), FocusPassing.Type.None);
|
||||||
this.pinManager.removeClient(kwinClient);
|
this.pinManager.removeClient(kwinClient);
|
||||||
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
|
||||||
desktop.onPinsChanged();
|
desktop.onPinsChanged();
|
||||||
@@ -166,9 +168,9 @@ class ClientManager {
|
|||||||
if (desktop === undefined) {
|
if (desktop === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
|
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), FocusPassing.Type.None);
|
||||||
} else if (clientState instanceof ClientState.Tiled) {
|
} else if (clientState instanceof ClientState.Tiled) {
|
||||||
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
|
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +208,7 @@ class ClientManager {
|
|||||||
|
|
||||||
private removeAllClients() {
|
private removeAllClients() {
|
||||||
for (const kwinClient of Array.from(this.clientMap.keys())) {
|
for (const kwinClient of Array.from(this.clientMap.keys())) {
|
||||||
this.removeClient(kwinClient, false);
|
this.removeClient(kwinClient, FocusPassing.Type.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +218,7 @@ class ClientManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace ClientManager {
|
namespace ClientManager {
|
||||||
export type Config = {
|
export interface Config {
|
||||||
floatingKeepAbove: boolean;
|
floatingKeepAbove: boolean;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class ClientWrapper {
|
|||||||
|
|
||||||
public setMaximize(horizontally: boolean, vertically: boolean) {
|
public setMaximize(horizontally: boolean, vertically: boolean) {
|
||||||
if (!this.kwinClient.maximizable) {
|
if (!this.kwinClient.maximizable) {
|
||||||
|
this.maximizedMode = MaximizedMode.Unmaximized;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,16 +109,6 @@ class ClientWrapper {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public setShade(shade: boolean) {
|
|
||||||
this.manipulatingGeometry.do(() => {
|
|
||||||
this.kwinClient.shade = shade;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public isShaded() {
|
|
||||||
return this.kwinClient.shade;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMaximizedMode() {
|
public getMaximizedMode() {
|
||||||
return this.maximizedMode;
|
return this.maximizedMode;
|
||||||
}
|
}
|
||||||
@@ -159,7 +150,7 @@ class ClientWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.stateManager.destroy(passFocus);
|
this.stateManager.destroy(passFocus);
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
if (this.rulesSignalManager !== null) {
|
if (this.rulesSignalManager !== null) {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ namespace Clients {
|
|||||||
];
|
];
|
||||||
|
|
||||||
export function canTileEver(kwinClient: KwinClient) {
|
export function canTileEver(kwinClient: KwinClient) {
|
||||||
return kwinClient.moveable &&
|
const shapeable = (kwinClient.moveable && kwinClient.resizeable) || kwinClient.fullScreen; // full-screen windows may become shapeable after exiting full-screen mode
|
||||||
kwinClient.resizeable &&
|
return shapeable &&
|
||||||
!kwinClient.popupWindow &&
|
!kwinClient.popupWindow &&
|
||||||
!prohibitedClasses.includes(kwinClient.resourceClass);
|
!prohibitedClasses.includes(kwinClient.resourceClass);
|
||||||
}
|
}
|
||||||
@@ -32,23 +32,23 @@ namespace Clients {
|
|||||||
|
|
||||||
export function getKwinDesktopApprox(kwinClient: KwinClient) {
|
export function getKwinDesktopApprox(kwinClient: KwinClient) {
|
||||||
switch (kwinClient.desktops.length) {
|
switch (kwinClient.desktops.length) {
|
||||||
case 0:
|
case 0:
|
||||||
|
return Workspace.currentDesktop;
|
||||||
|
case 1:
|
||||||
|
return kwinClient.desktops[0];
|
||||||
|
default:
|
||||||
|
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
|
||||||
return Workspace.currentDesktop;
|
return Workspace.currentDesktop;
|
||||||
case 1:
|
} else {
|
||||||
return kwinClient.desktops[0];
|
return kwinClient.desktops[0];
|
||||||
default:
|
}
|
||||||
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
|
|
||||||
return Workspace.currentDesktop;
|
|
||||||
} else {
|
|
||||||
return kwinClient.desktops[0];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFullScreenGeometry(kwinClient: KwinClient) {
|
export function isFullScreenGeometry(kwinClient: KwinClient) {
|
||||||
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
|
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
|
||||||
return kwinClient.clientGeometry.width === fullScreenArea.width &&
|
return kwinClient.clientGeometry.width >= fullScreenArea.width &&
|
||||||
kwinClient.clientGeometry.height === fullScreenArea.height;
|
kwinClient.clientGeometry.height >= fullScreenArea.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
|
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ class DesktopManager {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly pinManager: PinManager,
|
private readonly pinManager: PinManager,
|
||||||
private readonly config: Desktop.Config,
|
private readonly config: Desktop.Config,
|
||||||
public readonly layoutConfig: LayoutConfig,
|
private readonly layoutConfig: LayoutConfig,
|
||||||
currentActivity: string,
|
private readonly focusPasser: FocusPassing.Passer,
|
||||||
currentDesktop: KwinDesktop,
|
private readonly desktopFilter: DesktopFilter,
|
||||||
) {
|
) {
|
||||||
this.pinManager = pinManager;
|
this.pinManager = pinManager;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
@@ -18,10 +18,12 @@ class DesktopManager {
|
|||||||
this.selectedScreen = Workspace.activeScreen;
|
this.selectedScreen = Workspace.activeScreen;
|
||||||
this.kwinActivities = new Set(Workspace.activities);
|
this.kwinActivities = new Set(Workspace.activities);
|
||||||
this.kwinDesktops = new Set(Workspace.desktops);
|
this.kwinDesktops = new Set(Workspace.desktops);
|
||||||
this.addDesktop(currentActivity, currentDesktop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
|
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
|
||||||
|
if (!this.desktopFilter.shouldWorkOnDesktop(kwinDesktop)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
|
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
|
||||||
const desktop = this.desktops.get(desktopKey);
|
const desktop = this.desktops.get(desktopKey);
|
||||||
if (desktop !== undefined) {
|
if (desktop !== undefined) {
|
||||||
@@ -54,6 +56,7 @@ class DesktopManager {
|
|||||||
this.config,
|
this.config,
|
||||||
() => this.selectedScreen,
|
() => this.selectedScreen,
|
||||||
this.layoutConfig,
|
this.layoutConfig,
|
||||||
|
this.focusPasser,
|
||||||
);
|
);
|
||||||
this.desktops.set(desktopKey, desktop);
|
this.desktops.set(desktopKey, desktop);
|
||||||
return desktop;
|
return desktop;
|
||||||
|
|||||||
51
src/lib/world/FocusPassing.ts
Normal file
51
src/lib/world/FocusPassing.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
namespace FocusPassing {
|
||||||
|
export const enum Type {
|
||||||
|
None,
|
||||||
|
Immediate,
|
||||||
|
OnUnfocus,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Passer {
|
||||||
|
private currentRequest: Request | null = null;
|
||||||
|
|
||||||
|
public request(target: KwinClient) {
|
||||||
|
this.currentRequest = new Request(target, Date.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear() {
|
||||||
|
this.currentRequest = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearIfDifferent(kwinClient: KwinClient) {
|
||||||
|
if (this.currentRequest !== null && this.currentRequest.target !== kwinClient) {
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public activate() {
|
||||||
|
if (this.currentRequest === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentRequest.isExpired()) {
|
||||||
|
this.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Workspace.activeWindow = this.currentRequest.target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Request {
|
||||||
|
private static readonly validMs = 200;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly target: KwinClient,
|
||||||
|
private readonly time: number,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public isExpired() {
|
||||||
|
return Date.now() - this.time > Request.validMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,12 +5,16 @@ class World {
|
|||||||
private readonly workspaceSignalManager: SignalManager;
|
private readonly workspaceSignalManager: SignalManager;
|
||||||
private readonly shortcutActions: ShortcutAction[];
|
private readonly shortcutActions: ShortcutAction[];
|
||||||
private readonly screenResizedDelayer: Delayer;
|
private readonly screenResizedDelayer: Delayer;
|
||||||
|
private readonly cursorFollowsFocus: boolean;
|
||||||
|
|
||||||
constructor(config: Config) {
|
constructor(config: Config) {
|
||||||
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
|
const focusPasser = new FocusPassing.Passer();
|
||||||
|
this.workspaceSignalManager = initWorkspaceSignalHandlers(this, focusPasser);
|
||||||
|
this.cursorFollowsFocus = config.cursorFollowsFocus;
|
||||||
|
|
||||||
let presetWidths = {
|
let presetWidths = {
|
||||||
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
|
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
|
||||||
|
prev: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
|
||||||
getWidths: (minWidth: number, maxWidth: number): number[] => [],
|
getWidths: (minWidth: number, maxWidth: number): number[] => [],
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -39,6 +43,8 @@ class World {
|
|||||||
const layoutConfig = {
|
const layoutConfig = {
|
||||||
gapsInnerHorizontal: config.gapsInnerHorizontal,
|
gapsInnerHorizontal: config.gapsInnerHorizontal,
|
||||||
gapsInnerVertical: config.gapsInnerVertical,
|
gapsInnerVertical: config.gapsInnerVertical,
|
||||||
|
stackOffsetX: config.stackOffsetX,
|
||||||
|
stackOffsetY: config.stackOffsetY,
|
||||||
offScreenOpacity: config.offScreenOpacity / 100.0,
|
offScreenOpacity: config.offScreenOpacity / 100.0,
|
||||||
stackColumnsByDefault: config.stackColumnsByDefault,
|
stackColumnsByDefault: config.stackColumnsByDefault,
|
||||||
resizeNeighborColumn: config.resizeNeighborColumn,
|
resizeNeighborColumn: config.resizeNeighborColumn,
|
||||||
@@ -58,10 +64,13 @@ class World {
|
|||||||
marginRight: config.gapsOuterRight,
|
marginRight: config.gapsOuterRight,
|
||||||
scroller: World.createScroller(config),
|
scroller: World.createScroller(config),
|
||||||
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
|
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
|
||||||
|
gestureScroll: config.gestureScroll,
|
||||||
|
gestureScrollInvert: config.gestureScrollInvert,
|
||||||
|
gestureScrollStep: config.gestureScrollStep,
|
||||||
},
|
},
|
||||||
layoutConfig,
|
layoutConfig,
|
||||||
Workspace.currentActivity,
|
focusPasser,
|
||||||
Workspace.currentDesktop,
|
new DesktopFilter(config.tiledDesktops),
|
||||||
);
|
);
|
||||||
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
|
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
|
||||||
this.addExistingClients();
|
this.addExistingClients();
|
||||||
@@ -82,15 +91,32 @@ class World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private addExistingClients() {
|
private addExistingClients() {
|
||||||
const kwinClients = Workspace.windows;
|
for (const kwinClient of Workspace.windows) {
|
||||||
for (let i = 0; i < kwinClients.length; i++) {
|
|
||||||
const kwinClient = kwinClients[i];
|
|
||||||
this.clientManager.addClient(kwinClient);
|
this.clientManager.addClient(kwinClient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
this.desktopManager.getCurrentDesktop().arrange();
|
const currentDesktop = this.desktopManager.getCurrentDesktop();
|
||||||
|
if (currentDesktop !== undefined) {
|
||||||
|
currentDesktop.arrange();
|
||||||
|
this.moveCursorToFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveCursorToFocus() {
|
||||||
|
if (this.cursorFollowsFocus && Workspace.activeWindow !== null) {
|
||||||
|
// Only move cursor for tiled windows
|
||||||
|
const tiledWindow = this.clientManager.findTiledWindow(Workspace.activeWindow);
|
||||||
|
if (tiledWindow === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cursorAlreadyInFocus = rectContainsPoint(Workspace.activeWindow.frameGeometry, Workspace.cursorPos);
|
||||||
|
if (cursorAlreadyInFocus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
moveCursorToFocus.call();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
|
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
|
||||||
@@ -121,6 +147,24 @@ class World {
|
|||||||
this.doIfTiled(Workspace.activeWindow, f);
|
this.doIfTiled(Workspace.activeWindow, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public gestureScroll(amount: number) {
|
||||||
|
this.do((clientManager, desktopManager) => {
|
||||||
|
const currentDesktop = desktopManager.getCurrentDesktop();
|
||||||
|
if (currentDesktop !== undefined) {
|
||||||
|
currentDesktop.gestureScroll(amount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public gestureScrollFinish() {
|
||||||
|
this.do((clientManager, desktopManager) => {
|
||||||
|
const currentDesktop = desktopManager.getCurrentDesktop();
|
||||||
|
if (currentDesktop !== undefined) {
|
||||||
|
currentDesktop.gestureScrollFinish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public destroy() {
|
public destroy() {
|
||||||
this.workspaceSignalManager.destroy();
|
this.workspaceSignalManager.destroy();
|
||||||
for (const shortcutAction of this.shortcutActions) {
|
for (const shortcutAction of this.shortcutActions) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace ClientState {
|
|||||||
world.onScreenResized();
|
world.onScreenResized();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
this.world.onScreenResized();
|
this.world.onScreenResized();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ namespace ClientState {
|
|||||||
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
|
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
if (this.config.floatingKeepAbove) {
|
|
||||||
this.client.kwinClient.keepAbove = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to `Tiled.restoreClientAfterTiling`
|
// TODO: move to `Tiled.restoreClientAfterTiling`
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ClientState {
|
|||||||
this.state = initialState;
|
this.state = initialState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setState(constructNewState: () => State, passFocus: boolean) {
|
public setState(constructNewState: () => State, passFocus: FocusPassing.Type) {
|
||||||
this.state.destroy(passFocus);
|
this.state.destroy(passFocus);
|
||||||
this.state = constructNewState();
|
this.state = constructNewState();
|
||||||
}
|
}
|
||||||
@@ -15,12 +15,12 @@ namespace ClientState {
|
|||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.state.destroy(passFocus);
|
this.state.destroy(passFocus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = {
|
export interface State {
|
||||||
destroy(passFocus: boolean): void;
|
destroy(passFocus: FocusPassing.Type): void;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ namespace ClientState {
|
|||||||
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
|
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
if (this.config.floatingKeepAbove) {
|
|
||||||
this.kwinClient.keepAbove = true;
|
|
||||||
}
|
|
||||||
this.pinManager.removeClient(this.kwinClient);
|
this.pinManager.removeClient(this.kwinClient);
|
||||||
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
|
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
|
||||||
desktop.onPinsChanged();
|
desktop.onPinsChanged();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace ClientState {
|
|||||||
this.signalManager = Tiled.initSignalManager(world, window, grid.config);
|
this.signalManager = Tiled.initSignalManager(world, window, grid.config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
|
|
||||||
const window = this.window;
|
const window = this.window;
|
||||||
@@ -54,7 +54,7 @@ namespace ClientState {
|
|||||||
}
|
}
|
||||||
Tiled.moveWindowToGrid(window, desktop.grid);
|
Tiled.moveWindowToGrid(window, desktop.grid);
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
manager.connect(kwinClient.minimizedChanged, () => {
|
manager.connect(kwinClient.minimizedChanged, () => {
|
||||||
console.assert(kwinClient.minimized);
|
console.assert(kwinClient.minimized);
|
||||||
@@ -69,6 +69,7 @@ namespace ClientState {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let moving = false;
|
||||||
let resizing = false;
|
let resizing = false;
|
||||||
let resizeStartWidth = 0;
|
let resizeStartWidth = 0;
|
||||||
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
|
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
|
||||||
@@ -78,6 +79,8 @@ namespace ClientState {
|
|||||||
world.do((clientManager, desktopManager) => {
|
world.do((clientManager, desktopManager) => {
|
||||||
clientManager.floatClient(client);
|
clientManager.floatClient(client);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
moving = true;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,6 +102,10 @@ namespace ClientState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
|
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
|
||||||
|
if (moving) {
|
||||||
|
moving = false;
|
||||||
|
world.do(() => window.column.grid.desktop.onLayoutChanged()); // move the dragged window back to its position
|
||||||
|
}
|
||||||
if (resizing) {
|
if (resizing) {
|
||||||
resizing = false;
|
resizing = false;
|
||||||
resizeNeighbor = undefined;
|
resizeNeighbor = undefined;
|
||||||
@@ -106,7 +113,7 @@ namespace ClientState {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
|
const externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
|
||||||
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
|
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
|
||||||
// on Wayland, this fires after `tileChanged`
|
// on Wayland, this fires after `tileChanged`
|
||||||
if (kwinClient.tile !== null) {
|
if (kwinClient.tile !== null) {
|
||||||
@@ -160,7 +167,15 @@ namespace ClientState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(kwinClient.fullScreenChanged, () => {
|
manager.connect(kwinClient.fullScreenChanged, () => {
|
||||||
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
|
world.do((clientManager, desktopManager) => {
|
||||||
|
// some clients only turn out to be untileable after exiting full-screen mode
|
||||||
|
if (!Clients.canTileEver(kwinClient)) {
|
||||||
|
clientManager.floatClient(client);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onFullScreenChanged(kwinClient.fullScreen);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
manager.connect(kwinClient.tileChanged, () => {
|
manager.connect(kwinClient.tileChanged, () => {
|
||||||
@@ -194,17 +209,26 @@ namespace ClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
|
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
|
||||||
window.moveToColumn(newColumn, true);
|
const passFocus = window.isFocused() ? FocusPassing.Type.OnUnfocus : FocusPassing.Type.None;
|
||||||
|
window.moveToColumn(newColumn, true, passFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
|
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
|
||||||
if (config.skipSwitcher) {
|
if (config.skipSwitcher) {
|
||||||
client.kwinClient.skipSwitcher = true;
|
client.kwinClient.skipSwitcher = true;
|
||||||
}
|
}
|
||||||
if (config.tiledKeepBelow) {
|
|
||||||
client.kwinClient.keepBelow = true;
|
if (client.kwinClient.fullScreen) {
|
||||||
|
if (config.maximizedKeepAbove) {
|
||||||
|
client.kwinClient.keepAbove = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (config.tiledKeepBelow) {
|
||||||
|
client.kwinClient.keepBelow = true;
|
||||||
|
}
|
||||||
|
client.kwinClient.keepAbove = false;
|
||||||
}
|
}
|
||||||
client.setFullScreen(false);
|
|
||||||
if (client.kwinClient.tile !== null) {
|
if (client.kwinClient.tile !== null) {
|
||||||
client.setMaximize(false, true); // disable quick tile mode
|
client.setMaximize(false, true); // disable quick tile mode
|
||||||
}
|
}
|
||||||
@@ -221,7 +245,6 @@ namespace ClientState {
|
|||||||
if (config.offScreenOpacity < 1.0) {
|
if (config.offScreenOpacity < 1.0) {
|
||||||
client.kwinClient.opacity = 1.0;
|
client.kwinClient.opacity = 1.0;
|
||||||
}
|
}
|
||||||
client.setShade(false);
|
|
||||||
client.setFullScreen(false);
|
client.setFullScreen(false);
|
||||||
if (client.kwinClient.tile === null) {
|
if (client.kwinClient.tile === null) {
|
||||||
client.setMaximize(false, false);
|
client.setMaximize(false, false);
|
||||||
@@ -231,8 +254,8 @@ namespace ClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace Tiled {
|
namespace Tiled {
|
||||||
export type WindowState = {
|
export interface WindowState {
|
||||||
skipSwitcher: boolean;
|
skipSwitcher: boolean;
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ClientState {
|
|||||||
this.signalManager = TiledMinimized.initSignalManager(world, client);
|
this.signalManager = TiledMinimized.initSignalManager(world, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(passFocus: boolean) {
|
public destroy(passFocus: FocusPassing.Type) {
|
||||||
this.signalManager.destroy();
|
this.signalManager.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
43
src/tests/flows/centerFocused.ts
Normal file
43
src/tests/flows/centerFocused.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
tests.register("Center focused", 1, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client0, client1, client2] = workspaceMock.createClientsWithWidths(300, 152, 300);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.hasClient(client0));
|
||||||
|
Assert.assert(clientManager.hasClient(client1));
|
||||||
|
Assert.assert(clientManager.hasClient(client2));
|
||||||
|
});
|
||||||
|
Assert.assert(workspaceMock.activeWindow === client2);
|
||||||
|
Assert.columnsFillTilingArea([client0, client1, client2]);
|
||||||
|
|
||||||
|
// center client2
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.centered(config, tilingArea, client2);
|
||||||
|
Assert.fullyVisible(client1.frameGeometry);
|
||||||
|
Assert.fullyVisible(client2.frameGeometry);
|
||||||
|
|
||||||
|
// undo center client2
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.columnsFillTilingArea([client0, client1, client2]);
|
||||||
|
|
||||||
|
// center client2
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.centered(config, tilingArea, client2);
|
||||||
|
Assert.fullyVisible(client1.frameGeometry);
|
||||||
|
Assert.fullyVisible(client2.frameGeometry);
|
||||||
|
|
||||||
|
// focus client1 (no scrolling should occur)
|
||||||
|
qtMock.fireShortcut("karousel-focus-left");
|
||||||
|
Assert.centered(config, tilingArea, client2, { message: "No scrolling should have occured" });
|
||||||
|
Assert.fullyVisible(client1.frameGeometry);
|
||||||
|
Assert.fullyVisible(client2.frameGeometry);
|
||||||
|
|
||||||
|
// center client1
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.columnsFillTilingArea([client0, client1, client2]);
|
||||||
|
|
||||||
|
// undo center client1 (no scrolling should occur, because all clients are already visible and centered)
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.columnsFillTilingArea([client0, client1, client2]);
|
||||||
|
});
|
||||||
82
src/tests/flows/cursorFollowsFocus.ts
Normal file
82
src/tests/flows/cursorFollowsFocus.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
tests.register("Drag tiled window, untile", 10, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.cursorFollowsFocus = true;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client1, client2] = workspaceMock.createClients(2);
|
||||||
|
const initialCursorPos = new MockQmlPoint(380, 20);
|
||||||
|
Assert.assert(rectContainsPoint(client1.frameGeometry, initialCursorPos), { message: "invalid test setup" });
|
||||||
|
workspaceMock.cursorPos = initialCursorPos.clone();
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { Workspace.activeWindow = client1; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-1"); },
|
||||||
|
);
|
||||||
|
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
|
||||||
|
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
|
||||||
|
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos), { message: "Cursor should not have been moved because it was already within the focused client" });
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { Workspace.activeWindow = client2; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-2"); },
|
||||||
|
);
|
||||||
|
Assert.assert(!rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
|
||||||
|
Assert.assert(rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { Workspace.activeWindow = client1; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-1"); },
|
||||||
|
);
|
||||||
|
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
|
||||||
|
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
|
||||||
|
const lastCursorPos = workspaceMock.cursorPos.clone();
|
||||||
|
|
||||||
|
Workspace.activeWindow = null;
|
||||||
|
Assert.assert(pointEquals(Workspace.cursorPos, lastCursorPos), { message: "Cursor should not have been moved" });
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Cursor follows focus only on matched desktops", 1, () => {
|
||||||
|
// Test that cursor follow focus only works for windows on matched desktops (tiled windows)
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.cursorFollowsFocus = true;
|
||||||
|
config.tiledDesktops = "^Desktop 1$"; // Only work on Desktop 1
|
||||||
|
const { workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
// Create a client on Desktop 1 (matched desktop) - should be tiled
|
||||||
|
const client1 = new MockKwinClient();
|
||||||
|
client1.desktops = [workspaceMock.desktops[0]]; // Desktop 1
|
||||||
|
workspaceMock.createWindows(client1);
|
||||||
|
|
||||||
|
// Create a client on Desktop 2 (non-matched desktop) - should be floating
|
||||||
|
const client2 = new MockKwinClient();
|
||||||
|
client2.desktops = [workspaceMock.desktops[1]]; // Desktop 2
|
||||||
|
workspaceMock.createWindows(client2);
|
||||||
|
|
||||||
|
// Set initial cursor position outside both windows
|
||||||
|
const initialCursorPos = new MockQmlPoint(10, 10);
|
||||||
|
workspaceMock.cursorPos = initialCursorPos.clone();
|
||||||
|
|
||||||
|
// Test 1: Focus client1 on matched desktop (Desktop 1) - cursor should move
|
||||||
|
workspaceMock.currentDesktop = workspaceMock.desktops[0]; // Switch to Desktop 1
|
||||||
|
Workspace.activeWindow = client1;
|
||||||
|
world.do(() => {});
|
||||||
|
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos),
|
||||||
|
{ message: "Cursor should have moved to tiled window on matched desktop" });
|
||||||
|
|
||||||
|
// Test 2: Switch to non-matched desktop (Desktop 2) and focus client2 - cursor should NOT move
|
||||||
|
workspaceMock.cursorPos = initialCursorPos.clone();
|
||||||
|
workspaceMock.currentDesktop = workspaceMock.desktops[1]; // Switch to Desktop 2
|
||||||
|
Workspace.activeWindow = client2;
|
||||||
|
world.do(() => {});
|
||||||
|
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos),
|
||||||
|
{ message: "Cursor should NOT move on non-matched desktop" });
|
||||||
|
|
||||||
|
// Test 3: Even if we focus client1 (tiled) while on Desktop 2, cursor should NOT move
|
||||||
|
// because the current desktop is not matched
|
||||||
|
workspaceMock.cursorPos = initialCursorPos.clone();
|
||||||
|
workspaceMock.currentDesktop = workspaceMock.desktops[1]; // Stay on Desktop 2
|
||||||
|
Workspace.activeWindow = client1;
|
||||||
|
world.do(() => {});
|
||||||
|
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos),
|
||||||
|
{ message: "Cursor should NOT move even for tiled window when current desktop is not matched" });
|
||||||
|
});
|
||||||
73
src/tests/flows/desktopFiltering.ts
Normal file
73
src/tests/flows/desktopFiltering.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
tests.register("Desktop filtering", 1, () => {
|
||||||
|
// Test 1: Default config should work on all desktops
|
||||||
|
const config1 = getDefaultConfig();
|
||||||
|
const { workspaceMock: wm1, world: world1 } = init(config1);
|
||||||
|
|
||||||
|
const client1 = new MockKwinClient();
|
||||||
|
client1.desktops = [wm1.desktops[0]];
|
||||||
|
wm1.createWindows(client1);
|
||||||
|
|
||||||
|
world1.do((clientManager) => {
|
||||||
|
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on desktop1 with default config (*)" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Desktop filtering - specific desktop", 1, () => {
|
||||||
|
// Test 2: Specific desktop name - should work only on matching desktop
|
||||||
|
const config2 = getDefaultConfig();
|
||||||
|
config2.tiledDesktops = "^Desktop 1$";
|
||||||
|
const { workspaceMock: wm2, world: world2 } = init(config2);
|
||||||
|
|
||||||
|
const client1 = new MockKwinClient();
|
||||||
|
client1.desktops = [wm2.desktops[0]]; // Desktop 1
|
||||||
|
wm2.createWindows(client1);
|
||||||
|
world2.do((clientManager) => {
|
||||||
|
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on Desktop 1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
wm2.removeWindow(client1);
|
||||||
|
|
||||||
|
const client2 = new MockKwinClient();
|
||||||
|
client2.desktops = [wm2.desktops[1]]; // Desktop 2
|
||||||
|
wm2.createWindows(client2);
|
||||||
|
world2.do((clientManager) => {
|
||||||
|
Assert.notTiledClient(clientManager, client2, { message: "Client should NOT be tiled on Desktop 2" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Desktop filtering - multiple desktops", 1, () => {
|
||||||
|
// Test 3: Multiple desktop names using regex alternation
|
||||||
|
const config3 = getDefaultConfig();
|
||||||
|
config3.tiledDesktops = "^Desktop [12]$";
|
||||||
|
const { workspaceMock: wm3, world: world3 } = init(config3);
|
||||||
|
|
||||||
|
const client1 = new MockKwinClient();
|
||||||
|
client1.desktops = [wm3.desktops[0]]; // Desktop 1
|
||||||
|
wm3.createWindows(client1);
|
||||||
|
world3.do((clientManager) => {
|
||||||
|
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on Desktop 1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
wm3.removeWindow(client1);
|
||||||
|
|
||||||
|
const client2 = new MockKwinClient();
|
||||||
|
client2.desktops = [wm3.desktops[1]]; // Desktop 2
|
||||||
|
wm3.createWindows(client2);
|
||||||
|
world3.do((clientManager) => {
|
||||||
|
Assert.tiledClient(clientManager, client2, { message: "Client should be tiled on Desktop 2" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Desktop filtering - windows on multiple desktops", 1, () => {
|
||||||
|
// Test 4: Windows on multiple desktops should not be tiled (fallback to floating)
|
||||||
|
const config4 = getDefaultConfig();
|
||||||
|
config4.tiledDesktops = ".*";
|
||||||
|
const { workspaceMock: wm4, world: world4 } = init(config4);
|
||||||
|
|
||||||
|
const client1 = new MockKwinClient();
|
||||||
|
client1.desktops = [wm4.desktops[0], wm4.desktops[1]]; // Multiple desktops
|
||||||
|
wm4.createWindows(client1);
|
||||||
|
world4.do((clientManager) => {
|
||||||
|
Assert.notTiledClient(clientManager, client1, { message: "Client on multiple desktops should not be tiled" });
|
||||||
|
});
|
||||||
|
});
|
||||||
34
src/tests/flows/dragTiled.ts
Normal file
34
src/tests/flows/dragTiled.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
tests.register("Drag tiled window, untile", 20, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.untileOnDrag = true;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
const clientManager = getClientManager(world);
|
||||||
|
|
||||||
|
const [client0, client1] = workspaceMock.createClients(2);
|
||||||
|
Assert.tiledClient(clientManager, client0);
|
||||||
|
Assert.tiledClient(clientManager, client1);
|
||||||
|
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
|
||||||
|
|
||||||
|
workspaceMock.moveWindow(client0, new MockQmlPoint(10, 10));
|
||||||
|
Assert.notTiledClient(clientManager, client0);
|
||||||
|
Assert.tiledClient(clientManager, client1);
|
||||||
|
Assert.grid(config, tilingArea, 100, [[client1]], true);
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Drag tiled window, keep tiled", 20, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.untileOnDrag = false;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
const clientManager = getClientManager(world);
|
||||||
|
|
||||||
|
const [client0, client1] = workspaceMock.createClients(2);
|
||||||
|
Assert.tiledClient(clientManager, client0);
|
||||||
|
Assert.tiledClient(clientManager, client1);
|
||||||
|
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
|
||||||
|
|
||||||
|
const move = new MockQmlPoint(10, 10);
|
||||||
|
workspaceMock.moveWindow(client0, move, move, move, move, move, move, move, move, move); // many moves in order to trigger externalFrameGeometryChangedRateLimiter
|
||||||
|
Assert.tiledClient(clientManager, client0);
|
||||||
|
Assert.tiledClient(clientManager, client1);
|
||||||
|
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ tests.register("External resize", 1, () => {
|
|||||||
|
|
||||||
function getTiledFrame(width: number) {
|
function getTiledFrame(width: number) {
|
||||||
return new MockQmlRect(
|
return new MockQmlRect(
|
||||||
Math.round((screen.width - width) / 2),
|
tilingArea.left + Math.round((tilingArea.width - width) / 2),
|
||||||
tilingArea.top,
|
tilingArea.top,
|
||||||
width,
|
width,
|
||||||
tilingArea.height,
|
tilingArea.height,
|
||||||
|
|||||||
10
src/tests/flows/followWindowToDesktop.ts
Normal file
10
src/tests/flows/followWindowToDesktop.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
tests.register("Move and follow window to desktop", 20, () => {
|
||||||
|
// This tests the Kwin shortcuts for moving windows to adjacent desktops.
|
||||||
|
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client0, client1] = workspaceMock.createClients(2);
|
||||||
|
client1.moveAndFollowToDesktop(workspaceMock.desktops[1], workspaceMock);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client1);
|
||||||
|
});
|
||||||
191
src/tests/flows/layering.ts
Normal file
191
src/tests/flows/layering.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
tests.register("tiledKeepBelow", 10, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.tiledKeepBelow = true;
|
||||||
|
config.floatingKeepAbove = false;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
|
||||||
|
|
||||||
|
const [client] = workspaceMock.createClients(1);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
client.unpin();
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("floatingKeepAbove", 10, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.tiledKeepBelow = false;
|
||||||
|
config.floatingKeepAbove = true;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
|
||||||
|
|
||||||
|
const [client] = workspaceMock.createClients(1);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(client.keepAbove);
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(client.keepAbove);
|
||||||
|
|
||||||
|
client.unpin();
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(client.keepAbove);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null);
|
||||||
|
});
|
||||||
|
Assert.assert(!client.keepBelow);
|
||||||
|
Assert.assert(!client.keepAbove);
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("No layering", 10, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.tiledKeepBelow = false;
|
||||||
|
config.floatingKeepAbove = false;
|
||||||
|
// In this mode, Karousel shouldn't change keepBelow or keepAbove.
|
||||||
|
// Except when tiling a window, keepAbove should still be cleared.
|
||||||
|
|
||||||
|
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
|
||||||
|
|
||||||
|
const testCases = [
|
||||||
|
{ keepBelow: false, keepAbove: false },
|
||||||
|
{ keepBelow: false, keepAbove: true },
|
||||||
|
{ keepBelow: true, keepAbove: false },
|
||||||
|
{ keepBelow: true, keepAbove: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const assertOptions = { message: JSON.stringify(testCase) };
|
||||||
|
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client] = workspaceMock.createClients(1);
|
||||||
|
client.keepBelow = testCase.keepBelow;
|
||||||
|
client.keepAbove = testCase.keepAbove;
|
||||||
|
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
|
||||||
|
|
||||||
|
client.unpin();
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.assert(!client.keepAbove, assertOptions);
|
||||||
|
client.keepAbove = testCase.keepAbove;
|
||||||
|
|
||||||
|
client.pin(pinGeometry);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
|
||||||
|
});
|
||||||
|
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
|
||||||
|
Assert.assert(!client.keepAbove, assertOptions);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -12,7 +12,7 @@ tests.register("Focus and move windows", 1, () => {
|
|||||||
|
|
||||||
function testLayout(shortcutName: string, grid: KwinClient[][]) {
|
function testLayout(shortcutName: string, grid: KwinClient[][]) {
|
||||||
qtMock.fireShortcut(shortcutName);
|
qtMock.fireShortcut(shortcutName);
|
||||||
Assert.grid(config, screen, grid, { skip: 1 });
|
Assert.grid(config, tilingArea, 100, grid, true, [], { skip: 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function testFocus(shortcutName: string, expectedFocus: KwinClient) {
|
function testFocus(shortcutName: string, expectedFocus: KwinClient) {
|
||||||
|
|||||||
47
src/tests/flows/lazyScroller.ts
Normal file
47
src/tests/flows/lazyScroller.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
tests.register("LazyScroller", 20, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.scrollingLazy = true;
|
||||||
|
config.scrollingCentered = false;
|
||||||
|
config.scrollingGrouped = false;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client1] = workspaceMock.createClientsWithWidths(300);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1]], true);
|
||||||
|
|
||||||
|
const [client2] = workspaceMock.createClientsWithWidths(300);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2]], true);
|
||||||
|
|
||||||
|
const [client3] = workspaceMock.createClientsWithWidths(300);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
|
||||||
|
Assert.equal(client3.frameGeometry.right, tilingArea.right);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = client2; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-2"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-left"); },
|
||||||
|
);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
|
||||||
|
Assert.equal(client3.frameGeometry.right, tilingArea.right);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = client1; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-1"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-left"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-start"); },
|
||||||
|
);
|
||||||
|
workspaceMock.activeWindow = client1;
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
|
||||||
|
Assert.equal(client1.frameGeometry.left, tilingArea.left);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-grid-scroll-focused");
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1]], true);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = client2; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-2"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-right"); },
|
||||||
|
);
|
||||||
|
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
|
||||||
|
Assert.equal(client1.frameGeometry.left, tilingArea.left);
|
||||||
|
});
|
||||||
@@ -1,145 +1,341 @@
|
|||||||
tests.register("Maximization", 100, () => {
|
{
|
||||||
const config = getDefaultConfig();
|
function registerTests(
|
||||||
const { qtMock, workspaceMock, world } = init(config);
|
suffix: string,
|
||||||
|
getConfig: () => Config,
|
||||||
|
shouldKeepBelow: (tiled: boolean) => boolean,
|
||||||
|
shouldKeepAbove: (tiled: boolean) => boolean,
|
||||||
|
) {
|
||||||
|
tests.register("Maximization " + suffix, 100, () => {
|
||||||
|
const config = getConfig();
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
|
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
|
||||||
world.do((clientManager, desktopManager) => {
|
world.do((clientManager, desktopManager) => {
|
||||||
Assert.assert(clientManager.hasClient(kwinClient));
|
Assert.assert(clientManager.hasClient(kwinClient));
|
||||||
});
|
});
|
||||||
|
|
||||||
const columnLeftX = screen.width/2 - 300/2;
|
const columnLeftX = tilingArea.left + tilingArea.width/2 - 300/2;
|
||||||
const columnTopY = tilingArea.top;
|
const columnTopY = tilingArea.top;
|
||||||
const columnHeight = tilingArea.height;
|
const columnHeight = tilingArea.height;
|
||||||
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
Assert.assert(!kwinClient.fullScreen);
|
||||||
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
||||||
|
|
||||||
kwinClient.fullScreen = true;
|
kwinClient.fullScreen = true;
|
||||||
Assert.equalRects(kwinClient.frameGeometry, screen);
|
Assert.assert(kwinClient.fullScreen);
|
||||||
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, screen);
|
||||||
|
|
||||||
kwinClient.fullScreen = false;
|
kwinClient.fullScreen = false;
|
||||||
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
Assert.assert(!kwinClient.fullScreen);
|
||||||
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
||||||
|
|
||||||
kwinClient.setMaximize(true, true);
|
kwinClient.setMaximize(true, true);
|
||||||
Assert.equalRects(kwinClient.frameGeometry, screen);
|
Assert.assert(!kwinClient.fullScreen);
|
||||||
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, screen);
|
||||||
|
|
||||||
kwinClient.setMaximize(true, false);
|
kwinClient.setMaximize(true, false);
|
||||||
Assert.rect(kwinClient.frameGeometry, columnLeftX, 0, 300, screen.height);
|
Assert.assert(!kwinClient.fullScreen);
|
||||||
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.rect(kwinClient.frameGeometry, columnLeftX, 0, 300, screen.height);
|
||||||
|
|
||||||
kwinClient.setMaximize(false, false);
|
kwinClient.setMaximize(false, false);
|
||||||
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
Assert.assert(!kwinClient.fullScreen);
|
||||||
});
|
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
|
||||||
|
});
|
||||||
|
|
||||||
tests.register("Maximize with transient", 100, () => {
|
tests.register("Maximize with transient " + suffix, 100, () => {
|
||||||
const config = getDefaultConfig();
|
const config = getConfig();
|
||||||
const { qtMock, workspaceMock, world } = init(config);
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
const parent = new MockKwinClient(new MockQmlRect(10, 20, 300, 200));
|
const parent = new MockKwinClient(new MockQmlRect(10, 20, 300, 200));
|
||||||
const child = new MockKwinClient(new MockQmlRect(14, 24, 50, 50), parent);
|
const child = new MockKwinClient(new MockQmlRect(14, 24, 50, 50), parent);
|
||||||
|
|
||||||
workspaceMock.createWindows(parent);
|
workspaceMock.createWindows(parent);
|
||||||
world.do((clientManager, desktopManager) => {
|
world.do((clientManager, desktopManager) => {
|
||||||
Assert.assert(clientManager.hasClient(parent));
|
Assert.assert(clientManager.hasClient(parent));
|
||||||
});
|
});
|
||||||
|
|
||||||
runOneOf(
|
runOneOf(
|
||||||
() => parent.fullScreen = true,
|
() => { parent.fullScreen = true; },
|
||||||
() => parent.setMaximize(true, true),
|
() => { parent.setMaximize(true, true); },
|
||||||
|
);
|
||||||
|
Assert.equal(parent.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(parent.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(parent.frameGeometry, screen);
|
||||||
|
|
||||||
|
workspaceMock.createWindows(child);
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.hasClient(child));
|
||||||
|
});
|
||||||
|
Assert.assert(!child.fullScreen);
|
||||||
|
Assert.equal(child.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(child.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.rect(child.frameGeometry, 14, 24, 50, 50);
|
||||||
|
Assert.equal(parent.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(parent.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(parent.frameGeometry, screen);
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
function assertWindowed(config: Config, clients: KwinClient[]) {
|
||||||
|
Assert.assert(!clients[0].fullScreen);
|
||||||
|
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.assert(!clients[1].fullScreen);
|
||||||
|
Assert.equal(clients[1].keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(clients[1].keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.assert(!clients[2].fullScreen);
|
||||||
|
Assert.equal(clients[2].keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(clients[2].keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.grid(config, tilingArea, [300, 400], [[clients[0]], [clients[1], clients[2]]], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertFullScreenOrMaximized(clients: KwinClient[]) {
|
||||||
|
Assert.assert(!clients[0].fullScreen);
|
||||||
|
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.assert(!clients[1].fullScreen);
|
||||||
|
Assert.equal(clients[1].keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(clients[1].keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.equal(clients[2].keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(clients[2].keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(clients[2].frameGeometry, screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.register("Re-maximize disabled " + suffix, 100, () => {
|
||||||
|
const config = getConfig();
|
||||||
|
config.reMaximize = false;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const clients = workspaceMock.createClientsWithWidths(300, 400, 400);
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { clients[2].fullScreen = true; },
|
||||||
|
() => { clients[2].setMaximize(true, true); },
|
||||||
|
);
|
||||||
|
assertFullScreenOrMaximized(clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[0]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-1"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-left"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-start"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[2]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-2"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-right"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-end"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { clients[2].fullScreen = true; },
|
||||||
|
() => { clients[2].setMaximize(true, true); },
|
||||||
|
);
|
||||||
|
assertFullScreenOrMaximized(clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[1]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-up"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[2]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-down"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Re-maximize enabled " + suffix, 100, () => {
|
||||||
|
const config = getConfig();
|
||||||
|
config.reMaximize = true;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const clients = workspaceMock.createClientsWithWidths(300, 400, 400);
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { clients[2].fullScreen = true; },
|
||||||
|
() => { clients[2].setMaximize(true, true); },
|
||||||
|
);
|
||||||
|
assertFullScreenOrMaximized(clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[0]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-1"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-left"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-start"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[2]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-2"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-right"); },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-end"); },
|
||||||
|
);
|
||||||
|
assertFullScreenOrMaximized(clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[1]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-up"); },
|
||||||
|
);
|
||||||
|
assertWindowed(config, clients);
|
||||||
|
|
||||||
|
runOneOf(
|
||||||
|
() => { workspaceMock.activeWindow = clients[2]; },
|
||||||
|
() => { qtMock.fireShortcut("karousel-focus-down"); },
|
||||||
|
);
|
||||||
|
assertFullScreenOrMaximized(clients);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tests.register("Start full-screen " + suffix, 100, () => {
|
||||||
|
const config = getConfig();
|
||||||
|
config.reMaximize = true;
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [windowedClient] = workspaceMock.createClientsWithWidths(300);
|
||||||
|
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
|
||||||
|
fullScreenClient.resourceClass = "full-screen-app";
|
||||||
|
fullScreenClient.fullScreen = true;
|
||||||
|
workspaceMock.createWindows(fullScreenClient);
|
||||||
|
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.hasClient(windowedClient));
|
||||||
|
Assert.assert(clientManager.hasClient(fullScreenClient));
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.assert(!windowedClient.fullScreen);
|
||||||
|
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.centered(config, tilingArea, windowedClient);
|
||||||
|
Assert.assert(fullScreenClient.fullScreen);
|
||||||
|
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(fullScreenClient.frameGeometry, screen);
|
||||||
|
Assert.equal(Workspace.activeWindow, fullScreenClient);
|
||||||
|
|
||||||
|
{
|
||||||
|
qtMock.fireShortcut("karousel-focus-left");
|
||||||
|
const opts = { message: "fullScreenClient is not in the grid, so we can't move focus directionally" };
|
||||||
|
Assert.assert(!windowedClient.fullScreen);
|
||||||
|
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.centered(config, tilingArea, windowedClient);
|
||||||
|
Assert.assert(fullScreenClient.fullScreen);
|
||||||
|
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(fullScreenClient.frameGeometry, screen);
|
||||||
|
Assert.equal(Workspace.activeWindow, fullScreenClient, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
qtMock.fireShortcut("karousel-focus-1");
|
||||||
|
const opts = { message: "fullScreenClient is not in grid, so it should stay full-screen" };
|
||||||
|
Assert.assert(!windowedClient.fullScreen);
|
||||||
|
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.centered(config, tilingArea, windowedClient);
|
||||||
|
Assert.assert(fullScreenClient.fullScreen);
|
||||||
|
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(fullScreenClient.frameGeometry, screen);
|
||||||
|
Assert.equal(Workspace.activeWindow, windowedClient);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tests.register("Start full-screen (force tiling) " + suffix, 100, () => {
|
||||||
|
const config = getConfig();
|
||||||
|
config.reMaximize = true;
|
||||||
|
config.windowRules = '[{ "class": "full-screen-app", "tile": true }]';
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const column1Width = 300;
|
||||||
|
const [windowedClient] = workspaceMock.createClientsWithWidths(column1Width);
|
||||||
|
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
|
||||||
|
fullScreenClient.resourceClass = "full-screen-app";
|
||||||
|
fullScreenClient.fullScreen = true;
|
||||||
|
workspaceMock.createWindows(fullScreenClient);
|
||||||
|
|
||||||
|
world.do((clientManager, desktopManager) => {
|
||||||
|
Assert.assert(clientManager.hasClient(windowedClient));
|
||||||
|
Assert.assert(clientManager.hasClient(fullScreenClient));
|
||||||
|
});
|
||||||
|
Assert.assert(!windowedClient.fullScreen);
|
||||||
|
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.grid(config, tilingArea, [column1Width], [[windowedClient]], false);
|
||||||
|
Assert.assert(fullScreenClient.fullScreen);
|
||||||
|
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
|
||||||
|
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
|
||||||
|
Assert.equalRects(fullScreenClient.frameGeometry, screen);
|
||||||
|
Assert.equal(Workspace.activeWindow, fullScreenClient);
|
||||||
|
|
||||||
|
let expectedColumn2Width = 0;
|
||||||
|
let expectedActiveWindow;
|
||||||
|
runOneOf(
|
||||||
|
() => {
|
||||||
|
fullScreenClient.fullScreen = false;
|
||||||
|
expectedColumn2Width = 400;
|
||||||
|
expectedActiveWindow = fullScreenClient;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
qtMock.fireShortcut("karousel-focus-left");
|
||||||
|
expectedColumn2Width = tilingArea.width;
|
||||||
|
expectedActiveWindow = windowedClient;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const opts = { message: "fullScreenClient should be restored from full-screen mode to tiled mode" };
|
||||||
|
Assert.assert(!windowedClient.fullScreen);
|
||||||
|
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.assert(!fullScreenClient.fullScreen);
|
||||||
|
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(true));
|
||||||
|
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(true));
|
||||||
|
Assert.grid(config, tilingArea, [column1Width, expectedColumn2Width], [[windowedClient], [fullScreenClient]], false, [], opts);
|
||||||
|
Assert.equal(Workspace.activeWindow, expectedActiveWindow);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig(floatingKeepAbove: boolean) {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
config.tiledKeepBelow = !floatingKeepAbove;
|
||||||
|
config.floatingKeepAbove = floatingKeepAbove;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerTests(
|
||||||
|
"(tiled below)",
|
||||||
|
getConfig.partial(false),
|
||||||
|
tiled => tiled,
|
||||||
|
tiled => false,
|
||||||
);
|
);
|
||||||
Assert.equalRects(parent.frameGeometry, screen);
|
|
||||||
|
|
||||||
workspaceMock.createWindows(child);
|
registerTests(
|
||||||
world.do((clientManager, desktopManager) => {
|
"(floating above)",
|
||||||
Assert.assert(clientManager.hasClient(child));
|
getConfig.partial(true),
|
||||||
});
|
tiled => false,
|
||||||
Assert.rect(child.frameGeometry, 14, 24, 50, 50);
|
tiled => !tiled,
|
||||||
Assert.equalRects(parent.frameGeometry, screen);
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.register("Re-maximize disabled", 100, () => {
|
|
||||||
const config = getDefaultConfig();
|
|
||||||
config.reMaximize = false;
|
|
||||||
const { qtMock, workspaceMock, world } = init(config);
|
|
||||||
|
|
||||||
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
|
|
||||||
world.do((clientManager, desktopManager) => {
|
|
||||||
Assert.assert(clientManager.hasClient(client1));
|
|
||||||
Assert.assert(clientManager.hasClient(client2));
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
|
|
||||||
const column1LeftX = screen.width/2 - columnsWidth/2;
|
|
||||||
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
|
|
||||||
const columnTopY = tilingArea.top;
|
|
||||||
const columnHeight = tilingArea.height;
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => client2.fullScreen = true,
|
|
||||||
() => client2.setMaximize(true, true),
|
|
||||||
);
|
);
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
}
|
||||||
Assert.equalRects(client2.frameGeometry, screen);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => workspaceMock.activeWindow = client1,
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-1"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-left"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-start"),
|
|
||||||
);
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => workspaceMock.activeWindow = client2,
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-2"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-right"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-end"),
|
|
||||||
);
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
tests.register("Re-maximize enabled", 100, () => {
|
|
||||||
const config = getDefaultConfig();
|
|
||||||
config.reMaximize = true;
|
|
||||||
const { qtMock, workspaceMock, world } = init(config);
|
|
||||||
|
|
||||||
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
|
|
||||||
world.do((clientManager, desktopManager) => {
|
|
||||||
Assert.assert(clientManager.hasClient(client1));
|
|
||||||
Assert.assert(clientManager.hasClient(client2));
|
|
||||||
});
|
|
||||||
|
|
||||||
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
|
|
||||||
const column1LeftX = screen.width/2 - columnsWidth/2;
|
|
||||||
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
|
|
||||||
const columnTopY = tilingArea.top;
|
|
||||||
const columnHeight = tilingArea.height;
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => client2.fullScreen = true,
|
|
||||||
() => client2.setMaximize(true, true),
|
|
||||||
);
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.equalRects(client2.frameGeometry, screen);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => workspaceMock.activeWindow = client1,
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-1"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-left"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-start"),
|
|
||||||
);
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
|
|
||||||
|
|
||||||
runOneOf(
|
|
||||||
() => workspaceMock.activeWindow = client2,
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-2"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-right"),
|
|
||||||
() => qtMock.fireShortcut("karousel-focus-end"),
|
|
||||||
);
|
|
||||||
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
|
|
||||||
Assert.equalRects(client2.frameGeometry, screen);
|
|
||||||
});
|
|
||||||
|
|||||||
37
src/tests/flows/passFocus.ts
Normal file
37
src/tests/flows/passFocus.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
tests.register("Pass focus", 100, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [client0, client1a, client1b, client1c, client4, client5, client6] = workspaceMock.createClients(7);
|
||||||
|
workspaceMock.activeWindow = client1b;
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
workspaceMock.activeWindow = client1c;
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
workspaceMock.activeWindow = client1b;
|
||||||
|
workspaceMock.activeWindow = client5;
|
||||||
|
|
||||||
|
function removeWindow(client: MockKwinClient) {
|
||||||
|
runOneOf(
|
||||||
|
() => workspaceMock.removeWindow(client),
|
||||||
|
() => client.desktops = [workspaceMock.desktops[1]],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeWindow(client5);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client4);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-column-move-to-desktop-2");
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client1b);
|
||||||
|
|
||||||
|
removeWindow(client1b);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client1a);
|
||||||
|
|
||||||
|
removeWindow(client1a);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client1c);
|
||||||
|
|
||||||
|
removeWindow(client1c);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client0);
|
||||||
|
|
||||||
|
removeWindow(client0);
|
||||||
|
Assert.equal(workspaceMock.activeWindow, client6);
|
||||||
|
});
|
||||||
@@ -5,35 +5,48 @@ tests.register("Pin", 20, () => {
|
|||||||
const screenHalfLeft = new MockQmlRect(0, 0, screen.width/2, screen.height);
|
const screenHalfLeft = new MockQmlRect(0, 0, screen.width/2, screen.height);
|
||||||
const screenHalfRight = new MockQmlRect(screen.width/2, 0, screen.width/2, screen.height);
|
const screenHalfRight = new MockQmlRect(screen.width/2, 0, screen.width/2, screen.height);
|
||||||
|
|
||||||
|
const tilingAreaHalfLeft = new MockQmlRect(
|
||||||
|
tilingArea.x,
|
||||||
|
tilingArea.y,
|
||||||
|
screen.width/2 - config.gapsOuterLeft - config.gapsOuterRight,
|
||||||
|
tilingArea.height,
|
||||||
|
);
|
||||||
|
const tilingAreaHalfRight = new MockQmlRect(
|
||||||
|
screen.width/2 + config.gapsOuterLeft,
|
||||||
|
tilingArea.y,
|
||||||
|
screen.width/2 - config.gapsOuterLeft - config.gapsOuterRight,
|
||||||
|
tilingArea.height,
|
||||||
|
);
|
||||||
|
|
||||||
const [pinned, tiled1, tiled2] = workspaceMock.createClients(3);
|
const [pinned, tiled1, tiled2] = workspaceMock.createClients(3);
|
||||||
Assert.grid(config, screen, [ [pinned], [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingArea, 100, [ [pinned], [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.pin(screenHalfLeft);
|
pinned.pin(screenHalfLeft);
|
||||||
Assert.equalRects(pinned.frameGeometry, screenHalfLeft);
|
Assert.equalRects(pinned.frameGeometry, screenHalfLeft);
|
||||||
Assert.grid(config, screenHalfRight, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingAreaHalfRight, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.pin(screenHalfRight);
|
pinned.pin(screenHalfRight);
|
||||||
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
||||||
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.unpin();
|
pinned.unpin();
|
||||||
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
||||||
Assert.grid(config, screen, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.pin(screenHalfRight);
|
pinned.pin(screenHalfRight);
|
||||||
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
||||||
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.minimized = true;
|
pinned.minimized = true;
|
||||||
Assert.grid(config, screen, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
pinned.minimized = false;
|
pinned.minimized = false;
|
||||||
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
|
||||||
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
|
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
|
||||||
|
|
||||||
workspaceMock.activeWindow = pinned;
|
workspaceMock.activeWindow = pinned;
|
||||||
qtMock.fireShortcut("karousel-window-toggle-floating");
|
qtMock.fireShortcut("karousel-window-toggle-floating");
|
||||||
Assert.assert(pinned.tile === null);
|
Assert.assert(pinned.tile === null);
|
||||||
pinned.frameGeometry = new MockQmlRect(10, 20, 100, 200); // This is needed because the window's preferredWidth can change when pinning, because frameGeometryChanged can fire before tileChanged. TODO: Ensure pinned window keeps its preferredWidth.
|
pinned.frameGeometry = new MockQmlRect(10, 20, 100, 200); // This is needed because the window's preferredWidth can change when pinning, because frameGeometryChanged can fire before tileChanged. TODO: Ensure pinned window keeps its preferredWidth.
|
||||||
Assert.grid(config, screen, [ [tiled1], [tiled2], [pinned] ]);
|
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2], [pinned] ], true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ tests.register("Preset Widths default", 1, () => {
|
|||||||
|
|
||||||
function getRect(columnWidth: number) {
|
function getRect(columnWidth: number) {
|
||||||
return new MockQmlRect(
|
return new MockQmlRect(
|
||||||
(screen.width - columnWidth) / 2,
|
tilingArea.left + (tilingArea.width - columnWidth) / 2,
|
||||||
tilingArea.top,
|
tilingArea.top,
|
||||||
columnWidth,
|
columnWidth,
|
||||||
tilingArea.height,
|
tilingArea.height,
|
||||||
@@ -25,6 +25,12 @@ tests.register("Preset Widths default", 1, () => {
|
|||||||
|
|
||||||
qtMock.fireShortcut("karousel-cycle-preset-widths");
|
qtMock.fireShortcut("karousel-cycle-preset-widths");
|
||||||
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
|
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
|
||||||
});
|
});
|
||||||
|
|
||||||
tests.register("Preset Widths custom", 1, () => {
|
tests.register("Preset Widths custom", 1, () => {
|
||||||
@@ -37,7 +43,7 @@ tests.register("Preset Widths custom", 1, () => {
|
|||||||
|
|
||||||
function getRect(columnWidth: number) {
|
function getRect(columnWidth: number) {
|
||||||
return new MockQmlRect(
|
return new MockQmlRect(
|
||||||
(screen.width - columnWidth) / 2,
|
tilingArea.left + (tilingArea.width - columnWidth) / 2,
|
||||||
tilingArea.top,
|
tilingArea.top,
|
||||||
columnWidth,
|
columnWidth,
|
||||||
tilingArea.height,
|
tilingArea.height,
|
||||||
@@ -61,6 +67,15 @@ tests.register("Preset Widths custom", 1, () => {
|
|||||||
|
|
||||||
qtMock.fireShortcut("karousel-cycle-preset-widths");
|
qtMock.fireShortcut("karousel-cycle-preset-widths");
|
||||||
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
|
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
|
||||||
|
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
|
||||||
});
|
});
|
||||||
|
|
||||||
tests.register("Preset Widths fill screen uniform", 1, () => {
|
tests.register("Preset Widths fill screen uniform", 1, () => {
|
||||||
|
|||||||
30
src/tests/flows/stacked.ts
Normal file
30
src/tests/flows/stacked.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
tests.register("Stacked", 5, () => {
|
||||||
|
const config = getDefaultConfig();
|
||||||
|
const { qtMock, workspaceMock, world } = init(config);
|
||||||
|
|
||||||
|
const [leftTop, leftBottom, rightTop, rightBottom] = workspaceMock.createClients(4);
|
||||||
|
const grid = [[leftTop, leftBottom], [rightTop, rightBottom]];
|
||||||
|
workspaceMock.activeWindow = rightBottom;
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
workspaceMock.activeWindow = leftBottom;
|
||||||
|
qtMock.fireShortcut("karousel-window-move-left");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-column-toggle-stacked");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true, [0]);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-focus-up");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true, [0]);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-focus-down");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true, [0]);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-move-up");
|
||||||
|
Assert.grid(config, tilingArea, 100, [[leftBottom, leftTop], [rightTop, rightBottom]], true, [0]);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-window-move-down");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true, [0]);
|
||||||
|
|
||||||
|
qtMock.fireShortcut("karousel-column-toggle-stacked");
|
||||||
|
Assert.grid(config, tilingArea, 100, grid, true);
|
||||||
|
});
|
||||||
41
src/tests/units/rules/DesktopFilter.ts
Normal file
41
src/tests/units/rules/DesktopFilter.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
tests.register("DesktopFilter", 1, () => {
|
||||||
|
const desktop1 = { __brand: "KwinDesktop" as const, id: "1", name: "Desktop 1" };
|
||||||
|
const desktop2 = { __brand: "KwinDesktop" as const, id: "2", name: "Work" };
|
||||||
|
const desktop3 = { __brand: "KwinDesktop" as const, id: "3", name: "Desktop 2" };
|
||||||
|
|
||||||
|
// Test 1: Empty config means all desktops
|
||||||
|
let filter = new DesktopFilter("");
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Empty config should work on desktop1" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Empty config should work on desktop2" });
|
||||||
|
|
||||||
|
// Test 2: Whitespace only means all desktops
|
||||||
|
filter = new DesktopFilter(" \n \n ");
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Whitespace only should work on desktop1" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Whitespace only should work on desktop2" });
|
||||||
|
|
||||||
|
// Test 3: Match all regex pattern
|
||||||
|
filter = new DesktopFilter(".*");
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Regex '.*' should work on desktop1" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Regex '.*' should work on desktop2" });
|
||||||
|
|
||||||
|
// Test 4: Partial match without anchors
|
||||||
|
filter = new DesktopFilter("Work");
|
||||||
|
Assert.assert(!filter.shouldWorkOnDesktop(desktop1), { message: "Should not work on desktop1" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Should work on desktop2 containing 'Work'" });
|
||||||
|
|
||||||
|
// Test 5: Regex alternation for multiple desktops
|
||||||
|
filter = new DesktopFilter("Desktop 1|Work");
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Should work on desktop1" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Should work on desktop2" });
|
||||||
|
Assert.assert(!filter.shouldWorkOnDesktop(desktop3), { message: "Should not work on desktop3" });
|
||||||
|
|
||||||
|
// Test 6: Regex pattern with character class
|
||||||
|
filter = new DesktopFilter("Desktop [12]");
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Should work on desktop1" });
|
||||||
|
Assert.assert(!filter.shouldWorkOnDesktop(desktop2), { message: "Should not work on desktop2" });
|
||||||
|
Assert.assert(filter.shouldWorkOnDesktop(desktop3), { message: "Should work on desktop3" });
|
||||||
|
|
||||||
|
// Test 7: Case-sensitive matching
|
||||||
|
filter = new DesktopFilter("work");
|
||||||
|
Assert.assert(!filter.shouldWorkOnDesktop(desktop2), { message: "Should not work on desktop2 (case mismatch)" });
|
||||||
|
});
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
tests.register("WindowRuleEnforcer", 1, () => {
|
tests.register("WindowRuleEnforcer", 1, () => {
|
||||||
|
screen = new MockQmlRect(0, 0, 800, 600);
|
||||||
|
Workspace = new MockWorkspace();
|
||||||
|
|
||||||
const testCases = [
|
const testCases = [
|
||||||
{ tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true },
|
{ tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true },
|
||||||
{ tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false },
|
{ tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false },
|
||||||
@@ -25,6 +28,7 @@ tests.register("WindowRuleEnforcer", 1, () => {
|
|||||||
return {
|
return {
|
||||||
normalWindow: normalWindow,
|
normalWindow: normalWindow,
|
||||||
transient: false,
|
transient: false,
|
||||||
|
clientGeometry: new MockQmlRect(0, 0, 200, 200),
|
||||||
managed: true,
|
managed: true,
|
||||||
pid: 100,
|
pid: 100,
|
||||||
moveable: true,
|
moveable: true,
|
||||||
|
|||||||
53
src/tests/units/utils/math.ts
Normal file
53
src/tests/units/utils/math.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
tests.register("math", 1, () => {
|
||||||
|
const rect = new MockQmlRect(100, 200, 10, 20);
|
||||||
|
const testCases: {
|
||||||
|
rect: QmlRect,
|
||||||
|
point: QmlPoint,
|
||||||
|
contained: boolean,
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(100, 200),
|
||||||
|
contained: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(110, 220),
|
||||||
|
contained: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(105, 205),
|
||||||
|
contained: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(110.01, 205),
|
||||||
|
contained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(105, 220.01),
|
||||||
|
contained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(16, 205),
|
||||||
|
contained: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rect: rect,
|
||||||
|
point: new MockQmlPoint(105, 16),
|
||||||
|
contained: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const testCase of testCases) {
|
||||||
|
const result = rectContainsPoint(testCase.rect, testCase.point);
|
||||||
|
Assert.equal(
|
||||||
|
result,
|
||||||
|
testCase.contained,
|
||||||
|
{ message: JSON.stringify(testCase) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -4,6 +4,7 @@ tests.register("Clients.canTileEver", 1, () => {
|
|||||||
{ clientProperties: { resourceClass: "app", caption: "Title", moveable: false }, tileable: false },
|
{ clientProperties: { resourceClass: "app", caption: "Title", moveable: false }, tileable: false },
|
||||||
{ clientProperties: { resourceClass: "app", caption: "Caption", resizeable: false }, tileable: false },
|
{ clientProperties: { resourceClass: "app", caption: "Caption", resizeable: false }, tileable: false },
|
||||||
{ clientProperties: { resourceClass: "app", caption: "Caption", normalWindow: false, popupWindow: true }, tileable: false },
|
{ clientProperties: { resourceClass: "app", caption: "Caption", normalWindow: false, popupWindow: true }, tileable: false },
|
||||||
|
{ clientProperties: { resourceClass: "app", caption: "Caption", moveable: false, resizeable: false, fullScreen: true }, tileable: true },
|
||||||
{ clientProperties: { resourceClass: "ksmserver-logout-greeter", caption: "Caption" }, tileable: false },
|
{ clientProperties: { resourceClass: "ksmserver-logout-greeter", caption: "Caption" }, tileable: false },
|
||||||
{ clientProperties: { resourceClass: "xwaylandvideobridge", caption: "" }, tileable: false },
|
{ clientProperties: { resourceClass: "xwaylandvideobridge", caption: "" }, tileable: false },
|
||||||
];
|
];
|
||||||
@@ -24,6 +25,7 @@ tests.register("Clients.canTileEver", 1, () => {
|
|||||||
pid: 100,
|
pid: 100,
|
||||||
moveable: true,
|
moveable: true,
|
||||||
resizeable: true,
|
resizeable: true,
|
||||||
|
fullScreen: false,
|
||||||
popupWindow: false,
|
popupWindow: false,
|
||||||
minimized: false,
|
minimized: false,
|
||||||
desktops: [1],
|
desktops: [1],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
namespace Assert {
|
namespace Assert {
|
||||||
type Options = {
|
interface Options {
|
||||||
message?: string,
|
message?: string,
|
||||||
skip?: number,
|
skip?: number,
|
||||||
}
|
}
|
||||||
@@ -125,38 +125,111 @@ namespace Assert {
|
|||||||
|
|
||||||
export function grid(
|
export function grid(
|
||||||
config: Config,
|
config: Config,
|
||||||
screen: QmlRect,
|
tilingArea: QmlRect,
|
||||||
|
columnWidths: number[] | number,
|
||||||
grid: KwinClient[][],
|
grid: KwinClient[][],
|
||||||
|
centered: boolean,
|
||||||
|
stackedColumns: number[] = [],
|
||||||
{ message, skip=0 }: Options = {},
|
{ message, skip=0 }: Options = {},
|
||||||
) {
|
) {
|
||||||
// assumes uniformly sized windows within columns of width 100
|
const nColumns = grid.length;
|
||||||
|
function getGridWidth() {
|
||||||
|
function getColumnsWidth() {
|
||||||
|
if (columnWidths instanceof Array) {
|
||||||
|
let columnsWidth = 0;
|
||||||
|
for (const columnWidth of columnWidths) {
|
||||||
|
columnsWidth += columnWidth;
|
||||||
|
}
|
||||||
|
return columnsWidth;
|
||||||
|
} else {
|
||||||
|
return nColumns * columnWidths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gapsWidth = (nColumns-1) * config.gapsInnerHorizontal;
|
||||||
|
return getColumnsWidth() + gapsWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColumnWidth(column: number) {
|
||||||
|
if (columnWidths instanceof Array) {
|
||||||
|
return columnWidths[column];
|
||||||
|
} else {
|
||||||
|
return columnWidths;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gridWidth = getGridWidth();
|
||||||
|
const startX = centered ?
|
||||||
|
tilingArea.x + (tilingArea.width - gridWidth) / 2 :
|
||||||
|
grid[0][0].frameGeometry.x;
|
||||||
|
|
||||||
|
function getColumnX(column: number) {
|
||||||
|
if (columnWidths instanceof Array) {
|
||||||
|
let x = startX;
|
||||||
|
for (let i = 0; i < column; i++) {
|
||||||
|
x += columnWidths[i] + config.gapsInnerHorizontal;
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
} else {
|
||||||
|
return startX + column * (columnWidths + config.gapsInnerHorizontal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// assumes uniformly sized windows within columns of uniform width
|
||||||
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
|
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
|
||||||
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
|
const columnWidth = getColumnWidth(column);
|
||||||
const columnsWidth = nColumns * 100 + (nColumns-1) * config.gapsInnerHorizontal;
|
const windowHeight = (tilingArea.height - config.gapsInnerVertical * (nWindows-1)) / nWindows;
|
||||||
const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows;
|
|
||||||
return new MockQmlRect(
|
return new MockQmlRect(
|
||||||
screen.x + column * (100 + config.gapsInnerHorizontal) + (screen.width-columnsWidth) / 2,
|
getColumnX(column),
|
||||||
screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window,
|
tilingArea.y + (windowHeight + config.gapsInnerVertical) * window,
|
||||||
100,
|
columnWidth,
|
||||||
(columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows,
|
(tilingArea.height - config.gapsInnerVertical * (nWindows-1)) / nWindows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRectInGridStacked(column: number, window: number, nColumns: number, nWindows: number) {
|
||||||
|
const columnWidth = getColumnWidth(column);
|
||||||
|
return new MockQmlRect(
|
||||||
|
getColumnX(column) + window * config.stackOffsetX,
|
||||||
|
tilingArea.y + window * config.stackOffsetY,
|
||||||
|
columnWidth - (nWindows-1) * config.stackOffsetX,
|
||||||
|
tilingArea.height - (nWindows-1) * config.stackOffsetY,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nColumns = grid.length;
|
|
||||||
for (let iColumn = 0; iColumn < nColumns; iColumn++) {
|
for (let iColumn = 0; iColumn < nColumns; iColumn++) {
|
||||||
const column = grid[iColumn];
|
const column = grid[iColumn];
|
||||||
|
const stacked = stackedColumns.includes(iColumn);
|
||||||
|
const getRect = stacked ? getRectInGridStacked : getRectInGrid;
|
||||||
const nWindows = column.length;
|
const nWindows = column.length;
|
||||||
for (let iWindow = 0; iWindow < nWindows; iWindow++) {
|
for (let iWindow = 0; iWindow < nWindows; iWindow++) {
|
||||||
const window = column[iWindow];
|
const window = column[iWindow];
|
||||||
equalRects(
|
equalRects(
|
||||||
window.frameGeometry,
|
window.frameGeometry,
|
||||||
getRectInGrid(iColumn, iWindow, nColumns, nWindows),
|
getRect(iColumn, iWindow, nColumns, nWindows),
|
||||||
{ message: appendMessage(`window ${iWindow}, column ${iColumn}`, message), skip: skip+1 },
|
{ message: appendMessage(`column ${iColumn}, window ${iWindow}`, message), skip: skip+1 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function centered(
|
||||||
|
config: Config,
|
||||||
|
tilingArea: QmlRect,
|
||||||
|
client:KwinClient,
|
||||||
|
{ message, skip=0 }: Options = {},
|
||||||
|
) {
|
||||||
|
grid(
|
||||||
|
config,
|
||||||
|
tilingArea,
|
||||||
|
client.frameGeometry.width,
|
||||||
|
[[client]],
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
{ message: appendMessage("Window not centered", message), skip: skip+1 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function fullyVisible(
|
export function fullyVisible(
|
||||||
rect: QmlRect,
|
rect: QmlRect,
|
||||||
{ message, skip=0 }: Options = {},
|
{ message, skip=0 }: Options = {},
|
||||||
@@ -197,4 +270,26 @@ namespace Assert {
|
|||||||
}
|
}
|
||||||
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options);
|
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tiledClient(
|
||||||
|
clientManager: ClientManager,
|
||||||
|
client: KwinClient,
|
||||||
|
{ message, skip=0 }: Options = {},
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
clientManager.findTiledWindow(client) !== null,
|
||||||
|
{ message: message, skip: skip+1 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notTiledClient(
|
||||||
|
clientManager: ClientManager,
|
||||||
|
client: KwinClient,
|
||||||
|
{ message, skip=0 }: Options = {},
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
clientManager.findTiledWindow(client) === null,
|
||||||
|
{ message: message, skip: skip+1 },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class TestRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace TestRunner {
|
namespace TestRunner {
|
||||||
export type Test = {
|
export interface Test {
|
||||||
name: string,
|
name: string,
|
||||||
count: number,
|
count: number,
|
||||||
f: () => void,
|
f: () => void,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ let Qt: Qt;
|
|||||||
let KWin: KWin;
|
let KWin: KWin;
|
||||||
let Workspace: Workspace;
|
let Workspace: Workspace;
|
||||||
let qmlBase: QmlObject;
|
let qmlBase: QmlObject;
|
||||||
|
let notificationInvalidTiledDesktops: Notification;
|
||||||
let notificationInvalidWindowRules: Notification;
|
let notificationInvalidWindowRules: Notification;
|
||||||
let notificationInvalidPresetWidths: Notification;
|
let notificationInvalidPresetWidths: Notification;
|
||||||
|
let moveCursorToFocus: DBusCall;
|
||||||
|
|
||||||
let screen: MockQmlRect;
|
let screen: MockQmlRect;
|
||||||
let tilingArea: MockQmlRect;
|
let tilingArea: MockQmlRect;
|
||||||
@@ -28,6 +30,15 @@ function init(config: Config) {
|
|||||||
|
|
||||||
Qt = qtMock;
|
Qt = qtMock;
|
||||||
Workspace = workspaceMock;
|
Workspace = workspaceMock;
|
||||||
|
moveCursorToFocus = {
|
||||||
|
__brand: "QmlObject",
|
||||||
|
call: () => {
|
||||||
|
Assert.assert(Workspace.activeWindow !== null, { message: "moveCursorToFocus should never be called if there's no focused window" });
|
||||||
|
const frame = Workspace.activeWindow!.frameGeometry;
|
||||||
|
workspaceMock.cursorPos.x = Math.floor(frame.x + frame.width/2);
|
||||||
|
workspaceMock.cursorPos.y = Math.floor(frame.y + frame.height/2);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const world = new World(config);
|
const world = new World(config);
|
||||||
return { qtMock, workspaceMock, world };
|
return { qtMock, workspaceMock, world };
|
||||||
@@ -35,7 +46,7 @@ function init(config: Config) {
|
|||||||
|
|
||||||
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) {
|
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) {
|
||||||
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left;
|
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left;
|
||||||
const left = Math.floor((screen.width - columnsWidth) / 2);
|
const left = tilingArea.left + Math.floor((tilingArea.width - columnsWidth) / 2);
|
||||||
const right = left + columnsWidth;
|
const right = left + columnsWidth;
|
||||||
return { left, right };
|
return { left, right };
|
||||||
}
|
}
|
||||||
@@ -44,3 +55,17 @@ function getWindowHeight(windowsInColumn: number) {
|
|||||||
const totalGaps = (windowsInColumn-1) * gapV;
|
const totalGaps = (windowsInColumn-1) * gapV;
|
||||||
return Math.round((tilingArea.height - totalGaps) / windowsInColumn);
|
return Math.round((tilingArea.height - totalGaps) / windowsInColumn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getClientManager(world: World): ClientManager {
|
||||||
|
// don't do this outside of tests
|
||||||
|
let clientManager;
|
||||||
|
world.do((cm, dm) => clientManager = cm);
|
||||||
|
return clientManager!;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateRandomWindowOnDesktop(desktop: KwinDesktop) {
|
||||||
|
const windows = Workspace.windows.filter(w => w.desktops.includes(desktop));
|
||||||
|
if (windows.length > 0) {
|
||||||
|
Workspace.activeWindow = randomItem(windows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,49 +3,47 @@ class MockKwinClient {
|
|||||||
|
|
||||||
private static readonly borderThickness = 10;
|
private static readonly borderThickness = 10;
|
||||||
|
|
||||||
public readonly shadeable: boolean = false;
|
public caption = "App";
|
||||||
public readonly caption = "App";
|
|
||||||
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
|
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
|
||||||
public readonly transient: boolean;
|
public readonly transient: boolean;
|
||||||
public readonly move: boolean = false;
|
public move = false;
|
||||||
public resize: boolean = false;
|
public resize = false;
|
||||||
public readonly moveable: boolean = true;
|
|
||||||
public readonly resizeable: boolean = true;
|
|
||||||
public readonly fullScreenable: boolean = true;
|
public readonly fullScreenable: boolean = true;
|
||||||
public readonly maximizable: boolean = true;
|
public readonly maximizable: boolean = true;
|
||||||
public readonly output: Output = { __brand: "Output" };
|
public readonly output: Output = { __brand: "Output" };
|
||||||
public readonly resourceClass = "app";
|
public resourceClass = "app";
|
||||||
public readonly dock: boolean = false;
|
public readonly dock: boolean = false;
|
||||||
public readonly normalWindow: boolean = true;
|
public readonly normalWindow: boolean = true;
|
||||||
public readonly managed: boolean = true;
|
public readonly managed: boolean = true;
|
||||||
public readonly popupWindow: boolean = false;
|
public readonly popupWindow: boolean = false;
|
||||||
public readonly pid = 1;
|
public readonly pid = 1;
|
||||||
|
|
||||||
private _fullScreen: boolean = false;
|
private _maximizedVertically = false;
|
||||||
|
private _maximizedHorizontally = false;
|
||||||
|
private _fullScreen = false;
|
||||||
public activities: string[] = [];
|
public activities: string[] = [];
|
||||||
public skipSwitcher: boolean = false;
|
public skipSwitcher = false;
|
||||||
public keepAbove: boolean = false;
|
public keepAbove = false;
|
||||||
public keepBelow: boolean = false;
|
public keepBelow = false;
|
||||||
public shade: boolean = false;
|
private _minimized = false;
|
||||||
public _minimized: boolean = false;
|
private _desktops: KwinDesktop[] = [];
|
||||||
public desktops: KwinDesktop[] = [];
|
private _tile: Tile|null = null;
|
||||||
public _tile: Tile|null = null;
|
public opacity = 1.0;
|
||||||
public opacity: number = 1.0;
|
|
||||||
|
|
||||||
public readonly fullScreenChanged = new MockQSignal();
|
public readonly fullScreenChanged = new MockQSignal<[]>();
|
||||||
public readonly desktopsChanged = new MockQSignal();
|
public readonly desktopsChanged = new MockQSignal<[]>();
|
||||||
public readonly activitiesChanged = new MockQSignal();
|
public readonly activitiesChanged = new MockQSignal<[]>();
|
||||||
public readonly minimizedChanged = new MockQSignal();
|
public readonly minimizedChanged = new MockQSignal<[]>();
|
||||||
public readonly maximizedAboutToChange = new MockQSignal<[MaximizedMode]>();
|
public readonly maximizedAboutToChange = new MockQSignal<[MaximizedMode]>();
|
||||||
public readonly captionChanged = new MockQSignal();
|
public readonly captionChanged = new MockQSignal<[]>();
|
||||||
public readonly tileChanged = new MockQSignal();
|
public readonly tileChanged = new MockQSignal<[]>();
|
||||||
public readonly interactiveMoveResizeStarted = new MockQSignal();
|
public readonly interactiveMoveResizeStarted = new MockQSignal<[]>();
|
||||||
public readonly interactiveMoveResizeFinished = new MockQSignal();
|
public readonly interactiveMoveResizeFinished = new MockQSignal<[]>();
|
||||||
public readonly frameGeometryChanged = new MockQSignal<[oldGeometry: QmlRect]>();
|
public readonly frameGeometryChanged = new MockQSignal<[oldGeometry: QmlRect]>();
|
||||||
|
|
||||||
private windowedFrameGeometry: MockQmlRect;
|
private windowedFrameGeometry: MockQmlRect;
|
||||||
private windowed: boolean = false;
|
private windowed = true;
|
||||||
private hasBorder: boolean = true;
|
private hasBorder = true;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _frameGeometry: MockQmlRect = new MockQmlRect(10, 10, 100, 200),
|
private _frameGeometry: MockQmlRect = new MockQmlRect(10, 10, 100, 200),
|
||||||
@@ -53,17 +51,25 @@ class MockKwinClient {
|
|||||||
) {
|
) {
|
||||||
this.windowedFrameGeometry = _frameGeometry.clone();
|
this.windowedFrameGeometry = _frameGeometry.clone();
|
||||||
this.transient = transientFor !== null;
|
this.transient = transientFor !== null;
|
||||||
|
this._desktops = [Workspace.currentDesktop];
|
||||||
|
this.activities = [Workspace.currentActivity];
|
||||||
}
|
}
|
||||||
|
|
||||||
setMaximize(vertically: boolean, horizontally: boolean) {
|
setMaximize(vertically: boolean, horizontally: boolean) {
|
||||||
this.windowed = !(vertically || horizontally);
|
this.windowed = !(vertically || horizontally);
|
||||||
|
|
||||||
|
if (vertically === this._maximizedVertically && horizontally === this._maximizedHorizontally) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._maximizedVertically = vertically;
|
||||||
|
this._maximizedHorizontally = horizontally;
|
||||||
|
|
||||||
this.maximizedAboutToChange.fire(
|
this.maximizedAboutToChange.fire(
|
||||||
vertically ? (
|
vertically ? (
|
||||||
horizontally ? MaximizedMode.Maximized : MaximizedMode.Vertically
|
horizontally ? MaximizedMode.Maximized : MaximizedMode.Vertically
|
||||||
) : (
|
) : (
|
||||||
horizontally ? MaximizedMode.Horizontally : MaximizedMode.Unmaximized
|
horizontally ? MaximizedMode.Horizontally : MaximizedMode.Unmaximized
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.frameGeometry = new MockQmlRect(
|
this.frameGeometry = new MockQmlRect(
|
||||||
@@ -83,10 +89,26 @@ class MockKwinClient {
|
|||||||
this.frameGeometry.height - 2 * MockKwinClient.borderThickness,
|
this.frameGeometry.height - 2 * MockKwinClient.borderThickness,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return this.frameGeometry;
|
return runOneOf(
|
||||||
|
() => this.frameGeometry,
|
||||||
|
() => new MockQmlRect(
|
||||||
|
this.frameGeometry.x - 20,
|
||||||
|
this.frameGeometry.y - 20,
|
||||||
|
this.frameGeometry.width + 40,
|
||||||
|
this.frameGeometry.height + 40,
|
||||||
|
), // some full-screen windows that manage their own window decorations can temporarily have a client geometry bigger than the screen
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get moveable() {
|
||||||
|
return !this._fullScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get resizeable() {
|
||||||
|
return !this._fullScreen;
|
||||||
|
}
|
||||||
|
|
||||||
public get fullScreen() {
|
public get fullScreen() {
|
||||||
return this._fullScreen;
|
return this._fullScreen;
|
||||||
}
|
}
|
||||||
@@ -94,6 +116,7 @@ class MockKwinClient {
|
|||||||
public set fullScreen(fullScreen: boolean) {
|
public set fullScreen(fullScreen: boolean) {
|
||||||
const oldFullScreen = this._fullScreen;
|
const oldFullScreen = this._fullScreen;
|
||||||
this.hasBorder = !fullScreen;
|
this.hasBorder = !fullScreen;
|
||||||
|
const targetFrameGeometry = fullScreen ? screen : this.windowedFrameGeometry;
|
||||||
|
|
||||||
runReorder(
|
runReorder(
|
||||||
() => {
|
() => {
|
||||||
@@ -105,34 +128,34 @@ class MockKwinClient {
|
|||||||
() => {
|
() => {
|
||||||
if (oldFullScreen && !fullScreen) {
|
if (oldFullScreen && !fullScreen) {
|
||||||
// when switching from full-screen to windowed, Kwin sometimes first adds the frame before changing the frameGeometry to the final value
|
// when switching from full-screen to windowed, Kwin sometimes first adds the frame before changing the frameGeometry to the final value
|
||||||
if (rectEquals(this.frameGeometry, this.windowedFrameGeometry)) {
|
if (!rectEquals(this.frameGeometry, screen)) {
|
||||||
// already has windowed frame geometry, don't undo that
|
// already has windowed frame geometry, don't undo that
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
runOneOf(
|
runOneOf(
|
||||||
() => this.frameGeometry = new MockQmlRect(
|
() => {
|
||||||
0,
|
this.frameGeometry = new MockQmlRect(
|
||||||
0,
|
0,
|
||||||
screen.width + 2 * MockKwinClient.borderThickness,
|
0,
|
||||||
screen.height + 2 * MockKwinClient.borderThickness,
|
screen.width + 2 * MockKwinClient.borderThickness,
|
||||||
),
|
screen.height + 2 * MockKwinClient.borderThickness,
|
||||||
() => this.frameGeometry = new MockQmlRect(
|
);
|
||||||
-MockKwinClient.borderThickness,
|
},
|
||||||
-MockKwinClient.borderThickness,
|
() => {
|
||||||
screen.width + 2 * MockKwinClient.borderThickness,
|
this.frameGeometry = new MockQmlRect(
|
||||||
screen.height + 2 * MockKwinClient.borderThickness,
|
-MockKwinClient.borderThickness,
|
||||||
),
|
-MockKwinClient.borderThickness,
|
||||||
|
screen.width + 2 * MockKwinClient.borderThickness,
|
||||||
|
screen.height + 2 * MockKwinClient.borderThickness,
|
||||||
|
);
|
||||||
|
},
|
||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.windowed = !fullScreen;
|
this.windowed = !fullScreen;
|
||||||
if (fullScreen) {
|
this.frameGeometry = targetFrameGeometry;
|
||||||
this.frameGeometry = screen;
|
|
||||||
} else {
|
|
||||||
this.frameGeometry = this.windowedFrameGeometry;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -167,6 +190,29 @@ class MockKwinClient {
|
|||||||
this.minimizedChanged.fire();
|
this.minimizedChanged.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get desktops() {
|
||||||
|
return this._desktops;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set desktops(desktops: KwinDesktop[]) {
|
||||||
|
this._desktops = desktops;
|
||||||
|
this.desktopsChanged.fire();
|
||||||
|
if (Workspace.activeWindow === this && !desktops.includes(Workspace.currentDesktop)) {
|
||||||
|
Workspace.activeWindow = null;
|
||||||
|
runMaybe(() => Workspace.activeWindow = null); // fired again for some reason
|
||||||
|
if (Workspace.activeWindow === null) {
|
||||||
|
activateRandomWindowOnDesktop(Workspace.currentDesktop);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveAndFollowToDesktop(desktop: KwinDesktop, workspaceMock: MockWorkspace) {
|
||||||
|
Assert.assert(workspaceMock.activeWindow === this);
|
||||||
|
this._desktops = [desktop];
|
||||||
|
this.desktopsChanged.fire();
|
||||||
|
workspaceMock.currentDesktop = desktop;
|
||||||
|
}
|
||||||
|
|
||||||
public get tile() {
|
public get tile() {
|
||||||
return this._tile;
|
return this._tile;
|
||||||
}
|
}
|
||||||
@@ -189,4 +235,8 @@ class MockKwinClient {
|
|||||||
public getFrameGeometryCopy() {
|
public getFrameGeometryCopy() {
|
||||||
return this._frameGeometry.clone();
|
return this._frameGeometry.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `MockKwinClient("${this.caption}")`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class MockQSignal<T extends unknown[]> {
|
class MockQSignal<T extends unknown[]> {
|
||||||
public readonly __brand = "QSignal";
|
public readonly __brand = "QSignal";
|
||||||
|
|
||||||
private readonly handlers: Set<(...args: [...T]) => void> = new Set();
|
private readonly handlers = new Set<(...args: [...T]) => void>();
|
||||||
|
|
||||||
public connect(handler: (...args: [...T]) => void) {
|
public connect(handler: (...args: [...T]) => void) {
|
||||||
this.handlers.add(handler);
|
this.handlers.add(handler);
|
||||||
|
|||||||
@@ -5,4 +5,11 @@ class MockQmlPoint {
|
|||||||
public x: number,
|
public x: number,
|
||||||
public y: number,
|
public y: number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public clone() {
|
||||||
|
return new MockQmlPoint(
|
||||||
|
this.x,
|
||||||
|
this.y,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class MockQmlTimer {
|
|||||||
public readonly __brand = "QmlObject";
|
public readonly __brand = "QmlObject";
|
||||||
|
|
||||||
public interval = 0;
|
public interval = 0;
|
||||||
public readonly triggered = new MockQSignal();
|
public readonly triggered = new MockQSignal<[]>();
|
||||||
|
|
||||||
public restart() {
|
public restart() {
|
||||||
// no need to wait in tests, just fire immediately
|
// no need to wait in tests, just fire immediately
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class MockShortcutHandler {
|
class MockShortcutHandler {
|
||||||
public readonly __brand = "QmlObject";
|
public readonly __brand = "QmlObject";
|
||||||
|
|
||||||
public readonly activated: MockQSignal<[]> = new MockQSignal();
|
public readonly activated: MockQSignal<[]> = new MockQSignal<[]>();
|
||||||
|
|
||||||
public destroy() {}
|
public destroy() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,15 @@ class MockWorkspace {
|
|||||||
|
|
||||||
public activities = ["test-activity"];
|
public activities = ["test-activity"];
|
||||||
public desktops: KwinDesktop[] = [
|
public desktops: KwinDesktop[] = [
|
||||||
{ __brand: "KwinDesktop", id: "desktop1" },
|
{ __brand: "KwinDesktop", id: "desktop1", name: "Desktop 1" },
|
||||||
{ __brand: "KwinDesktop", id: "desktop2" }
|
{ __brand: "KwinDesktop", id: "desktop2", name: "Desktop 2" },
|
||||||
];
|
];
|
||||||
public currentDesktop = this.desktops[0];
|
|
||||||
public currentActivity = this.activities[0];
|
public currentActivity = this.activities[0];
|
||||||
public activeScreen: Output = { __brand: "Output" };
|
public activeScreen: Output = { __brand: "Output" };
|
||||||
public windows = [];
|
public readonly windows: MockKwinClient[] = [];
|
||||||
public cursorPos = new MockQmlPoint(0, 0);
|
public cursorPos = new MockQmlPoint(0, 0);
|
||||||
|
|
||||||
|
private _currentDesktop = this.desktops[0];
|
||||||
private _activeWindow: KwinClient|null = null;
|
private _activeWindow: KwinClient|null = null;
|
||||||
|
|
||||||
public readonly currentDesktopChanged = new MockQSignal<[]>();
|
public readonly currentDesktopChanged = new MockQSignal<[]>();
|
||||||
@@ -30,6 +30,7 @@ class MockWorkspace {
|
|||||||
|
|
||||||
public createWindows(...kwinClients: MockKwinClient[]) {
|
public createWindows(...kwinClients: MockKwinClient[]) {
|
||||||
for (const kwinClient of kwinClients) {
|
for (const kwinClient of kwinClients) {
|
||||||
|
this.windows.push(kwinClient);
|
||||||
this.windowAdded.fire(kwinClient);
|
this.windowAdded.fire(kwinClient);
|
||||||
this.activeWindow = kwinClient;
|
this.activeWindow = kwinClient;
|
||||||
}
|
}
|
||||||
@@ -41,6 +42,7 @@ class MockWorkspace {
|
|||||||
|
|
||||||
public createClientsWithFrames(...frames: MockQmlRect[]) {
|
public createClientsWithFrames(...frames: MockQmlRect[]) {
|
||||||
const clients = frames.map(rect => new MockKwinClient(rect));
|
const clients = frames.map(rect => new MockKwinClient(rect));
|
||||||
|
clients.forEach((client, index) => client.caption = `Client ${index}`);
|
||||||
this.createWindows(...clients);
|
this.createWindows(...clients);
|
||||||
return clients;
|
return clients;
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,39 @@ class MockWorkspace {
|
|||||||
return this.createClientsWithFrames(...widths.map(width => new MockQmlRect(randomInt(100), randomInt(100), width, 100+randomInt(400))));
|
return this.createClientsWithFrames(...widths.map(width => new MockQmlRect(randomInt(100), randomInt(100), width, 100+randomInt(400))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public removeWindow(window: MockKwinClient) {
|
||||||
|
this.activeWindow = null;
|
||||||
|
runReorder(
|
||||||
|
() => this.windows.splice(this.windows.indexOf(window), 1),
|
||||||
|
() => this.windowRemoved.fire(window),
|
||||||
|
);
|
||||||
|
if (this.activeWindow === null) {
|
||||||
|
activateRandomWindowOnDesktop(this.currentDesktop);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public moveWindow(window: MockKwinClient, ...deltas: QmlPoint[]) {
|
||||||
|
const frame = window.getFrameGeometryCopy();
|
||||||
|
window.move = true;
|
||||||
|
window.interactiveMoveResizeStarted.fire();
|
||||||
|
|
||||||
|
for (const delta of deltas) {
|
||||||
|
if (delta.x !== 0) {
|
||||||
|
frame.x += delta.x;
|
||||||
|
}
|
||||||
|
if (delta.y !== 0) {
|
||||||
|
frame.y += delta.y;
|
||||||
|
}
|
||||||
|
runOneOf(
|
||||||
|
() => window.frameGeometry.set(frame),
|
||||||
|
() => window.frameGeometry = frame,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.move = false;
|
||||||
|
window.interactiveMoveResizeFinished.fire();
|
||||||
|
}
|
||||||
|
|
||||||
public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) {
|
public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) {
|
||||||
const frame = window.getFrameGeometryCopy();
|
const frame = window.getFrameGeometryCopy();
|
||||||
if (edgeResize) {
|
if (edgeResize) {
|
||||||
@@ -81,13 +116,22 @@ class MockWorkspace {
|
|||||||
runOneOf(
|
runOneOf(
|
||||||
() => window.frameGeometry.set(frame),
|
() => window.frameGeometry.set(frame),
|
||||||
() => window.frameGeometry = frame,
|
() => window.frameGeometry = frame,
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.resize = false;
|
window.resize = false;
|
||||||
window.interactiveMoveResizeFinished.fire();
|
window.interactiveMoveResizeFinished.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get currentDesktop() {
|
||||||
|
return this._currentDesktop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set currentDesktop(currentDesktop: KwinDesktop) {
|
||||||
|
this._currentDesktop = currentDesktop;
|
||||||
|
this.currentDesktopChanged.fire();
|
||||||
|
}
|
||||||
|
|
||||||
public get activeWindow() {
|
public get activeWindow() {
|
||||||
return this._activeWindow;
|
return this._activeWindow;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ function runMaybe(f: () => void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runOneOf(...fs: (() => void)[]) {
|
function runOneOf<T>(...fs: (() => T)[]) {
|
||||||
const index = randomInt(fs.length);
|
const index = randomInt(fs.length);
|
||||||
runLog.push(`${getStackFrame(1)} - Chose ${index}`);
|
runLog.push(`${getStackFrame(1)} - Chose ${index}`);
|
||||||
fs[index]();
|
return fs[index]();
|
||||||
}
|
}
|
||||||
|
|
||||||
function runReorder(...fs: (() => void)[]) {
|
function runReorder(...fs: (() => void)[]) {
|
||||||
@@ -22,10 +22,22 @@ function runReorder(...fs: (() => void)[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runReorderDebug(order: number[], ...fs: (() => void)[]) {
|
||||||
|
for (const index of order) {
|
||||||
|
fs[index]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function randomInt(n: number) {
|
function randomInt(n: number) {
|
||||||
return Math.floor(Math.random() * n);
|
return Math.floor(Math.random() * n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function randomItem(items: any[]) {
|
||||||
|
Assert.assert(items.length > 0);
|
||||||
|
const index = randomInt(items.length);
|
||||||
|
return items[index];
|
||||||
|
}
|
||||||
|
|
||||||
function shuffle(items: any[]) {
|
function shuffle(items: any[]) {
|
||||||
for (let n = items.length; n > 1; n--) {
|
for (let n = items.length; n > 1; n--) {
|
||||||
const i = n-1;
|
const i = n-1;
|
||||||
|
|||||||
Reference in New Issue
Block a user