41 Commits

Author SHA1 Message Date
Peter Fajdiga
7d0e83ca2f Bump version to 0.16 2026-03-15 09:51:14 +01:00
Jin Liu
b8650a2e1f Add "Increase column width to maximum/minimum" actions (#148)
The width is set to the maximum/minimum of preset widths.
2026-03-15 09:13:17 +01:00
Peter Fajdiga
19f275325d config.ui: Reword layering options (issue 155) 2026-03-09 20:21:30 +01:00
Peter Fajdiga
9a5a28db82 Add wl-clipboard to default window rules (issue 161) 2026-03-07 22:15:01 +01:00
Peter Fajdiga
6c698c9c82 Fix frameGeometry, clientGeometry, minSize problems (issue 152) 2026-02-22 07:28:41 +01:00
Peter Fajdiga
973956ed5e MockKwinClient: Add some jitter to frameGeometry, clientGeometry, minSize 2026-02-22 00:05:13 +01:00
Peter Fajdiga
f05ae6686d Remove uses of QmlRect.left, right, top, bottom 2026-02-21 23:52:40 +01:00
Peter Fajdiga
3a47fef028 tests: presetWidths: Include column width actions 2026-02-21 20:03:07 +01:00
Peter Fajdiga
ccf0626795 tests: presetWidths: Add test "Preset Widths custom percentages" 2026-02-21 14:53:32 +01:00
Peter Fajdiga
9ba3bc6b0b Fix formatting in Actions.ts 2026-02-14 13:22:37 +01:00
Peter Fajdiga
99c6a39ac5 Update node_modules/js-yaml 2025-12-26 09:46:33 +01:00
Peter Fajdiga
85d7bbe777 Rename make test 2025-12-26 09:45:47 +01:00
Peter Fajdiga
6f69252001 set focus after scrolling with the touchpad gesture
Co-authored-by: Grafcube <grafcube@disroot.org>
2025-12-20 22:20:38 +01:00
Peter Fajdiga
60bee26e29 remove unused fullyVisible parameters 2025-12-20 13:48:38 +01:00
Peter Fajdiga
7d4eab03b9 bump version to 0.15 2025-11-09 20:57:31 +01:00
Peter Fajdiga
7070e59044 mark cursorFollowsFocus setting as experimental 2025-10-22 21:53:09 +02:00
Peter Fajdiga
7f5745b2cf fix cursorFollowsFocus setting after c7effc8913 2025-10-22 21:01:06 +02:00
Peter Fajdiga
c7752bf20a add error notification for invalid tiled desktops regex 2025-10-21 23:01:00 +02:00
Peter Fajdiga
8149100aac DesktopManager: remove addDesktop call in constructor 2025-10-21 22:57:02 +02:00
Peter Fajdiga
99bf71f0b9 config.ui: shorten tooltip for kcfg_tiledDesktops 2025-10-21 22:48:13 +02:00
Peter Fajdiga
2b882768d9 config.ui: don't use monospace font for kcfg_tiledDesktops editbox 2025-10-21 22:45:42 +02:00
Peter Fajdiga
bb42e4d3ad clean whitespace 2025-10-21 22:16:12 +02:00
SR_team
c7effc8913 Add Desktops settings tab to control which virtual desktops Karousel operates on (#133)
* Add Desktops settings tab to control which virtual desktops Karousel operates on

* Fix cursor follow focus to only work on matched desktops

* Resolve review comments

- Implement a RegExp-based DesktopFilter.
- Add new config key tiledDesktops (String) with default ".*" (match all).
- Make DesktopManager return undefined for desktops that should not be tiled.
- Mark KWinDesktop.name as readonly.
- Replace multiline desktop editor in settings UI with a single-line QLineEdit (moved to top of Window Rules tab), shorten label to "Tiled desktops:" and add a tooltip with examples.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-10-21 21:06:25 +02:00
Peter Fajdiga
2c433867f3 bump package to 0.14 2025-09-28 09:39:17 +02:00
Peter Fajdiga
e995555074 tests: passFocus: remove assertion for null activeWindow 2025-09-13 17:31:12 +02:00
Peter Fajdiga
6a1e018df1 tests: MockWorkspace: only focus a different window if none is focused 2025-09-13 16:58:44 +02:00
Peter Fajdiga
872a67e6e1 clear Focus Passer if another window is focused by anyone 2025-09-13 15:32:19 +02:00
Peter Fajdiga
5a57ba76d8 only clear Focus Passer if another window is focused by Karousel 2025-09-13 15:31:44 +02:00
Peter Fajdiga
55c6932338 always pass Window to Focus Passer 2025-09-13 15:02:27 +02:00
Peter Fajdiga
b1d6193e48 tests: make mocks more accurately mimic kwin behaviour 2025-09-13 12:42:22 +02:00
Peter Fajdiga
456bbf30b4 fix focus passing when moving a column to another desktop 2025-09-13 12:41:21 +02:00
Peter Fajdiga
24c1fa0a38 uncomment passFocus test 2025-09-13 12:26:57 +02:00
Peter Fajdiga
ac7566d2cf fix focus passing when closing windows 2025-09-07 20:37:00 +02:00
Peter Fajdiga
195f4e6d30 don't pass focus when window is moved and followed to a different desktop (issue 116) 2025-09-07 20:37:00 +02:00
Peter Fajdiga
e8f2a50420 tests: add test for kwin shortucts for moving windows to adjacent desktops 2025-09-07 20:37:00 +02:00
Peter Fajdiga
9910bc7041 tests: MockWorkspace.removeWindow: unfocus before focusing 2025-09-07 20:37:00 +02:00
Peter Fajdiga
1b592c5b4b fix detection of full-screen windows for apps that manage their own window decorations 2025-09-07 19:45:36 +02:00
Peter Fajdiga
75384d9fb4 tests: make runOneOf functions optionally return a value 2025-09-07 19:45:36 +02:00
Peter Fajdiga
dba92d3826 upgrade node modules 2025-09-07 19:45:36 +02:00
Peter Fajdiga
dbb95e0470 enable eslint comma-dangle 2025-09-07 19:45:34 +02:00
Peter Fajdiga
056149440d readme: add mention of npm requirement 2025-05-14 21:22:05 +02:00
60 changed files with 1068 additions and 442 deletions

View File

@@ -3,7 +3,7 @@ CHECKS := true
.PHONY: * .PHONY: *
build: lint tests build: lint test
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
@@ -19,7 +19,7 @@ endif
lint-fix: npm-install lint-fix: npm-install
npx eslint ./src --fix npx eslint ./src --fix
tests: test:
ifeq (${CHECKS}, true) ifeq (${CHECKS}, true)
./run-ts.sh ./src/tests ./run-ts.sh ./src/tests
endif endif

View File

@@ -27,7 +27,7 @@ First install the _org.kde.notification_ QML module (_qml-module-org-kde-notific
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_. Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
Or clone the repo and run `make install` (requires node and tsc). 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.

View File

@@ -8,6 +8,7 @@ export default tseslint.config(
rules: { rules: {
"@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-empty-function": "off",
"semi": "error", "semi": "error",
"comma-dangle": ["error", "always-multiline"],
"indent": ["error", 4], "indent": ["error", 4],
}, },
} }

125
package-lock.json generated
View File

@@ -4,15 +4,16 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "karousel",
"devDependencies": { "devDependencies": {
"eslint": "^9.24.0", "eslint": "^9.24.0",
"typescript-eslint": "^8.30.1" "typescript-eslint": "^8.30.1"
} }
}, },
"node_modules/@eslint-community/eslint-utils": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.6.1", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz",
"integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
@@ -49,9 +50,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.20.0", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/object-schema": "^2.1.6", "@eslint/object-schema": "^2.1.6",
@@ -63,18 +64,18 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.2.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
"integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.12.0", "version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.15" "@types/json-schema": "^7.0.15"
@@ -107,12 +108,15 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.24.0", "version": "9.35.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
"integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
@@ -125,30 +129,18 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.8", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint/core": "^0.13.0", "@eslint/core": "^0.15.2",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@eslint/plugin-kit/node_modules/@eslint/core": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
"dev": true,
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -390,9 +382,9 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -454,9 +446,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@@ -518,9 +510,9 @@
"dev": true "dev": true
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
@@ -638,19 +630,19 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.24.0", "version": "9.35.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
"integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.20.0", "@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.2.0", "@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.12.0", "@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.24.0", "@eslint/js": "9.35.0",
"@eslint/plugin-kit": "^0.2.7", "@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@@ -661,9 +653,9 @@
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.3.0", "eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.0", "eslint-visitor-keys": "^4.2.1",
"espree": "^10.3.0", "espree": "^10.4.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -698,9 +690,9 @@
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.3.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esrecurse": "^4.3.0", "esrecurse": "^4.3.0",
@@ -714,9 +706,9 @@
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -726,14 +718,14 @@
} }
}, },
"node_modules/espree": { "node_modules/espree": {
"version": "10.3.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"acorn": "^8.14.0", "acorn": "^8.15.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1008,10 +1000,11 @@
"dev": true "dev": true
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },

View File

@@ -32,7 +32,7 @@
<item> <item>
<widget class="QCheckBox" name="kcfg_cursorFollowsFocus"> <widget class="QCheckBox" name="kcfg_cursorFollowsFocus">
<property name="text"> <property name="text">
<string>Cursor follows focus</string> <string>Cursor follows focus (experimental)</string>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>When a window gains focus, move the cursor to it</string> <string>When a window gains focus, move the cursor to it</string>
@@ -149,14 +149,14 @@
<item> <item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow"> <widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text"> <property name="text">
<string>Keep tiled windows below</string> <string>Set "Keep Below" for tiled windows</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove"> <widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text"> <property name="text">
<string>Keep floating windows above</string> <string>Set "Keep Above" for floating windows</string>
</property> </property>
</widget> </widget>
</item> </item>
@@ -458,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">

View File

@@ -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"

View File

@@ -9,7 +9,7 @@
"Name": "Peter Fajdiga" "Name": "Peter Fajdiga"
}], }],
"Id": "karousel", "Id": "karousel",
"Version": "0.13", "Version": "0.16",
"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"

View File

@@ -2,6 +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; declare const moveCursorToFocus: DBusCall;

View File

@@ -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) {

View File

@@ -13,8 +13,8 @@ class ContextualResizer {
return; return;
} }
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true); const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true); const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
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;
@@ -50,8 +50,8 @@ class ContextualResizer {
return; return;
} }
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true); const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true); const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
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;
@@ -86,4 +86,22 @@ class ContextualResizer {
column.setWidth(newWidth, true); column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column); desktop.scrollCenterVisible(column);
} }
public maximizeWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const maxWidth = presetWidths[presetWidths.length-1];
column.setWidth(maxWidth, true);
desktop.scrollCenterVisible(column);
}
public minimizeWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const minWidth = presetWidths[0];
column.setWidth(minWidth, true);
desktop.scrollCenterVisible(column);
}
} }

View File

@@ -28,4 +28,16 @@ class RawResizer {
} }
column.setWidth(newWidth, true); column.setWidth(newWidth, true);
} }
public maximizeWidth(column: Column) {
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const maxWidth = presetWidths[presetWidths.length-1];
column.setWidth(maxWidth, true);
}
public minimizeWidth(column: Column) {
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const minWidth = presetWidths[0];
column.setWidth(minWidth, true);
}
} }

View File

@@ -25,4 +25,5 @@ interface Config {
tiledKeepBelow: boolean; tiledKeepBelow: boolean;
floatingKeepAbove: boolean; floatingKeepAbove: boolean;
windowRules: string; windowRules: string;
tiledDesktops: string;
} }

View File

@@ -31,6 +31,11 @@ const defaultWindowRules = `[
"class": "(org\\\\.kde\\\\.)?yakuake", "class": "(org\\\\.kde\\\\.)?yakuake",
"tile": false "tile": false
}, },
{
"class": "wl-copy|wl-paste",
"caption": "wl-clipboard",
"tile": false
},
{ {
"class": "steam", "class": "steam",
"caption": "Steam Big Picture Mode", "caption": "Steam Big Picture Mode",
@@ -188,5 +193,10 @@ const configDef = [
name: "windowRules", name: "windowRules",
type: "String", type: "String",
default: defaultWindowRules, default: defaultWindowRules,
} },
{
name: "tiledDesktops",
type: "String",
default: ".*",
},
]; ];

View File

@@ -102,6 +102,7 @@ interface KwinDesktop {
__brand: "KwinDesktop"; __brand: "KwinDesktop";
readonly id: string; readonly id: string;
readonly name: string;
} }
interface ShortcutHandler extends QmlObject { interface ShortcutHandler extends QmlObject {

View File

@@ -28,10 +28,6 @@ interface QmlRect {
y: number; y: number;
width: number; width: number;
height: number; height: number;
readonly top: number;
readonly bottom: number; // top + height
readonly left: number;
readonly right: number; // left + width
} }
interface QmlSize { interface QmlSize {

View File

@@ -8,7 +8,7 @@ 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) => {
@@ -16,7 +16,7 @@ class Actions {
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) => {
@@ -62,21 +62,29 @@ class Actions {
}; };
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) => {
@@ -86,12 +94,12 @@ 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);
} }
}; };
@@ -102,12 +110,12 @@ class Actions {
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);
} }
}; };
@@ -141,12 +149,12 @@ class Actions {
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) => {
@@ -184,6 +192,15 @@ class Actions {
this.config.columnResizer.decreaseWidth(column); this.config.columnResizer.decreaseWidth(column);
}; };
public readonly columnWidthMaximize = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.maximizeWidth(column);
};
public readonly columnWidthMinimize = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.minimizeWidth(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);
@@ -196,8 +213,11 @@ class Actions {
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));
const availableSpace = desktop.tilingArea.width; const availableSpace = desktop.tilingArea.width;
const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1); const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1);
@@ -219,7 +239,7 @@ class Actions {
return; return;
} }
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true)); const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column"); console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]); const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]);
@@ -246,7 +266,7 @@ class Actions {
return; return;
} }
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true)); const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column"); console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]); const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]);
@@ -297,11 +317,18 @@ class Actions {
}; };
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => { private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
desktopManager.getCurrentDesktop().adjustScroll(amount, false); const desktop = desktopManager.getCurrentDesktop();
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;
@@ -310,7 +337,11 @@ class Actions {
}; };
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;
@@ -328,8 +359,12 @@ class Actions {
}; };
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => { public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid; const desktop = dm.getCurrentDesktop();
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true); if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) { if (column === null) {
return; return;
} }
@@ -343,8 +378,12 @@ class Actions {
}; };
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => { public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid; const desktop = dm.getCurrentDesktop();
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true); if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) { if (column === null) {
return; return;
} }
@@ -362,12 +401,16 @@ class Actions {
}; };
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) => {
@@ -375,7 +418,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();
}; };
@@ -396,7 +439,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,7 +455,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;
} }
@@ -422,12 +473,14 @@ namespace Actions {
presetWidths: { presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number; next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
}; };
columnResizer: ColumnResizer; columnResizer: ColumnResizer;
} }
export interface ColumnResizer { export interface ColumnResizer {
increaseWidth(column: Column): void; increaseWidth(column: Column): void;
decreaseWidth(column: Column): void; decreaseWidth(column: Column): void;
maximizeWidth(column: Column): void;
minimizeWidth(column: Column): void;
} }
} }

View File

@@ -146,6 +146,16 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
defaultKeySequence: "Meta+Ctrl+-", defaultKeySequence: "Meta+Ctrl+-",
action: () => world.doIfTiledFocused(actions.columnWidthDecrease), action: () => world.doIfTiledFocused(actions.columnWidthDecrease),
}, },
{
name: "column-width-maximize",
description: "Increase column width to maximum",
action: () => world.doIfTiledFocused(actions.columnWidthMaximize),
},
{
name: "column-width-minimize",
description: "Decrease column width to minimum",
action: () => world.doIfTiledFocused(actions.columnWidthMinimize),
},
{ {
name: "cycle-preset-widths", name: "cycle-preset-widths",
description: "Cycle through preset column widths", description: "Cycle through preset column widths",

View File

@@ -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, this.isFocused()); 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()) {
@@ -79,7 +79,7 @@ class Column {
public getMinWidth() { public getMinWidth() {
let maxMinWidth = Column.minWidth; let maxMinWidth = Column.minWidth;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
const minWidth = window.client.kwinClient.minSize.width; const minWidth = window.client.kwinClient.minSize.width.ceil();
if (minWidth > maxMinWidth) { if (minWidth > maxMinWidth) {
maxMinWidth = minWidth; maxMinWidth = minWidth;
} }
@@ -195,12 +195,8 @@ 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;
}
window.focus();
} }
public isFocused() { public isFocused() {
@@ -273,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);
@@ -288,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;
}
} }
} }
@@ -308,7 +311,7 @@ class Column {
} }
} }
private destroy(passFocus: boolean) { private destroy(passFocus: FocusPassing.Type) {
this.grid.onColumnRemoved(this, passFocus); this.grid.onColumnRemoved(this, passFocus);
} }
} }

View File

@@ -14,13 +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.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);
} }
@@ -140,8 +141,20 @@ class Desktop {
this.setScroll(this.gestureScrollXInitial + this.config.gestureScrollStep * amount, false); this.setScroll(this.gestureScrollXInitial + this.config.gestureScrollStep * amount, false);
} }
public gestureScrollFinish() { public gestureScrollFinish(focusedWindow: Window|null) {
const scrolledRight = this.scrollX > this.gestureScrollXInitial!;
this.gestureScrollXInitial = null; this.gestureScrollXInitial = null;
const visibleRange = this.getCurrentVisibleRange();
if (focusedWindow !== null && !Range.contains(visibleRange, focusedWindow.column)) {
// the focused window is no longer visible, find a new window to focus
const focusTargetColumn = scrolledRight ?
this.grid.getLeftmostVisibleColumn(visibleRange) :
this.grid.getRightmostVisibleColumn(visibleRange);
if (focusTargetColumn !== null) {
focusTargetColumn.getWindowToFocus().focus();
}
}
} }
public arrange() { public arrange() {

View File

@@ -1,15 +1,17 @@
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;
@@ -104,7 +106,7 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal; this.width = x - this.config.gapsInnerHorizontal;
} }
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) { public getLeftmostVisibleColumn(visibleRange: Range) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) { if (Range.contains(visibleRange, column)) {
return column; return column;
@@ -113,7 +115,7 @@ class Grid {
return null; return null;
} }
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) { public getRightmostVisibleColumn(visibleRange: Range) {
let last = null; let last = null;
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) { if (Range.contains(visibleRange, column)) {
@@ -125,7 +127,7 @@ class Grid {
return last; return last;
} }
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) { public *getVisibleColumns(visibleRange: Range) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) { if (Range.contains(visibleRange, column)) {
yield column; yield column;
@@ -156,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;
@@ -168,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) {

View File

@@ -7,7 +7,7 @@ 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.round();
let maximizedMode = this.client.getMaximizedMode(); let maximizedMode = this.client.getMaximizedMode();
if (maximizedMode === undefined) { if (maximizedMode === undefined) {
@@ -23,11 +23,11 @@ class Window {
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, this.isFocused() && targetColumn.grid !== this.column.grid); this.column.onWindowRemoved(this, passFocus);
this.column = targetColumn; this.column = targetColumn;
targetColumn.onWindowAdded(this, bottom); targetColumn.onWindowAdded(this, bottom);
} }
@@ -61,6 +61,11 @@ class Window {
public focus() { public focus() {
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() {
@@ -118,11 +123,11 @@ class Window {
public onFrameGeometryChanged() { public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry; const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true); this.column.setWidth(newGeometry.width.round(), true);
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);
} }
} }

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

View File

@@ -52,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);

View File

@@ -20,6 +20,6 @@ function initQmlTimer() {
return Qt.createQmlObject( return Qt.createQmlObject(
`import QtQuick 6.0 `import QtQuick 6.0
Timer {}`, Timer {}`,
qmlBase qmlBase,
) as QmlTimer; ) as QmlTimer;
} }

View File

@@ -1,3 +1,21 @@
interface Number {
round(this: number): number;
floor(this: number): number;
ceil(this: number): number;
}
Number.prototype.round = function() {
return Math.round(this);
};
Number.prototype.floor = function() {
return Math.floor(this);
};
Number.prototype.ceil = function() {
return Math.ceil(this);
};
interface Function { interface Function {
partial<H extends any[], T extends any[], R>( partial<H extends any[], T extends any[], R>(
this: (...args: [...H, ...T]) => R, this: (...args: [...H, ...T]) => R,

View File

@@ -24,9 +24,34 @@ function pointEquals(a: QmlPoint, b: QmlPoint) {
a.y === b.y; a.y === b.y;
} }
function rectContainsPoint(rect: QmlRect, point: QmlPoint) { function rectRight(rect: QmlRect) {
return rect.left <= point.x && return rect.x + rect.width;
rect.right >= point.x && }
rect.top <= point.y &&
rect.bottom >= point.y; function rectBottom(rect: QmlRect) {
return rect.y + rect.height;
}
function rectContainsPoint(rect: QmlRect, point: QmlPoint) {
return rect.x <= point.x &&
rectRight(rect) >= point.x &&
rect.y <= point.y &&
rectBottom(rect) >= point.y;
}
function roundQtRect(rect: QmlRect) {
return Qt.rect(
rect.x.round(),
rect.y.round(),
rect.width.round(),
rect.height.round(),
);
}
function rectRightRound(rect: QmlRect) {
return rect.x.round() + rect.width.round();
}
function rectBottomRound(rect: QmlRect) {
return rect.y.round() + rect.height.round();
} }

View File

@@ -1,4 +1,4 @@
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) => {
@@ -9,17 +9,19 @@ function initWorkspaceSignalHandlers(world: World) {
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, () => {

View File

@@ -31,16 +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) &&
this.windowRuleEnforcer.shouldTile(kwinClient) this.windowRuleEnforcer.shouldTile(kwinClient) &&
(desktop = this.desktopManager.getDesktopForClient(kwinClient)) !== undefined
) { ) {
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);
@@ -55,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);
} }
@@ -84,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,
); );
} }
} }
@@ -95,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) {
@@ -131,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();
@@ -144,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();
@@ -164,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);
} }
} }
@@ -204,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);
} }
} }

View File

@@ -21,7 +21,7 @@ class ClientWrapper {
} }
this.signalManager = ClientWrapper.initSignalManager(this); this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager; this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width; this.preferredWidth = kwinClient.frameGeometry.width.round();
this.manipulatingGeometry = new Doer(); this.manipulatingGeometry = new Doer();
this.lastPlacement = null; this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this)); this.stateManager = new ClientState.Manager(constructInitialState(this));
@@ -49,10 +49,10 @@ class ClientWrapper {
if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) { if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry; const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect( this.kwinClient.frameGeometry = Qt.rect(
frame.x + dx, frame.x.round() + dx,
frame.y + dy, frame.y.round() + dy,
frame.width, frame.width.round(),
frame.height, frame.height.round(),
); );
} }
@@ -142,15 +142,15 @@ class ClientWrapper {
if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) { if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return; return;
} }
const frame = this.kwinClient.frameGeometry; const frame = roundQtRect(this.kwinClient.frameGeometry);
if (frame.left < screenSize.left) { if (frame.x < screenSize.x) {
frame.x = screenSize.left; frame.x = screenSize.x;
} else if (frame.right > screenSize.right) { } else if (rectRight(frame) > rectRight(screenSize)) {
frame.x = screenSize.right - frame.width; frame.x = rectRight(screenSize) - frame.width;
} }
} }
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) {

View File

@@ -47,8 +47,8 @@ namespace Clients {
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.round() >= fullScreenArea.width &&
kwinClient.clientGeometry.height === fullScreenArea.height; kwinClient.clientGeometry.height.round() >= fullScreenArea.height;
} }
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) { export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {

View File

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

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

View File

@@ -14,7 +14,7 @@ class PinManager {
} }
public getAvailableSpace(kwinDesktop: KwinDesktop, screen: QmlRect) { public getAvailableSpace(kwinDesktop: KwinDesktop, screen: QmlRect) {
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right); const baseLot = new PinManager.Lot(screen.y, rectBottom(screen), screen.x, rectRight(screen));
let lots = [baseLot]; let lots = [baseLot];
for (const client of this.pinnedClients) { for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, kwinDesktop) || client.minimized) { if (!Clients.isOnVirtualDesktop(client, kwinDesktop) || client.minimized) {
@@ -23,7 +23,7 @@ class PinManager {
const newLots: PinManager.Lot[] = []; const newLots: PinManager.Lot[] = [];
for (const lot of lots) { for (const lot of lots) {
lot.split(newLots, client.frameGeometry); lot.split(newLots, roundQtRect(client.frameGeometry));
} }
lots = newLots; lots = newLots;
} }
@@ -60,23 +60,23 @@ namespace PinManager {
return; return;
} }
if (obstacle.top - this.top >= Lot.minHeight) { if (obstacle.y - this.top >= Lot.minHeight) {
destLots.push(new Lot(this.top, obstacle.top, this.left, this.right)); destLots.push(new Lot(this.top, obstacle.y, this.left, this.right));
} }
if (this.bottom - obstacle.bottom >= Lot.minHeight) { if (this.bottom - rectBottom(obstacle) >= Lot.minHeight) {
destLots.push(new Lot(obstacle.bottom, this.bottom, this.left, this.right)); destLots.push(new Lot(rectBottom(obstacle), this.bottom, this.left, this.right));
} }
if (obstacle.left - this.left >= Lot.minWidth) { if (obstacle.x - this.left >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, this.left, obstacle.left)); destLots.push(new Lot(this.top, this.bottom, this.left, obstacle.x));
} }
if (this.right - obstacle.right >= Lot.minWidth) { if (this.right - rectRight(obstacle) >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, obstacle.right, this.right)); destLots.push(new Lot(this.top, this.bottom, rectRight(obstacle), this.right));
} }
} }
private contains(obstacle: QmlRect) { private contains(obstacle: QmlRect) {
return obstacle.right > this.left && obstacle.left < this.right && return rectRight(obstacle) > this.left && obstacle.x < this.right &&
obstacle.bottom > this.top && obstacle.top < this.bottom; rectBottom(obstacle) > this.top && obstacle.y < this.bottom;
} }
public area() { public area() {

View File

@@ -8,7 +8,8 @@ class World {
private readonly cursorFollowsFocus: boolean; 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; this.cursorFollowsFocus = config.cursorFollowsFocus;
let presetWidths = { let presetWidths = {
@@ -68,8 +69,8 @@ class World {
gestureScrollStep: config.gestureScrollStep, 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();
@@ -96,13 +97,21 @@ class World {
} }
private update() { private update() {
this.desktopManager.getCurrentDesktop().arrange(); const currentDesktop = this.desktopManager.getCurrentDesktop();
this.moveCursorToFocus(); if (currentDesktop !== undefined) {
currentDesktop.arrange();
this.moveCursorToFocus();
}
} }
private moveCursorToFocus() { private moveCursorToFocus() {
if (this.cursorFollowsFocus && Workspace.activeWindow !== null) { if (this.cursorFollowsFocus && Workspace.activeWindow !== null) {
const cursorAlreadyInFocus = rectContainsPoint(Workspace.activeWindow.frameGeometry, Workspace.cursorPos); // Only move cursor for tiled windows
const tiledWindow = this.clientManager.findTiledWindow(Workspace.activeWindow);
if (tiledWindow === null) {
return;
}
const cursorAlreadyInFocus = rectContainsPoint(roundQtRect(Workspace.activeWindow.frameGeometry), Workspace.cursorPos);
if (cursorAlreadyInFocus) { if (cursorAlreadyInFocus) {
return; return;
} }
@@ -139,11 +148,23 @@ class World {
} }
public gestureScroll(amount: number) { public gestureScroll(amount: number) {
this.do((clientManager, desktopManager) => desktopManager.getCurrentDesktop().gestureScroll(amount)); this.do((clientManager, desktopManager) => {
const currentDesktop = desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
currentDesktop.gestureScroll(amount);
}
});
} }
public gestureScrollFinish() { public gestureScrollFinish() {
this.do((clientManager, desktopManager) => desktopManager.getCurrentDesktop().gestureScrollFinish()); this.do((clientManager, desktopManager) => {
const focusedWindow = Workspace.activeWindow === null ? null : clientManager.findTiledWindow(Workspace.activeWindow);
const currentDesktop = desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
console.assert(focusedWindow === null || focusedWindow.column.grid.desktop === currentDesktop);
currentDesktop.gestureScrollFinish(focusedWindow);
}
});
} }
public destroy() { public destroy() {

View File

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

View File

@@ -16,7 +16,7 @@ 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();
} }
@@ -30,10 +30,10 @@ namespace ClientState {
const clientRect = client.kwinClient.frameGeometry; const clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth; const width = client.preferredWidth;
client.place( client.place(
clientRect.x, clientRect.x.round(),
clientRect.y, clientRect.y.round(),
width, width,
Math.min(clientRect.height, Math.round(placementArea.height / 2)), Math.min(clientRect.height.round(), Math.round(placementArea.height / 2)),
); );
} }

View File

@@ -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 interface State { export interface State {
destroy(passFocus: boolean): void; destroy(passFocus: FocusPassing.Type): void;
} }
} }

View File

@@ -17,7 +17,7 @@ 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();
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)) {

View File

@@ -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;
@@ -123,7 +123,12 @@ namespace ClientState {
return; return;
} }
const newGeometry = client.kwinClient.frameGeometry; const newGeometry = roundQtRect(client.kwinClient.frameGeometry);
if (rectEquals(oldGeometry, newGeometry)) {
// no real changes, nothing to do
return;
}
const oldCenterX = oldGeometry.x + oldGeometry.width/2; const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2; const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2; const newCenterX = newGeometry.x + newGeometry.width/2;
@@ -142,7 +147,7 @@ namespace ClientState {
window.column.onUserResizeWidth( window.column.onUserResizeWidth(
resizeStartWidth, resizeStartWidth,
newGeometry.width - resizeStartWidth, newGeometry.width - resizeStartWidth,
newGeometry.left !== oldGeometry.left, newGeometry.x !== oldGeometry.x,
resizeNeighbor, resizeNeighbor,
); );
} }
@@ -193,9 +198,9 @@ namespace ClientState {
private static getResizeNeighborColumn(window: Window) { private static getResizeNeighborColumn(window: Window) {
const kwinClient = window.client.kwinClient; const kwinClient = window.client.kwinClient;
const column = window.column; const column = window.column;
if (Workspace.cursorPos.x > kwinClient.clientGeometry.right) { if (Workspace.cursorPos.x > rectRightRound(kwinClient.clientGeometry)) {
return column.grid.getRightColumn(column); return column.grid.getRightColumn(column);
} else if (Workspace.cursorPos.x < kwinClient.clientGeometry.left) { } else if (Workspace.cursorPos.x < kwinClient.clientGeometry.x.round()) {
return column.grid.getLeftColumn(column); return column.grid.getLeftColumn(column);
} else { } else {
return null; return null;
@@ -209,7 +214,8 @@ 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) {

View File

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

View File

@@ -1,4 +1,4 @@
tests.register("Center focused", 1, () => { tests.register("Center focused", 5, () => {
const config = getDefaultConfig(); const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config); const { qtMock, workspaceMock, world } = init(config);
@@ -14,8 +14,8 @@ tests.register("Center focused", 1, () => {
// center client2 // center client2
qtMock.fireShortcut("karousel-grid-scroll-focused"); qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, tilingArea, client2); Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.frameGeometry); Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.frameGeometry); Assert.fullyVisible(client2.getActualFrameGeometry());
// undo center client2 // undo center client2
qtMock.fireShortcut("karousel-grid-scroll-focused"); qtMock.fireShortcut("karousel-grid-scroll-focused");
@@ -24,14 +24,14 @@ tests.register("Center focused", 1, () => {
// center client2 // center client2
qtMock.fireShortcut("karousel-grid-scroll-focused"); qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, tilingArea, client2); Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.frameGeometry); Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.frameGeometry); Assert.fullyVisible(client2.getActualFrameGeometry());
// focus client1 (no scrolling should occur) // focus client1 (no scrolling should occur)
qtMock.fireShortcut("karousel-focus-left"); qtMock.fireShortcut("karousel-focus-left");
Assert.centered(config, tilingArea, client2, { message: "No scrolling should have occured" }); Assert.centered(config, tilingArea, client2, { message: "No scrolling should have occured" });
Assert.fullyVisible(client1.frameGeometry); Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.frameGeometry); Assert.fullyVisible(client2.getActualFrameGeometry());
// center client1 // center client1
qtMock.fireShortcut("karousel-grid-scroll-focused"); qtMock.fireShortcut("karousel-grid-scroll-focused");

View File

@@ -1,4 +1,4 @@
tests.register("columns squeeze side", 1, () => { tests.register("columns squeeze side", 5, () => {
const baseTestCases = [ const baseTestCases = [
{ widths: [500, 500], blocked: [false, false], possible: true }, { widths: [500, 500], blocked: [false, false], possible: true },
{ widths: [500, 768], blocked: [false, false], possible: true }, { widths: [500, 768], blocked: [false, false], possible: true },
@@ -46,21 +46,21 @@ tests.register("columns squeeze side", 1, () => {
Assert.columnsFillTilingArea(clients, assertOpt); Assert.columnsFillTilingArea(clients, assertOpt);
for (let i = 0; i < clients.length; i++) { for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) { if (testCase.blocked[i]) {
Assert.equal(clients[i].frameGeometry.width, testCase.widths[i], assertOpt); Assert.equal(clients[i].getActualFrameGeometry().width, testCase.widths[i], assertOpt);
} }
} }
} }
const frames = clients.map(client => client.frameGeometry); const frames = clients.map(client => client.getActualFrameGeometry());
qtMock.fireShortcut(testCase.action); qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry); const newFrames = clients.map(client => client.getActualFrameGeometry());
for (let i = 0; i < clients.length; i++) { for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], assertOpt); Assert.equalRects(frames[i], newFrames[i], assertOpt);
} }
} }
}); });
tests.register("columns squeeze side (just scroll)", 1, () => { tests.register("columns squeeze side (just scroll)", 5, () => {
const baseTestCases = [ const baseTestCases = [
{ focus: 0, startVisible: [true, true, false], endVisible: [true, true, false] }, { focus: 0, startVisible: [true, true, false], endVisible: [true, true, false] },
{ focus: 1, startVisible: [false, true, true], endVisible: [true, true, false] }, { focus: 1, startVisible: [false, true, true], endVisible: [true, true, false] },
@@ -91,12 +91,12 @@ tests.register("columns squeeze side (just scroll)", 1, () => {
const config = getDefaultConfig(); const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config); const { qtMock, workspaceMock, world } = init(config);
function assertVisible(clients: KwinClient[], visible: boolean[]) { function assertVisible(clients: MockKwinClient[], visible: boolean[]) {
for (let i = 0; i < clients.length; i++) { for (let i = 0; i < clients.length; i++) {
if (visible[i]) { if (visible[i]) {
Assert.fullyVisible(clients[i].frameGeometry, { message: assertMsg, skip: 1 }); Assert.fullyVisible(clients[i].getActualFrameGeometry(), { message: assertMsg, skip: 1 });
} else { } else {
Assert.notFullyVisible(clients[i].frameGeometry, { message: assertMsg, skip: 1 }); Assert.notFullyVisible(clients[i].getActualFrameGeometry(), { message: assertMsg, skip: 1 });
} }
} }
} }
@@ -114,9 +114,9 @@ tests.register("columns squeeze side (just scroll)", 1, () => {
qtMock.fireShortcut(testCase.action); qtMock.fireShortcut(testCase.action);
assertVisible(clients, testCase.endVisible); assertVisible(clients, testCase.endVisible);
const frames = clients.map(client => client.frameGeometry); const frames = clients.map(client => client.getActualFrameGeometry());
qtMock.fireShortcut(testCase.action); qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry); const newFrames = clients.map(client => client.getActualFrameGeometry());
for (let i = 0; i < clients.length; i++) { for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], { message: assertMsg }); Assert.equalRects(frames[i], newFrames[i], { message: assertMsg });
} }

View File

@@ -5,32 +5,78 @@ tests.register("Drag tiled window, untile", 10, () => {
const [client1, client2] = workspaceMock.createClients(2); const [client1, client2] = workspaceMock.createClients(2);
const initialCursorPos = new MockQmlPoint(380, 20); const initialCursorPos = new MockQmlPoint(380, 20);
Assert.assert(rectContainsPoint(client1.frameGeometry, initialCursorPos), { message: "invalid test setup" }); Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), initialCursorPos), { message: "invalid test setup" });
workspaceMock.cursorPos = initialCursorPos.clone(); workspaceMock.cursorPos = initialCursorPos.clone();
runOneOf( runOneOf(
() => Workspace.activeWindow = client1, () => { Workspace.activeWindow = client1; },
() => qtMock.fireShortcut("karousel-focus-1"), () => { qtMock.fireShortcut("karousel-focus-1"); },
); );
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos)); Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos)); Assert.assert(!rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos), { message: "Cursor should not have been moved because it was already within the focused client" }); Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos), { message: "Cursor should not have been moved because it was already within the focused client" });
runOneOf( runOneOf(
() => Workspace.activeWindow = client2, () => { Workspace.activeWindow = client2; },
() => qtMock.fireShortcut("karousel-focus-2"), () => { qtMock.fireShortcut("karousel-focus-2"); },
); );
Assert.assert(!rectContainsPoint(client1.frameGeometry, Workspace.cursorPos)); Assert.assert(!rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(rectContainsPoint(client2.frameGeometry, Workspace.cursorPos)); Assert.assert(rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
runOneOf( runOneOf(
() => Workspace.activeWindow = client1, () => { Workspace.activeWindow = client1; },
() => qtMock.fireShortcut("karousel-focus-1"), () => { qtMock.fireShortcut("karousel-focus-1"); },
); );
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos)); Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos)); Assert.assert(!rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
const lastCursorPos = workspaceMock.cursorPos.clone(); const lastCursorPos = workspaceMock.cursorPos.clone();
Workspace.activeWindow = null; Workspace.activeWindow = null;
Assert.assert(pointEquals(Workspace.cursorPos, lastCursorPos), { message: "Cursor should not have been moved" }); 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.getActualFrameGeometry(), 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" });
});

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

View File

@@ -8,31 +8,31 @@ tests.register("External resize", 1, () => {
function getTiledFrame(width: number) { function getTiledFrame(width: number) {
return new MockQmlRect( return new MockQmlRect(
tilingArea.left + Math.round((tilingArea.width - width) / 2), tilingArea.x + Math.round((tilingArea.width - width) / 2),
tilingArea.top, tilingArea.y,
width, width,
tilingArea.height, tilingArea.height,
); );
} }
const [client] = workspaceMock.createClientsWithFrames(getClientDesiredFrame(100)); const [client] = workspaceMock.createClientsWithFrames(getClientDesiredFrame(100));
Assert.equalRects(client.frameGeometry, getTiledFrame(100), { message: "We should tile the window, respecting its desired width" }); Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(100), { message: "We should tile the window, respecting its desired width" });
function testExternalResizing() { function testExternalResizing() {
client.frameGeometry = getClientDesiredFrame(110); client.frameGeometry = getClientDesiredFrame(110);
Assert.equalRects(client.frameGeometry, getTiledFrame(110), { message: "We should re-arrange the window, respecting its new desired width" }); Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(110), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(120); client.frameGeometry = getClientDesiredFrame(120);
Assert.equalRects(client.frameGeometry, getTiledFrame(120), { message: "We should re-arrange the window, respecting its new desired width" }); Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(120), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(130); client.frameGeometry = getClientDesiredFrame(130);
Assert.equalRects(client.frameGeometry, getTiledFrame(130), { message: "We should re-arrange the window, respecting its new desired width" }); Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(130), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(140); client.frameGeometry = getClientDesiredFrame(140);
Assert.equalRects(client.frameGeometry, getTiledFrame(140), { message: "We should re-arrange the window, respecting its new desired width" }); Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(140), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(200); client.frameGeometry = getClientDesiredFrame(200);
Assert.equalRects(client.frameGeometry, getClientDesiredFrame(200), { message: "We should give up and let the client have its desired frame" }); Assert.equalRects(client.getActualFrameGeometry(), getClientDesiredFrame(200), { message: "We should give up and let the client have its desired frame" });
} }
timeControl(addTime => { timeControl(addTime => {

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

View File

@@ -10,7 +10,7 @@ tests.register("Focus and move windows", 1, () => {
}); });
Assert.assert(workspaceMock.activeWindow === client3); Assert.assert(workspaceMock.activeWindow === client3);
function testLayout(shortcutName: string, grid: KwinClient[][]) { function testLayout(shortcutName: string, grid: MockKwinClient[][]) {
qtMock.fireShortcut(shortcutName); qtMock.fireShortcut(shortcutName);
Assert.grid(config, tilingArea, 100, grid, true, [], { skip: 1 }); Assert.grid(config, tilingArea, 100, grid, true, [], { skip: 1 });
} }

View File

@@ -13,35 +13,35 @@ tests.register("LazyScroller", 20, () => {
const [client3] = workspaceMock.createClientsWithWidths(300); const [client3] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false); Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right); Assert.equal(rectRight(client3.getActualFrameGeometry()), rectRight(tilingArea));
runOneOf( runOneOf(
() => workspaceMock.activeWindow = client2, () => { workspaceMock.activeWindow = client2; },
() => qtMock.fireShortcut("karousel-focus-2"), () => { qtMock.fireShortcut("karousel-focus-2"); },
() => qtMock.fireShortcut("karousel-focus-left"), () => { qtMock.fireShortcut("karousel-focus-left"); },
); );
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false); Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right); Assert.equal(rectRight(client3.getActualFrameGeometry()), rectRight(tilingArea));
runOneOf( runOneOf(
() => workspaceMock.activeWindow = client1, () => { workspaceMock.activeWindow = client1; },
() => qtMock.fireShortcut("karousel-focus-1"), () => { qtMock.fireShortcut("karousel-focus-1"); },
() => qtMock.fireShortcut("karousel-focus-left"), () => { qtMock.fireShortcut("karousel-focus-left"); },
() => qtMock.fireShortcut("karousel-focus-start"), () => { qtMock.fireShortcut("karousel-focus-start"); },
); );
workspaceMock.activeWindow = client1; workspaceMock.activeWindow = client1;
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false); Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left); Assert.equal(client1.getActualFrameGeometry().x, tilingArea.x);
qtMock.fireShortcut("karousel-grid-scroll-focused"); qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false); Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1]], true); Assert.grid(config, tilingArea, 300, [[client1]], true);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = client2, () => { workspaceMock.activeWindow = client2; },
() => qtMock.fireShortcut("karousel-focus-2"), () => { qtMock.fireShortcut("karousel-focus-2"); },
() => qtMock.fireShortcut("karousel-focus-right"), () => { qtMock.fireShortcut("karousel-focus-right"); },
); );
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false); Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left); Assert.equal(client1.getActualFrameGeometry().x, tilingArea.x);
}); });

View File

@@ -14,43 +14,43 @@
Assert.assert(clientManager.hasClient(kwinClient)); Assert.assert(clientManager.hasClient(kwinClient));
}); });
const columnLeftX = tilingArea.left + tilingArea.width/2 - 300/2; const columnLeftX = tilingArea.x + tilingArea.width/2 - 300/2;
const columnTopY = tilingArea.top; const columnTopY = tilingArea.y;
const columnHeight = tilingArea.height; const columnHeight = tilingArea.height;
Assert.assert(!kwinClient.fullScreen); Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight); Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
kwinClient.fullScreen = true; kwinClient.fullScreen = true;
Assert.assert(kwinClient.fullScreen); Assert.assert(kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(kwinClient.frameGeometry, screen); Assert.equalRects(kwinClient.getActualFrameGeometry(), screen);
kwinClient.fullScreen = false; kwinClient.fullScreen = false;
Assert.assert(!kwinClient.fullScreen); Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight); Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
kwinClient.setMaximize(true, true); kwinClient.setMaximize(true, true);
Assert.assert(!kwinClient.fullScreen); Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(kwinClient.frameGeometry, screen); Assert.equalRects(kwinClient.getActualFrameGeometry(), screen);
kwinClient.setMaximize(true, false); kwinClient.setMaximize(true, false);
Assert.assert(!kwinClient.fullScreen); Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.rect(kwinClient.frameGeometry, columnLeftX, 0, 300, screen.height); Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, 0, 300, screen.height);
kwinClient.setMaximize(false, false); kwinClient.setMaximize(false, false);
Assert.assert(!kwinClient.fullScreen); Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true)); Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true)); Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight); Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
}); });
tests.register("Maximize with transient " + suffix, 100, () => { tests.register("Maximize with transient " + suffix, 100, () => {
@@ -66,12 +66,12 @@
}); });
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.keepBelow, shouldKeepBelow(false));
Assert.equal(parent.keepAbove, shouldKeepAbove(false)); Assert.equal(parent.keepAbove, shouldKeepAbove(false));
Assert.equalRects(parent.frameGeometry, screen); Assert.equalRects(parent.getActualFrameGeometry(), screen);
workspaceMock.createWindows(child); workspaceMock.createWindows(child);
world.do((clientManager, desktopManager) => { world.do((clientManager, desktopManager) => {
@@ -80,14 +80,14 @@
Assert.assert(!child.fullScreen); Assert.assert(!child.fullScreen);
Assert.equal(child.keepBelow, shouldKeepBelow(false)); Assert.equal(child.keepBelow, shouldKeepBelow(false));
Assert.equal(child.keepAbove, shouldKeepAbove(false)); Assert.equal(child.keepAbove, shouldKeepAbove(false));
Assert.rect(child.frameGeometry, 14, 24, 50, 50); Assert.rect(child.getActualFrameGeometry(), 14, 24, 50, 50);
Assert.equal(parent.keepBelow, shouldKeepBelow(false)); Assert.equal(parent.keepBelow, shouldKeepBelow(false));
Assert.equal(parent.keepAbove, shouldKeepAbove(false)); Assert.equal(parent.keepAbove, shouldKeepAbove(false));
Assert.equalRects(parent.frameGeometry, screen); Assert.equalRects(parent.getActualFrameGeometry(), screen);
}); });
{ {
function assertWindowed(config: Config, clients: KwinClient[]) { function assertWindowed(config: Config, clients: MockKwinClient[]) {
Assert.assert(!clients[0].fullScreen); Assert.assert(!clients[0].fullScreen);
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true)); Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true)); Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
@@ -100,7 +100,7 @@
Assert.grid(config, tilingArea, [300, 400], [[clients[0]], [clients[1], clients[2]]], true); Assert.grid(config, tilingArea, [300, 400], [[clients[0]], [clients[1], clients[2]]], true);
} }
function assertFullScreenOrMaximized(clients: KwinClient[]) { function assertFullScreenOrMaximized(clients: MockKwinClient[]) {
Assert.assert(!clients[0].fullScreen); Assert.assert(!clients[0].fullScreen);
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true)); Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true)); Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
@@ -109,7 +109,7 @@
Assert.equal(clients[1].keepAbove, shouldKeepAbove(true)); Assert.equal(clients[1].keepAbove, shouldKeepAbove(true));
Assert.equal(clients[2].keepBelow, shouldKeepBelow(false)); Assert.equal(clients[2].keepBelow, shouldKeepBelow(false));
Assert.equal(clients[2].keepAbove, shouldKeepAbove(false)); Assert.equal(clients[2].keepAbove, shouldKeepAbove(false));
Assert.equalRects(clients[2].frameGeometry, screen); Assert.equalRects(clients[2].getActualFrameGeometry(), screen);
} }
tests.register("Re-maximize disabled " + suffix, 100, () => { tests.register("Re-maximize disabled " + suffix, 100, () => {
@@ -123,42 +123,42 @@
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => clients[2].fullScreen = true, () => { clients[2].fullScreen = true; },
() => clients[2].setMaximize(true, true), () => { clients[2].setMaximize(true, true); },
); );
assertFullScreenOrMaximized(clients); assertFullScreenOrMaximized(clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[0], () => { workspaceMock.activeWindow = clients[0]; },
() => qtMock.fireShortcut("karousel-focus-1"), () => { qtMock.fireShortcut("karousel-focus-1"); },
() => qtMock.fireShortcut("karousel-focus-left"), () => { qtMock.fireShortcut("karousel-focus-left"); },
() => qtMock.fireShortcut("karousel-focus-start"), () => { qtMock.fireShortcut("karousel-focus-start"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[2], () => { workspaceMock.activeWindow = clients[2]; },
() => qtMock.fireShortcut("karousel-focus-2"), () => { qtMock.fireShortcut("karousel-focus-2"); },
() => qtMock.fireShortcut("karousel-focus-right"), () => { qtMock.fireShortcut("karousel-focus-right"); },
() => qtMock.fireShortcut("karousel-focus-end"), () => { qtMock.fireShortcut("karousel-focus-end"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => clients[2].fullScreen = true, () => { clients[2].fullScreen = true; },
() => clients[2].setMaximize(true, true), () => { clients[2].setMaximize(true, true); },
); );
assertFullScreenOrMaximized(clients); assertFullScreenOrMaximized(clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[1], () => { workspaceMock.activeWindow = clients[1]; },
() => qtMock.fireShortcut("karousel-focus-up"), () => { qtMock.fireShortcut("karousel-focus-up"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[2], () => { workspaceMock.activeWindow = clients[2]; },
() => qtMock.fireShortcut("karousel-focus-down"), () => { qtMock.fireShortcut("karousel-focus-down"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
}); });
@@ -174,36 +174,36 @@
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => clients[2].fullScreen = true, () => { clients[2].fullScreen = true; },
() => clients[2].setMaximize(true, true), () => { clients[2].setMaximize(true, true); },
); );
assertFullScreenOrMaximized(clients); assertFullScreenOrMaximized(clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[0], () => { workspaceMock.activeWindow = clients[0]; },
() => qtMock.fireShortcut("karousel-focus-1"), () => { qtMock.fireShortcut("karousel-focus-1"); },
() => qtMock.fireShortcut("karousel-focus-left"), () => { qtMock.fireShortcut("karousel-focus-left"); },
() => qtMock.fireShortcut("karousel-focus-start"), () => { qtMock.fireShortcut("karousel-focus-start"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[2], () => { workspaceMock.activeWindow = clients[2]; },
() => qtMock.fireShortcut("karousel-focus-2"), () => { qtMock.fireShortcut("karousel-focus-2"); },
() => qtMock.fireShortcut("karousel-focus-right"), () => { qtMock.fireShortcut("karousel-focus-right"); },
() => qtMock.fireShortcut("karousel-focus-end"), () => { qtMock.fireShortcut("karousel-focus-end"); },
); );
assertFullScreenOrMaximized(clients); assertFullScreenOrMaximized(clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[1], () => { workspaceMock.activeWindow = clients[1]; },
() => qtMock.fireShortcut("karousel-focus-up"), () => { qtMock.fireShortcut("karousel-focus-up"); },
); );
assertWindowed(config, clients); assertWindowed(config, clients);
runOneOf( runOneOf(
() => workspaceMock.activeWindow = clients[2], () => { workspaceMock.activeWindow = clients[2]; },
() => qtMock.fireShortcut("karousel-focus-down"), () => { qtMock.fireShortcut("karousel-focus-down"); },
); );
assertFullScreenOrMaximized(clients); assertFullScreenOrMaximized(clients);
}); });
@@ -232,7 +232,7 @@
Assert.assert(fullScreenClient.fullScreen); Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false)); Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false)); Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.frameGeometry, screen); Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient); Assert.equal(Workspace.activeWindow, fullScreenClient);
{ {
@@ -245,7 +245,7 @@
Assert.assert(fullScreenClient.fullScreen); Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false)); Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false)); Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.frameGeometry, screen); Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient, opts); Assert.equal(Workspace.activeWindow, fullScreenClient, opts);
} }
@@ -259,7 +259,7 @@
Assert.assert(fullScreenClient.fullScreen); Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false)); Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false)); Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.frameGeometry, screen); Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, windowedClient); Assert.equal(Workspace.activeWindow, windowedClient);
} }
}); });
@@ -288,7 +288,7 @@
Assert.assert(fullScreenClient.fullScreen); Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false)); Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false)); Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.frameGeometry, screen); Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient); Assert.equal(Workspace.activeWindow, fullScreenClient);
let expectedColumn2Width = 0; let expectedColumn2Width = 0;

View File

@@ -1,4 +1,4 @@
tests.register("Pass focus", 20, () => { tests.register("Pass focus", 100, () => {
const config = getDefaultConfig(); const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config); const { qtMock, workspaceMock, world } = init(config);
@@ -34,7 +34,4 @@ tests.register("Pass focus", 20, () => {
removeWindow(client0); removeWindow(client0);
Assert.equal(workspaceMock.activeWindow, client6); Assert.equal(workspaceMock.activeWindow, client6);
removeWindow(client6);
Assert.equal(workspaceMock.activeWindow, null);
}); });

View File

@@ -22,26 +22,26 @@ tests.register("Pin", 20, () => {
Assert.grid(config, tilingArea, 100, [ [pinned], [tiled1], [tiled2] ], true); Assert.grid(config, tilingArea, 100, [ [pinned], [tiled1], [tiled2] ], true);
pinned.pin(screenHalfLeft); pinned.pin(screenHalfLeft);
Assert.equalRects(pinned.frameGeometry, screenHalfLeft); Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfLeft);
Assert.grid(config, tilingAreaHalfRight, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingAreaHalfRight, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight); pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight); Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.unpin(); pinned.unpin();
Assert.equalRects(pinned.frameGeometry, screenHalfRight); Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight); pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight); Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = true; pinned.minimized = true;
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = false; pinned.minimized = false;
Assert.equalRects(pinned.frameGeometry, screenHalfRight); Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true); Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
workspaceMock.activeWindow = pinned; workspaceMock.activeWindow = pinned;

View File

@@ -1,4 +1,4 @@
tests.register("Preset Widths default", 1, () => { tests.register("Preset Widths default", 5, () => {
const config = getDefaultConfig(); const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config); const { qtMock, workspaceMock, world } = init(config);
@@ -7,33 +7,48 @@ tests.register("Preset Widths default", 1, () => {
function getRect(columnWidth: number) { function getRect(columnWidth: number) {
return new MockQmlRect( return new MockQmlRect(
tilingArea.left + (tilingArea.width - columnWidth) / 2, tilingArea.x + (tilingArea.width - columnWidth) / 2,
tilingArea.top, tilingArea.y,
columnWidth, columnWidth,
tilingArea.height, tilingArea.height,
); );
} }
const [kwinClient] = workspaceMock.createClientsWithWidths(300); const [kwinClient] = workspaceMock.createClientsWithWidths(300);
Assert.equalRects(kwinClient.frameGeometry, getRect(300)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(300));
qtMock.fireShortcut("karousel-cycle-preset-widths"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
}); });
tests.register("Preset Widths custom", 1, () => { tests.register("Preset Widths custom", 5, () => {
const config = getDefaultConfig(); const config = getDefaultConfig();
config.presetWidths = "500px, 250px, 100px, 50%"; config.presetWidths = "500px, 250px, 100px, 50%";
const { qtMock, workspaceMock, world } = init(config); const { qtMock, workspaceMock, world } = init(config);
@@ -43,39 +58,122 @@ tests.register("Preset Widths custom", 1, () => {
function getRect(columnWidth: number) { function getRect(columnWidth: number) {
return new MockQmlRect( return new MockQmlRect(
tilingArea.left + (tilingArea.width - columnWidth) / 2, tilingArea.x + (tilingArea.width - columnWidth) / 2,
tilingArea.top, tilingArea.y,
columnWidth, columnWidth,
tilingArea.height, tilingArea.height,
); );
} }
const [kwinClient] = workspaceMock.createClientsWithWidths(200); const [kwinClient] = workspaceMock.createClientsWithWidths(200);
Assert.equalRects(kwinClient.frameGeometry, getRect(200)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(200));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(250));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths"); qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(500)); () => qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
qtMock.fireShortcut("karousel-cycle-preset-widths"); );
Assert.equalRects(kwinClient.frameGeometry, getRect(100)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"); qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(100)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(500));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
});
tests.register("Preset Widths custom percentages", 5, () => {
const config = getDefaultConfig();
config.presetWidths = "25%, 50%, 75%, 100%";
const { qtMock, workspaceMock, world } = init(config);
const width100 = tilingArea.width;
const width75 = width100*0.75 - config.gapsInnerHorizontal*0.25;
const width50 = width100*0.50 - config.gapsInnerHorizontal*0.50;
const width25 = width100*0.25 - config.gapsInnerHorizontal*0.75;
function getRect(columnWidth: number) {
return new MockQmlRect(
tilingArea.x + (tilingArea.width - columnWidth) / 2,
tilingArea.y,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(200);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(200));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width50));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width75));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width100));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width25));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"); qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(500)); Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width100));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"); runOneOf(
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); () => qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width75));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width50));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width25));
}); });
tests.register("Preset Widths fill screen uniform", 1, () => { tests.register("Preset Widths fill screen uniform", 1, () => {
@@ -96,12 +194,12 @@ tests.register("Preset Widths fill screen uniform", 1, () => {
qtMock.fireShortcut("karousel-cycle-preset-widths"); qtMock.fireShortcut("karousel-cycle-preset-widths");
} }
const left = tilingArea.left; const left = tilingArea.x;
const right = tilingArea.right; const right = rectRight(tilingArea);
const maxLeftoverPx = nColumns - 1; const maxLeftoverPx = nColumns - 1;
const eps = Math.ceil(maxLeftoverPx / 2); const eps = Math.ceil(maxLeftoverPx / 2);
Assert.between(firstClient!.frameGeometry.left, left, left+eps, { message: `nColumns: ${nColumns}` }); Assert.between(firstClient!.getActualFrameGeometry().x, left, left+eps, { message: `nColumns: ${nColumns}` });
Assert.between(lastClient!.frameGeometry.right, right-eps, right, { message: `nColumns: ${nColumns}` }); Assert.between(rectRight(lastClient!.getActualFrameGeometry()), right-eps, right, { message: `nColumns: ${nColumns}` });
} }
}); });
@@ -123,12 +221,12 @@ tests.register("Preset Widths fill screen non-uniform", 1, () => {
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2; const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
const quarterWidth = halfWidth/2 - config.gapsInnerHorizontal/2; const quarterWidth = halfWidth/2 - config.gapsInnerHorizontal/2;
const height = tilingArea.height; const height = tilingArea.height;
const left1 = tilingArea.left; const left1 = tilingArea.x;
const left2 = left1 + config.gapsInnerHorizontal + quarterWidth; const left2 = left1 + config.gapsInnerHorizontal + quarterWidth;
const left3 = left2 + config.gapsInnerHorizontal + quarterWidth; const left3 = left2 + config.gapsInnerHorizontal + quarterWidth;
Assert.rect(clientThin1.frameGeometry, left1, tilingArea.top, quarterWidth, height); Assert.rect(clientThin1.getActualFrameGeometry(), left1, tilingArea.y, quarterWidth, height);
Assert.rect(clientThin2.frameGeometry, left2, tilingArea.top, quarterWidth, height); Assert.rect(clientThin2.getActualFrameGeometry(), left2, tilingArea.y, quarterWidth, height);
Assert.rect(clientWide.frameGeometry, left3, tilingArea.top, halfWidth, height); Assert.rect(clientWide.getActualFrameGeometry(), left3, tilingArea.y, halfWidth, height);
Assert.equal(clientWide.frameGeometry.right, tilingArea.right); Assert.equal(rectRight(clientWide.getActualFrameGeometry()), rectRight(tilingArea));
}); });

View File

@@ -6,9 +6,9 @@ tests.register("User resize", 10, () => {
let clientLeft: MockKwinClient, clientRightTop: MockKwinClient, clientRightBottom: MockKwinClient; let clientLeft: MockKwinClient, clientRightTop: MockKwinClient, clientRightBottom: MockKwinClient;
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) { function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeft, clientRightTop); const { left, right } = getGridBounds(clientLeft, clientRightTop);
Assert.rect(clientLeft.frameGeometry, left, tilingArea.top, leftWidth, tilingArea.height); Assert.rect(clientLeft.getActualFrameGeometry(), left, tilingArea.y, leftWidth, tilingArea.height);
Assert.rect(clientRightTop.frameGeometry, left+leftWidth+gapH, tilingArea.top, rightWidth, topHeight); Assert.rect(clientRightTop.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, topHeight);
Assert.rect(clientRightBottom.frameGeometry, left+leftWidth+gapH, tilingArea.top+topHeight+gapV, rightWidth, bottomHeight); Assert.rect(clientRightBottom.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y+topHeight+gapV, rightWidth, bottomHeight);
} }
{ {
@@ -89,9 +89,9 @@ tests.register("User resize", 10, () => {
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) { function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeftTop, clientRight); const { left, right } = getGridBounds(clientLeftTop, clientRight);
Assert.rect(clientLeftTop.frameGeometry, left, tilingArea.top, leftWidth, topHeight); Assert.rect(clientLeftTop.getActualFrameGeometry(), left, tilingArea.y, leftWidth, topHeight);
Assert.rect(clientLeftBottom.frameGeometry, left, tilingArea.top+topHeight+gapV, leftWidth, bottomHeight); Assert.rect(clientLeftBottom.getActualFrameGeometry(), left, tilingArea.y+topHeight+gapV, leftWidth, bottomHeight);
Assert.rect(clientRight.frameGeometry, left+leftWidth+gapH, tilingArea.top, rightWidth, tilingArea.height); Assert.rect(clientRight.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, tilingArea.height);
} }
workspaceMock.activeWindow = clientLeftBottom; workspaceMock.activeWindow = clientLeftBottom;

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

View File

@@ -127,7 +127,7 @@ namespace Assert {
config: Config, config: Config,
tilingArea: QmlRect, tilingArea: QmlRect,
columnWidths: number[] | number, columnWidths: number[] | number,
grid: KwinClient[][], grid: MockKwinClient[][],
centered: boolean, centered: boolean,
stackedColumns: number[] = [], stackedColumns: number[] = [],
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
@@ -161,7 +161,7 @@ namespace Assert {
const gridWidth = getGridWidth(); const gridWidth = getGridWidth();
const startX = centered ? const startX = centered ?
tilingArea.x + (tilingArea.width - gridWidth) / 2 : tilingArea.x + (tilingArea.width - gridWidth) / 2 :
grid[0][0].frameGeometry.x; grid[0][0].getActualFrameGeometry().x;
function getColumnX(column: number) { function getColumnX(column: number) {
if (columnWidths instanceof Array) { if (columnWidths instanceof Array) {
@@ -205,7 +205,7 @@ namespace Assert {
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.getActualFrameGeometry(),
getRect(iColumn, iWindow, nColumns, nWindows), getRect(iColumn, iWindow, nColumns, nWindows),
{ message: appendMessage(`column ${iColumn}, window ${iWindow}`, message), skip: skip+1 }, { message: appendMessage(`column ${iColumn}, window ${iWindow}`, message), skip: skip+1 },
); );
@@ -216,13 +216,13 @@ namespace Assert {
export function centered( export function centered(
config: Config, config: Config,
tilingArea: QmlRect, tilingArea: QmlRect,
client:KwinClient, client:MockKwinClient,
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
) { ) {
grid( grid(
config, config,
tilingArea, tilingArea,
client.frameGeometry.width, client.getActualFrameGeometry().width,
[[client]], [[client]],
true, true,
[], [],
@@ -235,7 +235,7 @@ namespace Assert {
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
) { ) {
assert( assert(
rect.left >= tilingArea.left && rect.right <= tilingArea.right, rect.x >= tilingArea.x && rectRight(rect) <= rectRight(tilingArea),
{ {
message: appendMessage(`Rect ${rect} not fully visible`, message), message: appendMessage(`Rect ${rect} not fully visible`, message),
skip: skip + 1, skip: skip + 1,
@@ -248,7 +248,7 @@ namespace Assert {
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
) { ) {
assert( assert(
rect.left < tilingArea.left || rect.right > tilingArea.right, rect.x < tilingArea.x || rectRight(rect) > rectRight(tilingArea),
{ {
message: appendMessage(`Rect ${rect} is fully visible, but shouldn't be`, message), message: appendMessage(`Rect ${rect} is fully visible, but shouldn't be`, message),
skip: skip + 1, skip: skip + 1,
@@ -257,18 +257,18 @@ namespace Assert {
} }
export function columnsFillTilingArea( export function columnsFillTilingArea(
columns: KwinClient[], columns: MockKwinClient[],
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
) { ) {
const options = { message: message, skip: skip+1 }; const options = { message: message, skip: skip+1 };
let x = tilingArea.left; let x = tilingArea.x;
for (const column of columns) { for (const column of columns) {
const width = column.frameGeometry.width; const width = column.getActualFrameGeometry().width;
fullyVisible(column.frameGeometry, options); fullyVisible(column.getActualFrameGeometry(), options);
rect(column.frameGeometry, x, tilingArea.top, width, tilingArea.height, options); rect(column.getActualFrameGeometry(), x, tilingArea.y, width, tilingArea.height, options);
x += width + gapH; x += width + gapH;
} }
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options); equal(rectRight(columns[columns.length-1].getActualFrameGeometry()), rectRight(tilingArea), options);
} }
export function tiledClient( export function tiledClient(

View File

@@ -2,6 +2,7 @@ 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 moveCursorToFocus: DBusCall;
@@ -33,7 +34,7 @@ function init(config: Config) {
__brand: "QmlObject", __brand: "QmlObject",
call: () => { call: () => {
Assert.assert(Workspace.activeWindow !== null, { message: "moveCursorToFocus should never be called if there's no focused window" }); Assert.assert(Workspace.activeWindow !== null, { message: "moveCursorToFocus should never be called if there's no focused window" });
const frame = Workspace.activeWindow!.frameGeometry; const frame = (Workspace.activeWindow! as MockKwinClient).getActualFrameGeometry();
workspaceMock.cursorPos.x = Math.floor(frame.x + frame.width/2); workspaceMock.cursorPos.x = Math.floor(frame.x + frame.width/2);
workspaceMock.cursorPos.y = Math.floor(frame.y + frame.height/2); workspaceMock.cursorPos.y = Math.floor(frame.y + frame.height/2);
}, },
@@ -43,9 +44,9 @@ function init(config: Config) {
return { qtMock, workspaceMock, world }; return { qtMock, workspaceMock, world };
} }
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) { function getGridBounds(clientLeft: MockKwinClient, clientRight: MockKwinClient) {
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left; const columnsWidth = rectRight(clientRight.getActualFrameGeometry()) - clientLeft.getActualFrameGeometry().x;
const left = tilingArea.left + Math.floor((tilingArea.width - columnsWidth) / 2); const left = tilingArea.x + Math.floor((tilingArea.width - columnsWidth) / 2);
const right = left + columnsWidth; const right = left + columnsWidth;
return { left, right }; return { left, right };
} }
@@ -61,3 +62,10 @@ function getClientManager(world: World): ClientManager {
world.do((cm, dm) => clientManager = cm); world.do((cm, dm) => clientManager = cm);
return clientManager!; return clientManager!;
} }
function activateRandomWindowOnDesktop(desktop: KwinDesktop) {
const windows = Workspace.windows.filter(w => w.desktops.includes(desktop));
if (windows.length > 0) {
Workspace.activeWindow = randomItem(windows);
}
}

View File

@@ -4,7 +4,7 @@ class MockKwinClient {
private static readonly borderThickness = 10; private static readonly borderThickness = 10;
public caption = "App"; public caption = "App";
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0); public minSize: Readonly<QmlSize> = new MockQmlSize(randomJitter(), randomJitter());
public readonly transient: boolean; public readonly transient: boolean;
public move = false; public move = false;
public resize = false; public resize = false;
@@ -52,6 +52,7 @@ class MockKwinClient {
this.windowedFrameGeometry = _frameGeometry.clone(); this.windowedFrameGeometry = _frameGeometry.clone();
this.transient = transientFor !== null; this.transient = transientFor !== null;
this._desktops = [Workspace.currentDesktop]; this._desktops = [Workspace.currentDesktop];
this.activities = [Workspace.currentActivity];
} }
setMaximize(vertically: boolean, horizontally: boolean) { setMaximize(vertically: boolean, horizontally: boolean) {
@@ -68,7 +69,7 @@ class MockKwinClient {
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(
@@ -88,7 +89,15 @@ 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
);
} }
} }
@@ -124,18 +133,22 @@ class MockKwinClient {
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,
);
},
() => {}, () => {},
); );
} }
@@ -147,10 +160,22 @@ class MockKwinClient {
); );
} }
public get frameGeometry() { // for assertions
public getActualFrameGeometry() {
return this._frameGeometry; return this._frameGeometry;
} }
// for Karousel
public get frameGeometry() {
return new MockQmlRect(
this._frameGeometry.x + randomJitter(),
this._frameGeometry.y + randomJitter(),
this._frameGeometry.width + randomJitter(),
this._frameGeometry.height + randomJitter(),
this.frameGeometryChanged.fire.bind(this.frameGeometryChanged),
);
}
public set frameGeometry(frameGeometry: MockQmlRect) { public set frameGeometry(frameGeometry: MockQmlRect) {
const oldFrameGeometry = this._frameGeometry; const oldFrameGeometry = this._frameGeometry;
this._frameGeometry = new MockQmlRect( this._frameGeometry = new MockQmlRect(
@@ -186,9 +211,20 @@ class MockKwinClient {
this.desktopsChanged.fire(); this.desktopsChanged.fire();
if (Workspace.activeWindow === this && !desktops.includes(Workspace.currentDesktop)) { if (Workspace.activeWindow === this && !desktops.includes(Workspace.currentDesktop)) {
Workspace.activeWindow = null; 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;
} }

View File

@@ -49,22 +49,6 @@ class MockQmlRect {
this.onChanged(oldRect); this.onChanged(oldRect);
} }
public get top() {
return this.y;
}
public get bottom() {
return this.y + this.height;
}
public get left() {
return this.x;
}
public get right() {
return this.x + this.width;
}
public set(target: QmlRect) { public set(target: QmlRect) {
const oldRect = this.clone(); const oldRect = this.clone();
this._x = target.x; this._x = target.x;

View File

@@ -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 readonly windows: MockKwinClient[] = []; 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<[]>();
@@ -52,13 +52,13 @@ class MockWorkspace {
} }
public removeWindow(window: MockKwinClient) { public removeWindow(window: MockKwinClient) {
this.activeWindow = null;
runReorder( runReorder(
() => this.windows.splice(this.windows.indexOf(window), 1), () => this.windows.splice(this.windows.indexOf(window), 1),
() => this.windowRemoved.fire(window), () => this.windowRemoved.fire(window),
); );
if (window === this.activeWindow) { if (this.activeWindow === null) {
const windows = this.windows.filter(w => w.desktops.includes(this.currentDesktop)); activateRandomWindowOnDesktop(this.currentDesktop);
Workspace.activeWindow = windows.length > 0 ? randomItem(windows) : null;
}; };
} }
@@ -75,7 +75,7 @@ class MockWorkspace {
frame.y += delta.y; frame.y += delta.y;
} }
runOneOf( runOneOf(
() => window.frameGeometry.set(frame), () => window.getActualFrameGeometry().set(frame),
() => window.frameGeometry = frame, () => window.frameGeometry = frame,
); );
} }
@@ -88,8 +88,8 @@ class MockWorkspace {
const frame = window.getFrameGeometryCopy(); const frame = window.getFrameGeometryCopy();
if (edgeResize) { if (edgeResize) {
this.cursorPos = new MockQmlPoint( this.cursorPos = new MockQmlPoint(
leftEdge ? frame.left : frame.right, leftEdge ? frame.x : rectRight(frame),
topEdge ? frame.top : frame.bottom, topEdge ? frame.y : rectBottom(frame),
); );
} else { } else {
this.cursorPos = new MockQmlPoint( this.cursorPos = new MockQmlPoint(
@@ -114,7 +114,7 @@ class MockWorkspace {
} }
} }
runOneOf( runOneOf(
() => window.frameGeometry.set(frame), () => window.getActualFrameGeometry().set(frame),
() => window.frameGeometry = frame, () => window.frameGeometry = frame,
); );
} }
@@ -123,6 +123,15 @@ class MockWorkspace {
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;
} }

View File

@@ -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)[]) {
@@ -28,6 +28,14 @@ function runReorderDebug(order: number[], ...fs: (() => void)[]) {
} }
} }
function randomJitter() {
if (Math.random() < 0.25) {
return (Math.random() - 0.5) * 0.5;
} else {
return 0;
}
}
function randomInt(n: number) { function randomInt(n: number) {
return Math.floor(Math.random() * n); return Math.floor(Math.random() * n);
} }