4 Commits

Author SHA1 Message Date
Peter Fajdiga
aec5750dc0 add TODOs 2024-07-08 20:52:13 +02:00
Peter Fajdiga
6784259c12 Desktop: implement per-screen desktops 2024-07-08 20:25:09 +02:00
Peter Fajdiga
23186bbe91 DesktopManager: remove desktops for removed screens 2024-07-08 20:25:09 +02:00
Peter Fajdiga
d7df3901e2 DesktopManager: add per-screen support 2024-07-08 20:25:09 +02:00
116 changed files with 1338 additions and 6996 deletions

View File

@@ -1,19 +0,0 @@
---
name: Compatibility issue
about: Report an issue with a specific application or window
title: "[Compatibility]"
labels: ''
assignees: ''
---
Karousel version:
Plasma version:
X11 / Wayland:
Window class:
Window caption (title):
Window type:
(Get this info [here](https://github.com/peterfajdiga/karousel/wiki/Getting-window-info))
Description:

View File

@@ -1,14 +0,0 @@
---
name: Generic bug report
about: Report a bug
title: "[Bug]"
labels: ''
assignees: ''
---
Karousel version:
Plasma version:
X11 / Wayland:
Description:

View File

@@ -1,22 +0,0 @@
---
name: Feature request
about: Request a feature
title: "[Feature]"
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
These sections are just guidelines, feel free to remove them.

2
.gitignore vendored
View File

@@ -2,6 +2,4 @@
/package/contents/config/main.xml
/karousel*.tar.gz
run-ts-tmp.js
/node_modules
/.idea

View File

@@ -1,43 +1,29 @@
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
CHECKS := true
.PHONY: *
build: lint test
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
build: tests
tsc -p ./src/main --outFile ./package/contents/code/main.js
mkdir -p ./package/contents/config
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
npm-install:
npm install
lint: npm-install
ifeq (${CHECKS}, true)
npx eslint ./src
endif
lint-fix: npm-install
npx eslint ./src --fix
test:
ifeq (${CHECKS}, true)
tests:
./run-ts.sh ./src/tests
endif
install: build
kpackagetool6 --type=KWin/Script --install=./package || kpackagetool6 --type=KWin/Script --upgrade=./package
kpackagetool6 --type=KWin/Script -i ./package || kpackagetool6 --type=KWin/Script -u ./package
uninstall:
kpackagetool6 --type=KWin/Script --remove=karousel
kpackagetool6 --type=KWin/Script -r karousel
package: build
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package --transform s/package/karousel/
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package
docs-key-bindings-bbcode:
@./run-ts.sh ./src/generators/docs/keyBindingsBbcode
docs-key-bindings-markdown:
@./run-ts.sh ./src/generators/docs/keyBindingsMarkdown
docs-key-bindings-table:
@./run-ts.sh ./src/generators/docs/keyBindingsTable
docs-key-bindings-fmt:
@./run-ts.sh ./src/generators/docs/keyBindingsFmt

View File

@@ -1,11 +1,17 @@
# Karousel
Scrollable tiling Kwin script. Works especially well with ultrawide screens.
KWin tiling script with scrolling. Works especially well with ultrawide screens.
Use with [this](https://github.com/peterfajdiga/kwin4_effect_geometry_change) for animations.
https://github.com/peterfajdiga/karousel/assets/22796326/2ab62d18-09c7-45f9-8fda-e5e36b8d7a02
A scrollable tiling window manager tiles windows, but it does not maximize their widths. Instead, it leaves the width of windows to the user's control.
Windows are automatically centered when possible. And when running out of width, windows can be scrolled through horizontally.
Karousel works differently from most tiling window managers in that it does not maximize the width
of windows, as this can be undesirable with wider screens, where it results in excessively wide
windows that require large return sweeps when reading their content.
Instead, it leaves the width of windows to the user's control. This additionally prevents
unprompted reflow of window content.
Windows are automatically centered when possible. And when running out of width, windows can be
scrolled through horizontally.
Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
[Niri](https://github.com/YaLTeR/niri), and
@@ -22,13 +28,6 @@ Karousel requires the following QML modules:
- Doesn't support windows on all desktops
- Doesn't support windows on multiple activities
## Installation
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
Or clone the repo and run `make install` (requires npm, node, and tsc).
## Key bindings
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
Here's the default ones:
@@ -39,30 +38,22 @@ Here's the default ones:
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| (unassigned) | Move focus to the next window in grid |
| (unassigned) | Move focus to the previous window in grid |
| Meta+Home | Move focus to start |
| Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
| Meta+Shift+D | Move window right (Moves window out of and into columns) |
| Meta+Shift+W | Move window up |
| Meta+Shift+S | Move window down |
| (unassigned) | Move window to the next position in grid |
| (unassigned) | Move window to the previous position in grid |
| Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column (Only the active window visible) |
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start |
| Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width |
| Meta+R | Cycle through preset column widths |
| Meta+Shift+R | Cycle through preset column widths in reverse |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Ctrl+D | Squeeze right column onto the screen |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right |
@@ -70,7 +61,6 @@ Here's the default ones:
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end |
| Meta+Ctrl+Return | Move Karousel grid to the current screen |
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |

View File

@@ -1,15 +0,0 @@
// @ts-check
import tseslint from "typescript-eslint";
export default tseslint.config(
{
extends: [tseslint.configs.stylistic],
rules: {
"@typescript-eslint/no-empty-function": "off",
"semi": "error",
"comma-dangle": ["error", "always-multiline"],
"indent": ["error", 4],
},
}
);

1466
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +0,0 @@
{
"devDependencies": {
"eslint": "^9.24.0",
"typescript-eslint": "^8.30.1"
}
}

View File

@@ -29,23 +29,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_cursorFollowsFocus">
<property name="text">
<string>Cursor follows focus (experimental)</string>
</property>
<property name="toolTip">
<string>When a window gains focus, move the cursor to it</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (only the active window visible)</string>
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
</property>
</widget>
</item>
@@ -111,35 +101,6 @@
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
<string>Touchpad scrolling (Wayland only)</string>
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QCheckBox" name="kcfg_gestureScroll">
<property name="text">
<string>Enable scrolling with touchpad gestures
(please don't forget to disable KDE's workspace switching gestures)</string>
</property>
<property name="toolTip">
<string>Scroll with a three-finger horizontal swipe gesture</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="kcfg_gestureScrollInvert">
<property name="text">
<string>Invert scroll direction (Natural scrolling)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox">
<property name="title">
@@ -149,14 +110,14 @@
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Set "Keep Below" for tiled windows</string>
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Set "Keep Above" for floating windows</string>
<string>Keep floating windows above</string>
</property>
</widget>
</item>
@@ -319,55 +280,13 @@
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_stackOffsetX">
<property name="text">
<string>Horizontal offset for stacked columns:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_stackOffsetX">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_stackOffsetY">
<property name="text">
<string>Vertical offset for stacked columns:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="kcfg_stackOffsetY">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_manualScrollStep">
<property name="text">
<string>Manual scroll step size:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep">
<property name="suffix">
<string> px</string>
@@ -381,62 +300,35 @@
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_gestureScrollStep">
<item row="7" column="0">
<widget class="QLabel" name="label_manualResizeStep">
<property name="text">
<string>Touchpad gesture scrolling speed:</string>
</property>
<property name="toolTip">
<string>The amount to scroll per edge-to-edge gesture</string>
<string>Manual resize step size:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QSpinBox" name="kcfg_gestureScrollStep">
<item row="7" column="1">
<widget class="QSpinBox" name="kcfg_manualResizeStep">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>10000</number>
</property>
<property name="minimum">
<number>100</number>
</property>
<property name="singleStep">
<number>100</number>
<number>999</number>
</property>
<property name="value">
<number>1920</number>
<number>0</number>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_presetWidths">
<property name="text">
<string>Preset widths:</string>
</property>
<property name="toolTip">
<string>Widths used for cycling through widths</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="kcfg_presetWidths">
<property name="toolTip">
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
</property>
</widget>
</item>
<item row="11" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_offScreenOpacity">
<property name="text">
<string>Obscured window opacity:</string>
</property>
</widget>
</item>
<item row="11" column="1">
<item row="8" column="1">
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
<property name="suffix">
<string> %</string>
@@ -449,7 +341,6 @@
</property>
</widget>
</item>
</layout>
</widget>
@@ -458,27 +349,6 @@
<string>Window Rules</string>
</attribute>
<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>
<widget class="QPlainTextEdit" name="kcfg_windowRules">
<property name="tabChangesFocus">

View File

@@ -16,16 +16,6 @@ Item {
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 {
id: notificationInvalidWindowRules
componentName: "plasma_workspace"
@@ -35,39 +25,4 @@ Item {
flags: Notification.Persistent
urgency: Notification.HighUrgency
}
Notification {
id: notificationInvalidPresetWidths
componentName: "plasma_workspace"
eventId: "notification"
title: "Karousel"
text: "Your preset widths are malformed, please review your Karousel configuration"
flags: Notification.Persistent
urgency: Notification.HighUrgency
}
SwipeGestureHandler {
direction: SwipeGestureHandler.Direction.Left
fingerCount: 3
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
onProgressChanged: qmlBase.karouselInstance.gestureScroll(-progress)
}
SwipeGestureHandler {
direction: SwipeGestureHandler.Direction.Right
fingerCount: 3
onActivated: qmlBase.karouselInstance.gestureScrollFinish()
onCancelled: qmlBase.karouselInstance.gestureScrollFinish()
onProgressChanged: qmlBase.karouselInstance.gestureScroll(progress)
}
DBusCall {
id: moveCursorToFocus
service: "org.kde.kglobalaccel"
path: "/component/kwin"
method: "invokeShortcut"
arguments: ["MoveMouseToFocus"]
}
}

View File

@@ -9,7 +9,7 @@
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"Version": "0.16",
"Version": "0.9.3",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"

View File

@@ -1,8 +1,2 @@
#!/bin/bash
set -e
set -o pipefail
JS_FILE='./run-ts-tmp.js'
tsc -p "$1" --outFile "$JS_FILE"
node "$JS_FILE"
tsc -p "$1" --outFile ./run-ts-tmp.js && node ./run-ts-tmp.js && rm ./run-ts-tmp.js

View File

@@ -1,8 +0,0 @@
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidTiledDesktops: Notification;
declare const notificationInvalidWindowRules: Notification;
declare const notificationInvalidPresetWidths: Notification;
declare const moveCursorToFocus: DBusCall;

View File

@@ -1,8 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": [
"../../extern/**/*",
"../../lib/**/*",
"./**/*"
]
"include": ["../../lib/**/*", "./**/*"]
}

View File

@@ -1,11 +1,5 @@
interface DocsKeyBinding {
description: string;
keySequence: string;
}
function formatDescription(item: {description: string, comment?: string}) {
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
return `${applyMacro(item.description, "N")}${suffix}`;
function formatComment(comment: string | undefined) {
return comment === undefined ? "" : ` (${comment})`;
}
function printCols(...columns: (string[] | string)[]) {
@@ -15,9 +9,9 @@ function printCols(...columns: (string[] | string)[]) {
}
let nRows = Math.min(...columns.filter(
(column: string[] | string) => column instanceof Array,
(column: string[] | string) => column instanceof Array
).map(
(column: string[] | string) => column.length,
(column: string[] | string) => column.length
));
if (nRows === Infinity) {
// we only have single string columns
@@ -28,12 +22,12 @@ function printCols(...columns: (string[] | string)[]) {
(column: string[] | string) => {
if (column instanceof Array) {
return Math.max(...column.map(
(cell: string) => cell.length,
));
(cell: string) => cell.length
))
} else {
return column.length;
}
},
}
);
function getCell(col: number, row: number) {
@@ -54,15 +48,3 @@ function printCols(...columns: (string[] | string)[]) {
console.log(line);
}
}
const empty: any = {};
const keyBindings: DocsKeyBinding[] = Array.prototype.concat(
getKeyBindings(empty, empty).map(binding => ({
description: formatDescription(binding),
keySequence: binding.defaultKeySequence || "(unassigned)",
})),
getNumKeyBindings(empty, empty).map(binding => ({
description: formatDescription(binding),
keySequence: `${binding.defaultModifiers}+${binding.fKeys ? "F" : ""}[N]`,
})),
);

View File

@@ -1,7 +1,12 @@
console.log(`[list]`);
for (const binding of keyBindings) {
console.log(` [*] ${binding.keySequence}${binding.description}`);
console.log(` [*] ${binding.defaultKeySequence}${binding.description}${formatComment(binding.comment)}`);
}
for (const binding of numKeyBindings) {
const numPrefix = binding.fKeys ? "F" : "";
console.log(` [*] ${binding.defaultModifiers}+${numPrefix}[N] — ${binding.description}N${formatComment(binding.comment)}`);
}
console.log(`[/list]`);

View File

@@ -1,7 +1,6 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../extern/**/*",
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"

View File

@@ -1,9 +1,14 @@
const colLeft = [
...keyBindings.map(binding => binding.keySequence),
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
...numKeyBindings.map((binding: NumKeyBinding) => {
const numPrefix = binding.fKeys ? "F" : "";
return `${binding.defaultModifiers}+${numPrefix}[N]`;
}),
];
const colRight = [
...keyBindings.map(binding => binding.description),
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
];
printCols(colLeft, " ", colRight);

View File

@@ -1,7 +1,6 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../extern/**/*",
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"

View File

@@ -1,13 +0,0 @@
const colLeft = [
"Shortcut",
"---",
...keyBindings.map(binding => binding.keySequence),
];
const colRight = [
"Action",
"---",
...keyBindings.map(binding => binding.description),
];
printCols("| ", colLeft, " | ", colRight, " |");

View File

@@ -0,0 +1,18 @@
const colLeft = [
"Shortcut",
"---",
...keyBindings.map((binding: KeyBinding) => binding.defaultKeySequence),
...numKeyBindings.map((binding: NumKeyBinding) => {
const numPrefix = binding.fKeys ? "F" : "";
return `${binding.defaultModifiers}+${numPrefix}[N]`;
}),
];
const colRight = [
"Action",
"---",
...keyBindings.map((binding: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
];
printCols("| ", colLeft, " | ", colRight, " |");

View File

@@ -1,7 +1,6 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../extern/**/*",
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"

346
src/lib/Actions.ts Normal file
View File

@@ -0,0 +1,346 @@
namespace Actions {
export function getAction(world: World, config: Config, name: string) {
switch (name) {
case "focus-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
};
case "focus-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
};
case "focus-up": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
};
case "focus-down": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
};
case "focus-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
};
case "focus-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
};
case "window-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
window.moveToColumn(prevColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
});
};
case "window-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
window.moveToColumn(nextColumn);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
});
};
case "window-move-up": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
};
case "window-move-down": return () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
};
case "window-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
};
case "window-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
};
case "window-toggle-floating": return () => {
const kwinClient = Workspace.activeWindow;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
};
case "column-move-left": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
};
case "column-move-right": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
};
case "column-move-start": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumn(column, null);
});
};
case "column-move-end": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumn(column, grid.getLastColumn());
});
};
case "column-toggle-stacked": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked();
});
};
case "column-width-increase": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.increaseWidth(column, config.manualResizeStep);
});
};
case "column-width-decrease": return () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.decreaseWidth(column, config.manualResizeStep);
});
};
case "columns-width-equalize": return () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
};
case "grid-scroll-left": return () => {
gridScroll(world, -config.manualScrollStep);
};
case "grid-scroll-right": return () => {
gridScroll(world, config.manualScrollStep);
};
case "grid-scroll-start": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
});
};
case "grid-scroll-end": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
});
};
case "grid-scroll-focused": return () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
};
case "grid-scroll-left-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.desktop.scrollToColumn(prevColumn);
});
};
case "grid-scroll-right-column": return () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.desktop.scrollToColumn(nextColumn);
});
};
default: throw new Error("unknown action: " + name);
}
}
export function getNumAction(world: World, name: string) {
switch (name) {
case "focus-": return (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.focus();
});
};
case "window-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
window.moveToColumn(targetColumn);
grid.desktop.autoAdjustScroll();
});
};
case "column-move-to-column-": return (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return;
}
if (targetColumn.isToTheRightOf(column)) {
grid.moveColumn(column, targetColumn);
} else {
grid.moveColumn(column, grid.getPrevColumn(targetColumn));
}
});
};
case "column-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
};
case "tail-move-to-desktop-": return (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newGrid = desktopManager.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
};
default: throw new Error("unknown num action: " + name);
}
}
function gridScroll(world: World, amount: number) {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
grid.desktop.adjustScroll(amount, false);
});
}
export type Config = {
manualScrollStep: number,
manualResizeStep: number,
columnResizer: ColumnResizer,
};
export type ColumnResizer = {
increaseWidth(column: Column, step: number): void,
decreaseWidth(column: Column, step: number): void,
}
}

View File

@@ -1,62 +0,0 @@
class PresetWidths {
private readonly presets: ((maxWidth: number) => number)[];
constructor(presetWidths: string, spacing: number) {
this.presets = PresetWidths.parsePresetWidths(presetWidths, spacing);
}
public next(currentWidth: number, minWidth: number, maxWidth: number) {
const widths = this.getWidths(minWidth, maxWidth);
const nextIndex = widths.findIndex(width => width > currentWidth);
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
}
public prev(currentWidth: number, minWidth: number, maxWidth: number) {
const widths = this.getWidths(minWidth, maxWidth).reverse();
const nextIndex = widths.findIndex(width => width < currentWidth);
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
}
public getWidths(minWidth: number, maxWidth: number) {
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
widths.sort((a, b) => a - b);
return uniq(widths);
}
private static parsePresetWidths(presetWidths: string, spacing: number): ((maxWidth: number) => number)[] {
function getRatioFunction(ratio: number) {
return (maxWidth: number) => Math.floor((maxWidth + spacing) * ratio - spacing);
}
return presetWidths.split(",").map((widthStr: string) => {
widthStr = widthStr.trim();
const widthPx = PresetWidths.parseNumberWithSuffix(widthStr, "px");
if (widthPx !== undefined) {
return () => widthPx;
}
const widthPct = PresetWidths.parseNumberWithSuffix(widthStr, "%");
if (widthPct !== undefined) {
return getRatioFunction(widthPct / 100.0);
}
return getRatioFunction(PresetWidths.parseNumberSafe(widthStr));
});
}
private static parseNumberSafe(str: string) {
const num = Number(str);
if (isNaN(num) || num <= 0) {
throw new Error("Invalid number: " + str);
}
return num;
}
private static parseNumberWithSuffix(str: string, suffix: string) {
if (!str.endsWith(suffix)) {
return undefined;
}
return PresetWidths.parseNumberSafe(str.substring(0, str.length-suffix.length).trim());
}
}

View File

@@ -1,20 +1,14 @@
class ContextualResizer {
constructor(
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
) {}
public increaseWidth(column: Column) {
public increaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
if(!column.isVisible(visibleRange, true) || column.getWidth() >= column.getMaxWidth()) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
let leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
let rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
@@ -23,15 +17,16 @@ class ContextualResizer {
const leftSpace = leftVisibleColumn.getLeft() - visibleRange.getLeft();
const rightSpace = visibleRange.getRight() - rightVisibleColumn.getRight();
const newWidth = findMinPositive(
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() + step,
column.getWidth() + leftSpace + rightSpace,
column.getWidth() + leftSpace + rightSpace + leftVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
column.getWidth() + leftSpace + rightSpace + rightVisibleColumn.getWidth() + grid.config.gapsInnerHorizontal,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => width - column.getWidth(),
);
)
if (newWidth === undefined) {
return;
}
@@ -40,28 +35,26 @@ class ContextualResizer {
desktop.scrollCenterVisible(column);
}
public decreaseWidth(column: Column) {
public decreaseWidth(column: Column, step: number) {
const grid = column.grid;
const desktop = grid.desktop;
const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth();
if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
if(!column.isVisible(visibleRange, true) || column.getWidth() <= column.getMinWidth()) {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
}
let leftOffScreenColumn = grid.getLeftColumn(leftVisibleColumn);
let leftOffScreenColumn = grid.getPrevColumn(leftVisibleColumn);
if (leftOffScreenColumn === column) {
leftOffScreenColumn = null;
}
let rightOffScreenColumn = grid.getRightColumn(rightVisibleColumn);
let rightOffScreenColumn = grid.getNextColumn(rightVisibleColumn);
if (rightOffScreenColumn === column) {
rightOffScreenColumn = null;
}
@@ -71,14 +64,15 @@ class ContextualResizer {
const leftOffScreen = leftOffScreenColumn === null ? 0 : leftOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const rightOffScreen = rightOffScreenColumn === null ? 0 : rightOffScreenColumn.getWidth() + grid.config.gapsInnerHorizontal - unusedWidth;
const newWidth = findMinPositive(
const newWidth = ContextualResizer.findNextStep(
[
visibleRange.getWidth(),
column.getWidth() - step,
column.getWidth() - leftOffScreen,
column.getWidth() - rightOffScreen,
...this.presetWidths.getWidths(minWidth, maxWidth),
],
width => column.getWidth() - width,
);
)
if (newWidth === undefined) {
return;
}
@@ -87,21 +81,16 @@ class ContextualResizer {
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);
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
let bestScore = Infinity;
let bestStep = undefined;
for (const step of steps) {
const score = evaluate(step);
if (score > 0 && score < bestScore) {
bestScore = score;
bestStep = step;
}
}
return bestStep;
}
}

View File

@@ -1,43 +1,9 @@
class RawResizer {
constructor(
private readonly presetWidths: { getWidths: (minWidth: number, maxWidth: number) => number[] },
) {}
public increaseWidth(column: Column) {
const newWidth = findMinPositive(
[
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
],
width => width - column.getWidth(),
);
if (newWidth === undefined) {
return;
}
column.setWidth(newWidth, true);
public increaseWidth(column: Column, step: number) {
column.adjustWidth(step, true);
}
public decreaseWidth(column: Column) {
const newWidth = findMinPositive(
[
...this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth()),
],
width => column.getWidth() - width,
);
if (newWidth === undefined) {
return;
}
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);
public decreaseWidth(column: Column, step: number) {
column.adjustWidth(-step, true);
}
}

View File

@@ -6,8 +6,8 @@ class CenterClamper {
}
const lastColumn = desktop.grid.getLastColumn()!;
const minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
const maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
let minScroll = Math.round((firstColumn.getWidth() - desktop.tilingArea.width) / 2);
let maxScroll = Math.round(desktop.grid.getWidth() - (desktop.tilingArea.width + lastColumn.getWidth()) / 2);
return clamp(x, minScroll, maxScroll);
}
}

View File

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

View File

@@ -1,29 +1,22 @@
interface Config {
gapsOuterTop: number;
gapsOuterBottom: number;
gapsOuterLeft: number;
gapsOuterRight: number;
gapsInnerHorizontal: number;
gapsInnerVertical: number;
stackOffsetX: number;
stackOffsetY: number;
manualScrollStep: number;
presetWidths: string;
offScreenOpacity: number;
untileOnDrag: boolean;
cursorFollowsFocus: boolean;
stackColumnsByDefault: boolean;
resizeNeighborColumn: boolean;
reMaximize: boolean;
skipSwitcher: boolean;
scrollingLazy: boolean;
scrollingCentered: boolean;
scrollingGrouped: boolean;
gestureScroll: boolean;
gestureScrollInvert: boolean;
gestureScrollStep: number;
tiledKeepBelow: boolean;
floatingKeepAbove: boolean;
windowRules: string;
tiledDesktops: string;
}
type Config = {
gapsOuterTop: number,
gapsOuterBottom: number,
gapsOuterLeft: number,
gapsOuterRight: number,
gapsInnerHorizontal: number,
gapsInnerVertical: number,
manualScrollStep: number,
manualResizeStep: number,
offScreenOpacity: number,
untileOnDrag: boolean,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
scrollingLazy: boolean,
scrollingCentered: boolean,
scrollingGrouped: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string,
};

View File

@@ -1,10 +1,14 @@
const defaultWindowRules = `[
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?polkit-kde-authentication-agent-1",
"class": "xwaylandvideobridge",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false
},
{
@@ -31,28 +35,28 @@ const defaultWindowRules = `[
"class": "(org\\\\.kde\\\\.)?yakuake",
"tile": false
},
{
"class": "wl-copy|wl-paste",
"caption": "wl-clipboard",
"tile": false
},
{
"class": "steam",
"caption": "Steam Big Picture Mode",
"tile": false
},
{
"class": "zoom",
"caption": "Zoom Cloud Meetings|zoom|zoom <2>",
"tile": false
},
{
"class": "jetbrains-.*",
"class": "jetbrains-idea",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-.*",
"class": "jetbrains-studio",
"caption": "splash",
"tile": false
},
{
"class": "jetbrains-idea",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
},
{
"class": "jetbrains-studio",
"caption": "Unstash Changes|Paths Affected by stash@.*",
"tile": true
}
@@ -89,25 +93,15 @@ const configDef = [
type: "UInt",
default: 8,
},
{
name: "stackOffsetX",
type: "UInt",
default: 8,
},
{
name: "stackOffsetY",
type: "UInt",
default: 32,
},
{
name: "manualScrollStep",
type: "UInt",
default: 200,
},
{
name: "presetWidths",
type: "String",
default: "50%, 100%",
name: "manualResizeStep",
type: "UInt",
default: 600,
},
{
name: "offScreenOpacity",
@@ -119,11 +113,6 @@ const configDef = [
type: "Bool",
default: true,
},
{
name: "cursorFollowsFocus",
type: "Bool",
default: false,
},
{
name: "stackColumnsByDefault",
type: "Bool",
@@ -159,21 +148,6 @@ const configDef = [
type: "Bool",
default: false,
},
{
name: "gestureScroll",
type: "Bool",
default: false,
},
{
name: "gestureScrollInvert",
type: "Bool",
default: false,
},
{
name: "gestureScrollStep",
type: "UInt",
default: 1920,
},
{
name: "tiledKeepBelow",
type: "Bool",
@@ -193,10 +167,5 @@ const configDef = [
name: "windowRules",
type: "String",
default: defaultWindowRules,
},
{
name: "tiledDesktops",
type: "String",
default: ".*",
},
}
];

7
src/lib/config/loader.ts Normal file
View File

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

View File

@@ -1,3 +0,0 @@
interface DBusCall extends QmlObject {
call(): void;
}

View File

@@ -1 +1,6 @@
declare const console: Console;
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;

View File

@@ -1,26 +1,23 @@
interface KWin {
__brand: "KWin";
type KWin = {
readConfig(key: string, defaultValue: any): any;
}
interface Workspace {
__brand: "Workspace";
};
type Workspace = {
readonly activities: string[];
readonly desktops: KwinDesktop[];
readonly currentDesktop: KwinDesktop;
readonly currentActivity: string;
readonly activeScreen: Output;
readonly screens: Output[];
readonly windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
activeWindow: KwinClient|null;
activeWindow: KwinClient;
readonly currentDesktopChanged: QSignal<[]>;
readonly currentDesktopChanged: QSignal<[]>
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient|null]>;
readonly windowActivated: QSignal<[KwinClient]>;
readonly screensChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
@@ -28,7 +25,7 @@ interface Workspace {
readonly virtualScreenSizeChanged: QSignal<[]>;
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
}
};
const enum ClientAreaOption {
PlacementArea,
@@ -48,16 +45,18 @@ const enum MaximizedMode {
Maximized,
}
interface Tile { __brand: "Tile" }
interface Output { __brand: "Output" }
type Tile = unknown;
type Output = {
name: string;
};
interface KwinClient {
__brand: "KwinClient";
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient | null;
readonly transientFor: KwinClient;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
@@ -78,17 +77,19 @@ interface KwinClient {
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile|null;
tile: Tile;
opacity: number;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly outputChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
@@ -99,13 +100,10 @@ interface KwinClient {
}
interface KwinDesktop {
__brand: "KwinDesktop";
readonly id: string;
readonly name: string;
}
interface ShortcutHandler extends QmlObject {
type ShortcutHandler = {
readonly activated: QSignal<[]>;
destroy(): void;
}
};

View File

@@ -1,3 +1,3 @@
interface Notification extends QmlObject {
type Notification = {
sendEvent(): void;
}
};

41
src/lib/extern/qt.ts vendored
View File

@@ -1,52 +1,45 @@
interface Console {
__brand: "Console";
type Console = {
log(...args: any[]): void;
trace(): void;
assert(assertion: boolean, message?: string): void;
}
interface Qt {
__brand: "Qt";
type Qt = {
rect(x: number, y: number, width: number, height: number): QmlRect;
createQmlObject(qml: string, parent: QmlObject): QmlObject;
}
interface QmlObject { __brand: "QmlObject" }
interface QmlPoint {
__brand: "QmlPoint";
type QmlObject = unknown;
type QmlPoint = {
x: number;
y: number;
}
interface QmlRect {
__brand: "QmlRect";
type QmlRect = {
x: number;
y: number;
width: number;
height: number;
}
interface QmlSize {
__brand: "QmlSize";
top: number;
bottom: number; // top + height
left: number;
right: number; // left + width
};
type QmlSize = {
width: number;
height: number;
}
interface QSignal<T extends unknown[]> {
__brand: "QSignal";
};
type QSignal<T extends unknown[]> = {
connect(handler: (...args: [...T]) => void): void;
disconnect(handler: (...args: [...T]) => void): void;
}
};
interface QmlTimer extends QmlObject {
type QmlTimer = {
interval: number;
readonly triggered: QSignal<[]>;
restart(): void;
destroy(): void;
}
};

View File

@@ -1,486 +0,0 @@
class Actions {
constructor(
private readonly config: Actions.Config,
) {}
public readonly focusLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
leftColumn.getWindowToFocus().focus();
};
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
rightColumn.getWindowToFocus().focus();
};
public readonly focusUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const aboveWindow = column.getAboveWindow(window);
if (aboveWindow === null) {
return;
}
aboveWindow.focus();
};
public readonly focusDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const belowWindow = column.getBelowWindow(window);
if (belowWindow === null) {
return;
}
belowWindow.focus();
};
public readonly focusNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const belowWindow = column.getBelowWindow(window);
if (belowWindow !== null) {
belowWindow.focus();
} else {
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
rightColumn.getFirstWindow().focus();
}
};
public readonly focusPrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const aboveWindow = column.getAboveWindow(window);
if (aboveWindow !== null) {
aboveWindow.focus();
} else {
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
leftColumn.getLastWindow().focus();
}
};
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.getWindowToFocus().focus();
};
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.getWindowToFocus().focus();
};
public readonly windowMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
window.moveToColumn(leftColumn, true, FocusPassing.Type.None);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getLeftColumn(column));
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
}
};
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom = true) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
window.moveToColumn(rightColumn, bottom, FocusPassing.Type.None);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
}
};
// TODO (optimization): only arrange moved windows
public readonly windowMoveUp = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.moveWindowUp(window);
};
// TODO (optimization): only arrange moved windows
public readonly windowMoveDown = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.moveWindowDown(window);
};
public readonly windowMoveNext = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const canMoveDown = window !== column.getLastWindow();
if (canMoveDown) {
column.moveWindowDown(window);
} else {
this.windowMoveRight(cm, dm, window, column, grid, false);
}
};
public readonly windowMovePrevious = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const canMoveUp = window !== column.getFirstWindow();
if (canMoveUp) {
column.moveWindowUp(window);
} else {
this.windowMoveLeft(cm, dm, window, column, grid);
}
};
public readonly windowMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
};
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn, true, FocusPassing.Type.None);
};
public readonly windowToggleFloating = (cm: ClientManager, dm: DesktopManager) => {
if (Workspace.activeWindow === null) {
return;
}
cm.toggleFloatingClient(Workspace.activeWindow);
};
public readonly columnMoveLeft = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumnLeft(column);
};
public readonly columnMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumnRight(column);
};
public readonly columnMoveStart = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumn(column, null);
};
public readonly columnMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.moveColumn(column, grid.getLastColumn());
};
public readonly columnToggleStacked = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
column.toggleStacked();
};
public readonly columnWidthIncrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.increaseWidth(column);
};
public readonly columnWidthDecrease = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
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) => {
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
};
public readonly cyclePresetWidthsReverse = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const nextWidth = this.config.presetWidths.prev(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
};
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const visibleRange = desktop.getCurrentVisibleRange();
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange));
const availableSpace = desktop.tilingArea.width;
const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1);
const widths = fillSpace(
availableSpace - gapsWidth,
visibleColumns.map(column => ({ min: column.getMinWidth(), max: column.getMaxWidth() })),
);
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Range.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
};
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!Range.contains(visibleRange, focusedColumn)) {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]);
if (targetColumn === null) {
return;
}
const wantedVisibleColumns = [targetColumn, ...currentVisibleColumns];
while (true) {
const success = this.squeezeColumns(wantedVisibleColumns);
if (success) {
break;
}
const removedColumn = wantedVisibleColumns.pop();
if (removedColumn === focusedColumn) {
break; // don't scroll past the currently focused column
}
}
};
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!Range.contains(visibleRange, focusedColumn)) {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]);
if (targetColumn === null) {
return;
}
const wantedVisibleColumns = [...currentVisibleColumns, targetColumn];
while (true) {
const success = this.squeezeColumns(wantedVisibleColumns);
if (success) {
break;
}
const removedColumn = wantedVisibleColumns.shift();
if (removedColumn === focusedColumn) {
break; // don't scroll past the currently focused column
}
}
};
private readonly squeezeColumns = (columns: Column[]) => {
const firstColumn = columns[0];
const lastColumn = columns[columns.length-1];
const grid = firstColumn.grid;
const desktop = grid.desktop;
const availableSpace = desktop.tilingArea.width;
const gapsWidth = grid.config.gapsInnerHorizontal * (columns.length-1);
const columnConstraints = columns.map(column => ({ min: column.getMinWidth(), max: column.getWidth() }));
const minTotalWidth = gapsWidth + columnConstraints.reduce((acc, constraint) => acc + constraint.min, 0);
if (minTotalWidth > availableSpace) {
// there's nothing we can do
return false;
}
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
columns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Range.fromRanges(firstColumn, lastColumn));
return true;
};
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, -this.config.manualScrollStep);
};
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, this.config.manualScrollStep);
};
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
const desktop = desktopManager.getCurrentDesktop();
if (desktop !== undefined) {
desktop.adjustScroll(amount, false);
}
};
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn, false);
};
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn, false);
};
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const scrollAmount = Range.minus(column, grid.desktop.getCurrentVisibleRange());
if (scrollAmount !== 0) {
grid.desktop.adjustScroll(scrollAmount, true);
} else {
grid.desktop.scrollToColumn(column, true);
}
};
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) {
return;
}
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
grid.desktop.scrollToColumn(leftColumn, false);
};
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) {
return;
}
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
grid.desktop.scrollToColumn(rightColumn, false);
};
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
dm.selectScreen(Workspace.activeScreen);
};
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.getWindowToFocus().focus();
};
public readonly windowMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
window.moveToColumn(targetColumn, true, FocusPassing.Type.None);
grid.desktop.autoAdjustScroll();
};
public readonly columnMoveToColumn = (columnIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return;
}
if (targetColumn.isToTheRightOf(column)) {
grid.moveColumn(column, targetColumn);
} else {
grid.moveColumn(column, grid.getLeftColumn(targetColumn));
}
};
public readonly columnMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
if (newDesktop === undefined) {
return;
}
const newGrid = newDesktop.grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
};
public readonly tailMoveToDesktop = (desktopIndex: number, cm: ClientManager, dm: DesktopManager, window: Window, column: Column, oldGrid: Grid) => {
const kwinDesktop = Workspace.desktops[desktopIndex];
if (kwinDesktop === undefined) {
return;
}
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
if (newDesktop === undefined) {
return;
}
const newGrid = newDesktop.grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
};
}
namespace Actions {
export interface Config {
manualScrollStep: number;
presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
};
columnResizer: ColumnResizer;
}
export interface ColumnResizer {
increaseWidth(column: Column): void;
decreaseWidth(column: Column): void;
maximizeWidth(column: Column): void;
minimizeWidth(column: Column): void;
}
}

View File

@@ -1,283 +1,185 @@
function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
return [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
action: () => world.do(actions.windowToggleFloating),
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
action: () => world.doIfTiledFocused(actions.focusLeft),
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
action: () => world.doIfTiledFocused(actions.focusRight),
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
action: () => world.doIfTiledFocused(actions.focusUp),
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
action: () => world.doIfTiledFocused(actions.focusDown),
},
{
name: "focus-next",
description: "Move focus to the next window in grid",
action: () => world.doIfTiledFocused(actions.focusNext),
},
{
name: "focus-previous",
description: "Move focus to the previous window in grid",
action: () => world.doIfTiledFocused(actions.focusPrevious),
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
action: () => world.do(actions.focusStart),
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
action: () => world.do(actions.focusEnd),
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
action: () => world.doIfTiledFocused(actions.windowMoveLeft),
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
action: () => world.doIfTiledFocused(actions.windowMoveRight),
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
action: () => world.doIfTiledFocused(actions.windowMoveUp),
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
action: () => world.doIfTiledFocused(actions.windowMoveDown),
},
{
name: "window-move-next",
description: "Move window to the next position in grid",
action: () => world.doIfTiledFocused(actions.windowMoveNext),
},
{
name: "window-move-previous",
description: "Move window to the previous position in grid",
action: () => world.doIfTiledFocused(actions.windowMovePrevious),
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
action: () => world.doIfTiledFocused(actions.windowMoveStart),
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
action: () => world.doIfTiledFocused(actions.windowMoveEnd),
},
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "Only the active window visible",
defaultKeySequence: "Meta+X",
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
action: () => world.doIfTiledFocused(actions.columnMoveLeft),
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
action: () => world.doIfTiledFocused(actions.columnMoveRight),
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
action: () => world.doIfTiledFocused(actions.columnMoveStart),
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
action: () => world.doIfTiledFocused(actions.columnMoveEnd),
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
action: () => world.doIfTiledFocused(actions.columnWidthIncrease),
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
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",
description: "Cycle through preset column widths",
defaultKeySequence: "Meta+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
},
{
name: "cycle-preset-widths-reverse",
description: "Cycle through preset column widths in reverse",
defaultKeySequence: "Meta+Shift+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidthsReverse),
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
action: () => world.do(actions.columnsWidthEqualize),
},
{
name: "columns-squeeze-left",
description: "Squeeze left column onto the screen",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+Ctrl+A",
action: () => world.doIfTiledFocused(actions.columnsSqueezeLeft),
},
{
name: "columns-squeeze-right",
description: "Squeeze right column onto the screen",
defaultKeySequence: "Meta+Ctrl+D",
action: () => world.doIfTiledFocused(actions.columnsSqueezeRight),
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
action: () => world.doIfTiledFocused(actions.gridScrollFocused),
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
action: () => world.do(actions.gridScrollLeftColumn),
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
action: () => world.do(actions.gridScrollRightColumn),
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
action: () => world.do(actions.gridScrollLeft),
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
action: () => world.do(actions.gridScrollRight),
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
action: () => world.do(actions.gridScrollStart),
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
action: () => world.do(actions.gridScrollEnd),
},
{
name: "screen-switch",
description: "Move Karousel grid to the current screen",
defaultKeySequence: "Meta+Ctrl+Return",
action: () => world.do(actions.screenSwitch),
},
];
}
const keyBindings: KeyBinding[] = [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
},
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "One window in the column visible, others shaded; not supported on Wayland",
defaultKeySequence: "Meta+X",
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
},
{
name: "grid-scroll-focused",
description: "Center focused window",
comment: "Scrolls so that the focused window is centered in the screen",
defaultKeySequence: "Meta+Alt+Return",
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
},
];
function getNumKeyBindings(world: World, actions: Actions): NumKeyBinding[] {
return [
{
name: "focus-{}",
description: "Move focus to column {}",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
action: (i: number) => world.do(actions.focus.partial(i)),
},
{
name: "window-move-to-column-{}",
description: "Move window to column {}",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
defaultModifiers: "Meta+Shift",
fKeys: false,
action: (i: number) => world.doIfTiledFocused(actions.windowMoveToColumn.partial(i)),
},
{
name: "column-move-to-column-{}",
description: "Move column to position {}",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: false,
action: (i: number) => world.doIfTiledFocused(actions.columnMoveToColumn.partial(i)),
},
{
name: "column-move-to-desktop-{}",
description: "Move column to desktop {}",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
action: (i: number) => world.doIfTiledFocused(actions.columnMoveToDesktop.partial(i)),
},
{
name: "tail-move-to-desktop-{}",
description: "Move this and all following columns to desktop {}",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
action: (i: number) => world.doIfTiledFocused(actions.tailMoveToDesktop.partial(i)),
},
];
}
const numKeyBindings: NumKeyBinding[] = [
{
name: "focus-",
description: "Move focus to column ",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
},
{
name: "window-move-to-column-",
description: "Move window to column ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!",
defaultModifiers: "Meta+Shift",
fKeys: false,
},
{
name: "column-move-to-column-",
description: "Move column to position ",
comment: "Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: false,
},
{
name: "column-move-to-desktop-",
description: "Move column to desktop ",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
},
{
name: "tail-move-to-desktop-",
description: "Move this and all following columns to desktop ",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
},
];

View File

@@ -1,19 +1,17 @@
interface KeyBinding {
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence?: string;
action: () => void;
}
defaultKeySequence: string;
};
interface NumKeyBinding {
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: (i: number) => void;
}
};
function catchWrap(f: () => void) {
return () => {
@@ -26,14 +24,14 @@ function catchWrap(f: () => void) {
};
}
function registerKeyBinding(shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
function registerKeyBinding(world: World, config: Actions.Config, shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(keyBinding.action),
catchWrap(Actions.getAction(world, config, keyBinding.name)),
));
}
function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
function registerNumKeyBindings(world: World, shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
@@ -41,27 +39,28 @@ function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding
const keySequence = i < n ?
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
"";
const action = Actions.getNumAction(world, numKeyBinding.name);
shortcutActions.push(new ShortcutAction(
{
name: applyMacro(numKeyBinding.name, numKey),
description: applyMacro(numKeyBinding.description, numKey),
name: numKeyBinding.name + numKey,
description: numKeyBinding.description + numKey,
defaultKeySequence: keySequence,
},
catchWrap(() => numKeyBinding.action(i)),
catchWrap(() => action(i)),
));
}
}
// TODO: refactor
function registerKeyBindings(world: World, config: Actions.Config) {
const actions = new Actions(config);
const shortcutActions: ShortcutAction[] = [];
for (const keyBinding of getKeyBindings(world, actions)) {
registerKeyBinding(shortcutActions, keyBinding);
for (const keyBinding of keyBindings) {
registerKeyBinding(world, config, shortcutActions, keyBinding);
}
for (const numKeyBinding of getNumKeyBindings(world, actions)) {
registerNumKeyBindings(shortcutActions, numKeyBinding);
for (const numKeyBinding of numKeyBindings) {
registerNumKeyBindings(world, shortcutActions, numKeyBinding);
}
return shortcutActions;

View File

@@ -5,25 +5,25 @@ class Column {
private readonly windows: LinkedList<Window>;
private stacked: boolean;
private focusTaker: Window|null;
private static readonly minWidth = 40;
private static readonly minWidth = 10;
constructor(grid: Grid, leftColumn: Column|null) {
constructor(grid: Grid, prevColumn: Column|null) {
this.gridX = 0;
this.width = 0;
this.windows = new LinkedList();
this.stacked = grid.config.stackColumnsByDefault;
this.focusTaker = null;
this.grid = grid;
this.grid.onColumnAdded(this, leftColumn);
this.grid.onColumnAdded(this, prevColumn);
}
public moveToGrid(targetGrid: Grid, leftColumn: Column|null) {
public moveToGrid(targetGrid: Grid, prevColumn: Column|null) {
if (targetGrid === this.grid) {
this.grid.moveColumn(this, leftColumn);
this.grid.moveColumn(this, prevColumn);
} else {
this.grid.onColumnRemoved(this, this.isFocused() ? FocusPassing.Type.Immediate : FocusPassing.Type.None);
this.grid.onColumnRemoved(this, false);
this.grid = targetGrid;
targetGrid.onColumnAdded(this, leftColumn);
targetGrid.onColumnAdded(this, prevColumn);
for (const window of this.windows.iterator()) {
window.client.kwinClient.desktops = [targetGrid.desktop.kwinDesktop];
}
@@ -56,19 +56,11 @@ class Column {
return this.getWindowCount() === 0;
}
public getFirstWindow(): Window {
return this.windows.getFirst()!;
}
public getLastWindow(): Window {
return this.windows.getLast()!;
}
public getAboveWindow(window: Window) {
public getPrevWindow(window: Window) {
return this.windows.getPrev(window);
}
public getBelowWindow(window: Window) {
public getNextWindow(window: Window) {
return this.windows.getNext(window);
}
@@ -79,7 +71,7 @@ class Column {
public getMinWidth() {
let maxMinWidth = Column.minWidth;
for (const window of this.windows.iterator()) {
const minWidth = window.client.kwinClient.minSize.width.ceil();
const minWidth = window.client.kwinClient.minSize.width;
if (minWidth > maxMinWidth) {
maxMinWidth = minWidth;
}
@@ -133,27 +125,6 @@ class Column {
return this.gridX + this.width;
}
public onUserResizeWidth(
startWidth: number,
currentDelta: number,
resizingLeftSide: boolean,
neighbor?: { column: Column, startWidth: number },
) {
const oldColumnWidth = this.getWidth();
this.setWidth(startWidth + currentDelta, true);
const actualDelta = this.getWidth() - startWidth;
let leftEdgeDeltaStep = resizingLeftSide ? oldColumnWidth - this.getWidth() : 0;
if (neighbor !== undefined) {
const oldNeighborWidth = neighbor.column.getWidth();
neighbor.column.setWidth(neighbor.startWidth - actualDelta, true);
if (resizingLeftSide) {
leftEdgeDeltaStep -= neighbor.column.getWidth() - oldNeighborWidth;
}
}
this.grid.desktop.adjustScroll(-leftEdgeDeltaStep, true);
}
public adjustWindowHeight(window: Window, heightDelta: number, top: boolean) {
const otherWindow = top ? this.windows.getPrev(window) : this.windows.getNext(window);
if (otherWindow === null) {
@@ -195,48 +166,58 @@ class Column {
return this.focusTaker;
}
public getWindowToFocus() {
return this.getFocusTaker() ?? this.windows.getFirst()!;
}
public isFocused() {
const lastFocusedWindow = this.grid.getLastFocusedWindow();
if (lastFocusedWindow === null) {
return false;
public focus() {
const window = this.getFocusTaker() ?? this.windows.getFirst();
if (window === null) {
return;
}
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
window.focus();
}
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = Range.contains(visibleRange, this) ? 100 : this.grid.config.offScreenOpacity;
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity;
}
}
if (this.stacked && this.windows.length() >= 2) {
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x);
return;
}
let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
window.client.setShade(false);
window.arrange(x, y, this.width, window.height);
y += window.height + this.grid.config.gapsInnerVertical;
}
}
public arrangeStacked(x: number) {
const nWindows = this.windows.length();
const windowWidth = this.width - (nWindows - 1) * this.grid.config.stackOffsetX;
const windowHeight = this.grid.desktop.tilingArea.height - (nWindows - 1) * this.grid.config.stackOffsetY;
let windowX = x;
let windowY = this.grid.desktop.tilingArea.y;
const expandedWindow = this.getFocusTaker();
let collapsedHeight;
for (const window of this.windows.iterator()) {
window.arrange(windowX, windowY, windowWidth, windowHeight);
windowX += this.grid.config.stackOffsetX;
windowY += this.grid.config.stackOffsetY;
if (window === expandedWindow) {
window.client.setShade(false);
} else {
window.client.setShade(true);
collapsedHeight = window.client.kwinClient.frameGeometry.height;
}
}
const nCollapsed = this.getWindowCount() - 1;
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.arrange(x, y, this.width, expandedHeight);
y += expandedHeight;
} else {
window.arrange(x, y, this.width, window.height);
y += collapsedHeight!;
}
y += this.grid.config.gapsInnerVertical;
}
}
@@ -248,13 +229,27 @@ class Column {
this.grid.desktop.onLayoutChanged();
}
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);
} else {
this.windows.insertStart(window);
private canStack() {
for (const window of this.windows.iterator()) {
if (!window.client.kwinClient.shadeable) {
return false;
}
}
return true;
}
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
} else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
}
}
public onWindowAdded(window: Window) {
this.windows.insertEnd(window);
if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false);
}
@@ -269,9 +264,9 @@ class Column {
this.grid.desktop.onLayoutChanged();
}
public onWindowRemoved(window: Window, passFocus: FocusPassing.Type) {
public onWindowRemoved(window: Window, passFocus: boolean) {
const lastWindow = this.windows.length() === 1;
const windowToFocus = this.getAboveWindow(window) ?? this.getBelowWindow(window);
const windowToFocus = this.getPrevWindow(window) ?? this.getNextWindow(window);
this.windows.remove(window);
@@ -284,15 +279,8 @@ class Column {
this.destroy(passFocus);
} else {
this.resizeWindows();
if (windowToFocus !== null) {
switch (passFocus) {
case FocusPassing.Type.Immediate:
windowToFocus.focus();
break;
case FocusPassing.Type.OnUnfocus:
this.grid.focusPasser.request(windowToFocus.client.kwinClient);
break;
}
if (passFocus && windowToFocus !== null) {
windowToFocus.focus();
}
}
@@ -300,18 +288,18 @@ class Column {
}
public onWindowFocused(window: Window) {
this.grid.onColumnFocused(this, window);
this.grid.onColumnFocused(this);
this.focusTaker = window;
}
public restoreToTiled(focusedWindow: Window) {
public restoreToTiled() {
const lastFocusedWindow = this.getFocusTaker();
if (lastFocusedWindow !== null && lastFocusedWindow !== focusedWindow) {
if (lastFocusedWindow !== null) {
lastFocusedWindow.restoreToTiled();
}
}
private destroy(passFocus: FocusPassing.Type) {
private destroy(passFocus: boolean) {
this.grid.onColumnRemoved(this, passFocus);
}
}

View File

@@ -1,34 +1,33 @@
class Desktop {
public readonly grid: Grid;
public readonly screen: Output;
public readonly kwinDesktop: KwinDesktop;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private gestureScrollXInitial: number | null;
private dirty: boolean;
private dirtyScroll: boolean;
private dirtyPins: boolean;
public clientArea: QmlRect;
public tilingArea: QmlRect;
constructor(
public readonly kwinDesktop: KwinDesktop,
private readonly pinManager: PinManager,
private readonly config: Desktop.Config,
private readonly getScreen: () => Output,
layoutConfig: LayoutConfig,
focusPasser: FocusPassing.Passer,
) {
constructor(screen: Output, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.gestureScrollXInitial = null;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.grid = new Grid(this, layoutConfig, focusPasser);
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
this.screen = screen;
this.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(screen, kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.getScreen(), this.kwinDesktop);
if (rectEquals(newClientArea, this.clientArea) && !this.dirtyPins) {
const newClientArea = Desktop.getClientArea(this.screen, this.kwinDesktop);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
@@ -55,10 +54,10 @@ class Desktop {
top,
right - left,
bottom - top,
);
)
}
public scrollIntoView(range: Range) {
public scrollIntoView(range: Desktop.Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
@@ -75,9 +74,10 @@ class Desktop {
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Range) {
const scrollAmount = Range.minus(range, this.getCurrentVisibleRange());
this.adjustScroll(scrollAmount, true);
public scrollCenterRange(range: Desktop.Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public scrollCenterVisible(focusedColumn: Column) {
@@ -93,17 +93,17 @@ class Desktop {
return;
}
this.scrollToColumn(focusedColumn, false);
this.scrollToColumn(focusedColumn);
}
public scrollToColumn(column: Column, force: boolean) {
if (force || this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) {
this.config.scroller.scrollToColumn(this, column);
}
}
private getVisibleRange(scrollX: number) {
return Range.create(scrollX, this.tilingArea.width);
return new Desktop.RangeImpl(scrollX, this.tilingArea.width);
}
public getCurrentVisibleRange() {
@@ -127,34 +127,38 @@ class Desktop {
this.setScroll(this.scrollX + dx, force);
}
public gestureScroll(amount: number) {
if (!this.config.gestureScroll) {
return;
}
if (this.gestureScrollXInitial === null) {
this.gestureScrollXInitial = this.scrollX;
}
if (this.config.gestureScrollInvert) {
amount = -amount;
}
this.setScroll(this.gestureScrollXInitial + this.config.gestureScrollStep * amount, false);
}
public gestureScrollFinish(focusedWindow: Window|null) {
const scrolledRight = this.scrollX > this.gestureScrollXInitial!;
this.gestureScrollXInitial = null;
public equalizeVisibleColumnsWidths() {
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();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
const minWidths = visibleColumns.map(column => column.getMinWidth()).sort((a, b) => b - a);
for (const minWidth of minWidths) {
if (minWidth > remainingWidth / remainingColumns) {
remainingWidth -= minWidth;
remainingColumns--;
}
}
const avgWidth = remainingWidth / remainingColumns;
for (const column of visibleColumns) {
const minWidth = column.getMinWidth();
if (minWidth > avgWidth) {
column.setWidth(minWidth, true);
} else {
const columnWidth = Math.round(remainingWidth / remainingColumns);
column.setWidth(columnWidth, true);
remainingWidth -= column.getWidth();
remainingColumns--;
}
}
this.scrollCenterRange(Desktop.RangeImpl.fromRanges(
visibleColumns[0],
visibleColumns[visibleColumns.length - 1],
));
}
public arrange() {
@@ -167,10 +171,6 @@ class Desktop {
this.dirty = false;
}
public forceArrange() {
this.dirty = true;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
@@ -188,16 +188,47 @@ class Desktop {
}
namespace Desktop {
export interface Config {
marginTop: number;
marginBottom: number;
marginLeft: number;
marginRight: number;
gestureScroll: boolean;
gestureScrollInvert: boolean;
gestureScrollStep: number;
scroller: Desktop.Scroller;
clamper: Desktop.Clamper;
export type Config = {
marginTop: number,
marginBottom: number,
marginLeft: number,
marginRight: number,
scroller: Desktop.Scroller,
clamper: Desktop.Clamper,
};
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange {
@@ -211,7 +242,7 @@ namespace Desktop {
this.width = initialColumn.getWidth();
}
public addNeighbors(visibleRange: Range, gap: number) {
public addNeighbors(visibleRange: Desktop.Range, gap: number) {
const grid = this.left.grid;
const columnRange = this;
@@ -222,8 +253,8 @@ namespace Desktop {
return column !== null && canFit(column);
}
let leftColumn = grid.getLeftColumn(this.left);
let rightColumn = grid.getRightColumn(this.right);
let leftColumn = grid.getPrevColumn(this.left);
let rightColumn = grid.getNextColumn(this.right);
function checkColumns() {
if (!isUsable(leftColumn)) {
leftColumn = null;
@@ -240,10 +271,10 @@ namespace Desktop {
const rightToCenter = rightColumn === null ? Infinity : Math.abs(rightColumn.getRight() - visibleCenter);
if (leftToCenter < rightToCenter) {
this.addLeft(leftColumn!, gap);
leftColumn = grid.getLeftColumn(leftColumn!);
leftColumn = grid.getPrevColumn(leftColumn!);
} else {
this.addRight(rightColumn!, gap);
rightColumn = grid.getRightColumn(rightColumn!);
rightColumn = grid.getNextColumn(rightColumn!);
}
checkColumns();
}
@@ -272,11 +303,11 @@ namespace Desktop {
}
}
export interface Scroller {
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
}
export interface Clamper {
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
}
}

View File

@@ -1,17 +1,17 @@
import Range = Desktop.Range;
class Grid {
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
public readonly focusPasser: FocusPassing.Passer;
private readonly columns: LinkedList<Column>;
private lastFocusedColumn: Column|null;
private width: number;
private userResize: boolean; // is any part of the grid being resized by the user
private readonly userResizeFinishedDelayer: Delayer;
constructor(desktop: Desktop, config: LayoutConfig, focusPasser: FocusPassing.Passer) {
constructor(desktop: Desktop, config: LayoutConfig) {
this.desktop = desktop;
this.config = config;
this.focusPasser = focusPasser;
this.columns = new LinkedList();
this.lastFocusedColumn = null;
this.width = 0;
@@ -24,13 +24,13 @@ class Grid {
});
}
public moveColumn(column: Column, leftColumn: Column|null) {
if (column === leftColumn) {
public moveColumn(column: Column, prevColumn: Column|null) {
if (column === prevColumn) {
return;
}
const movedLeft = leftColumn === null ? true : column.isToTheRightOf(leftColumn);
const firstMovedColumn = movedLeft ? column : this.getRightColumn(column);
this.columns.move(column, leftColumn);
const movedLeft = prevColumn === null ? true : column.isToTheRightOf(prevColumn);
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
this.columnsSetX(firstMovedColumn);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
@@ -44,11 +44,11 @@ class Grid {
}
public moveColumnRight(column: Column) {
const rightColumn = this.columns.getNext(column);
if (rightColumn === null) {
const nextColumn = this.columns.getNext(column);
if (nextColumn === null) {
return;
}
this.moveColumnLeft(rightColumn);
this.moveColumnLeft(nextColumn);
}
public getWidth() {
@@ -59,11 +59,11 @@ class Grid {
return this.userResize;
}
public getLeftColumn(column: Column) {
public getPrevColumn(column: Column) {
return this.columns.getPrev(column);
}
public getRightColumn(column: Column) {
public getNextColumn(column: Column) {
return this.columns.getNext(column);
}
@@ -106,19 +106,19 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(visibleRange: Range) {
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
if (column.isVisible(visibleRange, fullyVisible)) {
return column;
}
}
return null;
}
public getRightmostVisibleColumn(visibleRange: Range) {
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) {
let last = null;
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
if (column.isVisible(visibleRange, fullyVisible)) {
last = column;
} else if (last !== null) {
break;
@@ -127,14 +127,29 @@ class Grid {
return last;
}
public *getVisibleColumns(visibleRange: Range) {
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
if (column.isVisible(visibleRange, fullyVisible)) {
yield column;
}
}
}
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
let width = 0;
let nVisible = 0;
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
width += column.getWidth();
nVisible++;
}
if (nVisible > 0) {
width += (nVisible-1) * this.config.gapsInnerHorizontal;
}
return width;
}
public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize);
@@ -147,58 +162,52 @@ class Grid {
}
}
public onColumnAdded(column: Column, leftColumn: Column|null) {
if (leftColumn === null) {
public onColumnAdded(column: Column, prevColumn: Column|null) {
if (prevColumn === null) {
this.columns.insertStart(column);
} else {
this.columns.insertAfter(column, leftColumn);
this.columns.insertAfter(column, prevColumn);
}
this.columnsSetX(column);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public onColumnRemoved(column: Column, passFocus: FocusPassing.Type) {
public onColumnRemoved(column: Column, passFocus: boolean) {
const isLastColumn = this.columns.length() === 1;
const rightColumn = this.getRightColumn(column);
const columnToFocus = isLastColumn ? null : this.getLeftColumn(column) ?? rightColumn;
const nextColumn = this.getNextColumn(column);
const columnToFocus = isLastColumn ? null : this.getPrevColumn(column) ?? nextColumn;
if (column === this.lastFocusedColumn) {
this.lastFocusedColumn = columnToFocus;
}
this.columns.remove(column);
this.columnsSetX(rightColumn);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (columnToFocus !== null) {
switch (passFocus) {
case FocusPassing.Type.Immediate:
columnToFocus.getWindowToFocus().focus();
return;
case FocusPassing.Type.OnUnfocus:
this.focusPasser.request(columnToFocus.getWindowToFocus().client.kwinClient);
return;
}
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} else {
this.desktop.autoAdjustScroll();
}
this.desktop.autoAdjustScroll();
}
public onColumnWidthChanged(column: Column) {
const rightColumn = this.columns.getNext(column);
this.columnsSetX(rightColumn);
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) {
this.desktop.autoAdjustScroll();
}
}
public onColumnFocused(column: Column, window: Window) {
public onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null) {
lastFocusedColumn.restoreToTiled(window);
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;
this.desktop.scrollToColumn(column, false);
this.desktop.scrollToColumn(column);
}
public onScreenSizeChanged() {

View File

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

View File

@@ -1,47 +0,0 @@
interface Range {
getLeft(): number;
getRight(): number;
getWidth(): number;
}
namespace Range {
export function create(x: number, width: number) {
return new Basic(x, width);
}
export function fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new Basic(left, right - left);
}
export function contains(parent: Range, child: Range) {
return child.getLeft() >= parent.getLeft() &&
child.getRight() <= parent.getRight();
}
export function minus(a: Range, b: Range) {
const aCenter = a.getLeft() + a.getWidth() / 2;
const bCenter = b.getLeft() + b.getWidth() / 2;
return Math.round(aCenter - bCenter);
}
class Basic {
constructor(
private readonly x: number,
private readonly width: number,
) {}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
}
}

View File

@@ -7,29 +7,23 @@ class Window {
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height.round();
let maximizedMode = this.client.getMaximizedMode();
if (maximizedMode === undefined) {
maximizedMode = MaximizedMode.Unmaximized; // defaulting to unmaximized, as this is set in Tiled.prepareClientForTiling
}
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: this.client.kwinClient.fullScreen,
maximizedMode: maximizedMode,
fullScreen: false,
maximizedMode: MaximizedMode.Unmaximized,
};
this.skipArrange = this.client.kwinClient.fullScreen || maximizedMode !== MaximizedMode.Unmaximized;
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this, true);
column.onWindowAdded(this);
}
public moveToColumn(targetColumn: Column, bottom: boolean, passFocus: FocusPassing.Type) {
public moveToColumn(targetColumn: Column) {
if (targetColumn === this.column) {
return;
}
this.column.onWindowRemoved(this, passFocus);
this.column.onWindowRemoved(this, false);
this.column = targetColumn;
targetColumn.onWindowAdded(this, bottom);
targetColumn.onWindowAdded(this);
}
public arrange(x: number, y: number, width: number, height: number) {
@@ -60,12 +54,11 @@ class Window {
}
public 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);
if (this.client.isShaded()) {
// workaround for KWin deactivating clients when unshading immediately after activation
this.client.setShade(false);
}
this.client.focus();
}
public isFocused() {
@@ -73,14 +66,6 @@ class Window {
}
public onFocused() {
if (this.column.grid.config.reMaximize && (
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
this.focusedState.fullScreen
)) {
// We need to maximize/fullscreen this window, but we can't do it here.
// We need to do it in `arrange` to ensure it happens after placement.
this.column.grid.desktop.forceArrange();
}
this.column.onWindowFocused(this);
}
@@ -90,6 +75,7 @@ class Window {
}
this.client.setFullScreen(false);
this.client.setMaximize(false, false);
this.column.grid.desktop.onLayoutChanged();
}
public onMaximizedChanged(maximizedMode: MaximizedMode) {
@@ -121,20 +107,45 @@ class Window {
this.column.grid.desktop.onLayoutChanged();
}
public onUserResize(oldGeometry: QmlRect, resizeNeighborColumn: boolean) {
const newGeometry = this.client.kwinClient.frameGeometry;
const widthDelta = newGeometry.width - oldGeometry.width;
const heightDelta = newGeometry.height - oldGeometry.height;
if (widthDelta !== 0) {
this.column.adjustWidth(widthDelta, true);
let leftEdgeDelta = newGeometry.left - oldGeometry.left;
const resizingLeftSide = leftEdgeDelta !== 0;
if (resizeNeighborColumn && this.column.grid.config.resizeNeighborColumn) {
const neighborColumn = resizingLeftSide ? this.column.grid.getPrevColumn(this.column) : this.column.grid.getNextColumn(this.column);
if (neighborColumn !== null) {
const oldNeighborWidth = neighborColumn.getWidth();
neighborColumn.adjustWidth(-widthDelta, true);
if (resizingLeftSide) {
leftEdgeDelta -= neighborColumn.getWidth() - oldNeighborWidth;
}
}
}
this.column.grid.desktop.adjustScroll(-leftEdgeDelta, true);
}
if (heightDelta !== 0) {
this.column.adjustWindowHeight(this, heightDelta, newGeometry.y !== oldGeometry.y);
}
}
public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width.round(), true);
this.column.setWidth(newGeometry.width, true);
this.column.grid.desktop.onLayoutChanged();
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.column.onWindowRemoved(this, passFocus);
}
}
namespace Window {
export interface State {
fullScreen: boolean;
maximizedMode: MaximizedMode;
export type State = {
fullScreen: boolean,
maximizedMode: MaximizedMode,
}
}

View File

@@ -1,30 +0,0 @@
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

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

View File

@@ -11,14 +11,14 @@ class WindowRuleEnforcer {
}
public shouldTile(kwinClient: KwinClient) {
return this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!kwinClient.fullScreen &&
!Clients.isFullScreenGeometry(kwinClient) &&
!this.preferFloating.matches(kwinClient)
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!this.preferFloating.matches(kwinClient)
)
);
}
@@ -30,7 +30,7 @@ class WindowRuleEnforcer {
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = Clients.canTileNow(kwinClient) && enforcer.shouldTile(kwinClient);
const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
@@ -52,7 +52,7 @@ class WindowRuleEnforcer {
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
const ruleString = ClientMatcher.getRuleString(
WindowRuleEnforcer.wrapParens(ruleClass),
WindowRuleEnforcer.wrapParens(ruleCaption),
WindowRuleEnforcer.wrapParens(ruleCaption)
);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);

View File

@@ -17,9 +17,9 @@ class Delayer {
}
function initQmlTimer() {
return Qt.createQmlObject(
return <QmlTimer>Qt.createQmlObject(
`import QtQuick 6.0
Timer {}`,
qmlBase,
) as QmlTimer;
qmlBase
);
}

View File

@@ -1,24 +0,0 @@
class RateLimiter {
private i = 0;
private intervalStart = 0;
constructor(
private readonly n: number,
private readonly intervalMs: number,
) {}
public acquire() {
const now = Date.now();
if (now - this.intervalStart >= this.intervalMs) {
this.i = 0;
this.intervalStart = now;
}
if (this.i < this.n) {
this.i++;
return true;
} else {
return false;
}
}
}

View File

@@ -1,7 +1,7 @@
class ShortcutAction {
private readonly shortcutHandler: ShortcutHandler;
constructor(keyBinding: ShortcutAction.KeyBinding, f: () => void) {
constructor(keyBinding: KeyBinding, f: () => void) {
this.shortcutHandler = ShortcutAction.initShortcutHandler(keyBinding);
this.shortcutHandler.activated.connect(f);
}
@@ -10,28 +10,16 @@ class ShortcutAction {
this.shortcutHandler.destroy();
}
private static initShortcutHandler(keyBinding: ShortcutAction.KeyBinding) {
const sequenceLine = keyBinding.defaultKeySequence !== undefined ?
` sequence: "${keyBinding.defaultKeySequence}";
` :
"";
return Qt.createQmlObject(
private static initShortcutHandler(keyBinding: KeyBinding) {
return <ShortcutHandler>Qt.createQmlObject(
`import QtQuick 6.0
import org.kde.kwin 3.0
ShortcutHandler {
name: "karousel-${keyBinding.name}";
text: "Karousel: ${keyBinding.description}";
${sequenceLine}}`,
sequence: "${keyBinding.defaultKeySequence}";
}`,
qmlBase,
) as ShortcutHandler;
}
}
namespace ShortcutAction {
export interface KeyBinding {
name: string;
description: string;
defaultKeySequence?: string;
);
}
}

View File

@@ -1,39 +0,0 @@
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}
function uniq(sortedArray: any[]) {
const filtered = [];
let lastItem;
for (const item of sortedArray) {
if (item !== lastItem) {
filtered.push(item);
lastItem = item;
}
}
return filtered;
}
function mapGetOrInit<K, V>(map: Map<K, V>, key: K, defaultItem: V) {
const item = map.get(key);
if (item !== undefined) {
return item;
} else {
map.set(key, defaultItem);
return defaultItem;
}
}
function findMinPositive<T>(items: T[], evaluate: (item: T) => number) {
let bestScore = Infinity;
let bestItem = undefined;
for (const item of items) {
const score = evaluate(item);
if (score > 0 && score < bestScore) {
bestScore = score;
bestItem = item;
}
}
return bestItem;
}

View File

@@ -1,98 +0,0 @@
function fillSpace(availableSpace: number, items: { min: number, max: number }[]) {
if (items.length === 0) {
return [];
}
const middleSize = findMiddleSize(availableSpace, items);
const sizes = items.map(item => clamp(middleSize, item.min, item.max));
if (middleSize !== Math.floor(availableSpace / items.length)) {
distributeRemainder(availableSpace, middleSize, sizes, items);
}
return sizes;
function findMiddleSize(availableSpace: number, items: { min: number, max: number }[]) {
const ranges = buildRanges(items);
let requiredSpace = items.reduce((acc, item) => acc + item.min, 0);
for (const range of ranges) {
const rangeSize = range.end - range.start;
const maxRequiredSpaceDelta = rangeSize * range.n;
if (requiredSpace + maxRequiredSpaceDelta >= availableSpace) {
const positionInRange = (availableSpace - requiredSpace) / maxRequiredSpaceDelta;
return Math.floor(range.start + rangeSize * positionInRange);
}
requiredSpace += maxRequiredSpaceDelta;
}
return ranges[ranges.length-1].end;
}
function buildRanges(items: { min: number, max: number }[]) {
const fenceposts = extractFenceposts(items);
if (fenceposts.length === 1) {
return [{
start: fenceposts[0].value,
end: fenceposts[0].value,
n: items.length,
}];
}
const ranges: Range[] = [];
let n = 0;
for (let i = 1; i < fenceposts.length; i++) {
const startFencepost = fenceposts[i-1];
const endFencepost = fenceposts[i];
n = n - startFencepost.nMax + startFencepost.nMin;
ranges.push({
start: startFencepost.value,
end: endFencepost.value,
n: n,
});
}
return ranges;
}
function extractFenceposts(items: { min: number, max: number }[]) {
const fenceposts = new Map<number, Fencepost>();
for (const item of items) {
mapGetOrInit(fenceposts, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
mapGetOrInit(fenceposts, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
}
const array = Array.from(fenceposts.values());
array.sort((a, b) => a.value - b.value);
return array;
}
function distributeRemainder(availableSpace: number, middleSize: number, sizes: number[], constraints: { max: number }[]) {
const indexes = Array.from(sizes.keys())
.filter(i => sizes[i] === middleSize);
indexes.sort((a, b) => constraints[a].max - constraints[b].max);
const requiredSpace = sum(...sizes);
let remainder = availableSpace - requiredSpace;
let n = indexes.length;
for (const i of indexes) {
if (remainder <= 0) {
break;
}
const enlargable = constraints[i].max - sizes[i];
if (enlargable > 0) {
const enlarge = Math.min(enlargable, Math.ceil(remainder / n));
sizes[i] += enlarge;
remainder -= enlarge;
}
n--;
}
}
interface Range {
start: number,
end: number,
n: number,
}
interface Fencepost {
value: number,
nMin: number,
nMax: number,
}
}

View File

@@ -1,28 +0,0 @@
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 {
partial<H extends any[], T extends any[], R>(
this: (...args: [...H, ...T]) => R,
...head: H
) : (...tail: T) => R;
}
Function.prototype.partial = function<H extends any[], T extends any[]>(...head: H) {
return (...tail: T) => this(...head, ...tail);
};

View File

@@ -8,50 +8,7 @@ function clamp(value: number, min: number, max: number) {
return value;
}
function sum(...list: number[]) {
return list.reduce((acc, val) => acc + val);
}
function rectEquals(a: QmlRect, b: QmlRect) {
return a.x === b.x &&
a.y === b.y &&
a.width === b.width &&
a.height === b.height;
}
function pointEquals(a: QmlPoint, b: QmlPoint) {
return a.x === b.x &&
a.y === b.y;
}
function rectRight(rect: QmlRect) {
return rect.x + rect.width;
}
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();
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}

View File

@@ -1,3 +0,0 @@
function applyMacro(base: string, value: string) {
return base.replace("{}", String(value));
}

View File

@@ -1,27 +1,29 @@
function initWorkspaceSignalHandlers(world: World, focusPasser: FocusPassing.Passer) {
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
Clients.makeTileable(kwinClient);
}
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient);
clientManager.addClient(kwinClient)
});
});
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, FocusPassing.Type.Immediate);
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
focusPasser.activate();
} else {
focusPasser.clearIfDifferent(kwinClient);
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
return;
}
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(Workspace.currentDesktopChanged, () => {
@@ -32,25 +34,25 @@ function initWorkspaceSignalHandlers(world: World, focusPasser: FocusPassing.Pas
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.screensChanged, () => {
manager.connect(Workspace.screensChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.selectScreen(Workspace.activeScreen);
});
desktopManager.updateScreens();
})
});
manager.connect(Workspace.activitiesChanged, () => {
manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
});
})
});
manager.connect(Workspace.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
});
})
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});

View File

@@ -1,17 +1,15 @@
class ClientManager {
private readonly world: World;
private readonly config: ClientManager.Config;
private readonly desktopManager: DesktopManager;
private readonly pinManager: PinManager;
private readonly clientMap: Map<KwinClient, ClientWrapper>;
private lastFocusedClient: KwinClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(
config: Config,
private readonly world: World,
private readonly desktopManager: DesktopManager,
private readonly pinManager: PinManager,
) {
constructor(config: Config, world: World, desktopManager: DesktopManager, pinManager: PinManager) {
this.world = world;
this.config = config;
this.config = { keepAbove: config.floatingKeepAbove };
this.desktopManager = desktopManager;
this.pinManager = pinManager;
this.clientMap = new Map();
@@ -29,19 +27,13 @@ class ClientManager {
public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
let constructState: (client: ClientWrapper) => ClientState.State;
let desktop: Desktop | undefined;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (
Clients.canTileEver(kwinClient) &&
this.windowRuleEnforcer.shouldTile(kwinClient) &&
(desktop = this.desktopManager.getDesktopForClient(kwinClient)) !== undefined
) {
Clients.makeTileable(kwinClient);
console.assert(Clients.canTileNow(kwinClient));
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient) && desktop !== undefined) {
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop.grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
}
@@ -55,21 +47,18 @@ class ClientManager {
this.clientMap.set(kwinClient, client);
}
public removeClient(kwinClient: KwinClient, passFocus: FocusPassing.Type) {
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
console.assert(this.hasClient(kwinClient));
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (kwinClient !== this.lastFocusedClient) {
passFocus = FocusPassing.Type.None;
}
client.destroy(passFocus);
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
this.clientMap.delete(kwinClient);
}
private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient || kwinClient.transientFor === null) {
if (!kwinClient.transient) {
return null;
}
@@ -87,10 +76,9 @@ class ClientManager {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) {
const passFocus = kwinClient === this.lastFocusedClient ? FocusPassing.Type.Immediate : FocusPassing.Type.None;
client.stateManager.setState(
() => new ClientState.TiledMinimized(this.world, client),
passFocus,
kwinClient === this.lastFocusedClient,
);
}
}
@@ -99,14 +87,14 @@ class ClientManager {
if (client.stateManager.getState() instanceof ClientState.Tiled) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
public floatClient(client: ClientWrapper) {
if (client.stateManager.getState() instanceof ClientState.Floating) {
return;
}
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
public tileKwinClient(kwinClient: KwinClient, grid: Grid) {
@@ -135,7 +123,7 @@ class ClientManager {
kwinClient.tile = null;
return;
}
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
this.pinManager.addClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
@@ -148,7 +136,7 @@ class ClientManager {
return;
}
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
this.pinManager.removeClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
@@ -168,9 +156,9 @@ class ClientManager {
if (desktop === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, desktop.grid), false);
} else if (clientState instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), FocusPassing.Type.None);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
}
@@ -180,27 +168,27 @@ class ClientManager {
public onClientFocused(kwinClient: KwinClient) {
this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient);
const window = this.findTiledWindow(kwinClient, true);
if (window !== null) {
window.onFocused();
}
}
public findTiledWindow(kwinClient: KwinClient) {
public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return null;
}
return this.findTiledWindowOfClient(client);
return this.findTiledWindowOfClient(client, followTransient);
}
private findTiledWindowOfClient(client: ClientWrapper): Window|null {
private findTiledWindowOfClient(client: ClientWrapper, followTransient: boolean): Window|null {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientState.Tiled) {
return clientState.window;
} else if (client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor);
} else if (followTransient && client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor, true);
} else {
return null;
}
@@ -208,7 +196,7 @@ class ClientManager {
private removeAllClients() {
for (const kwinClient of Array.from(this.clientMap.keys())) {
this.removeClient(kwinClient, FocusPassing.Type.None);
this.removeClient(kwinClient, false);
}
}
@@ -218,7 +206,7 @@ class ClientManager {
}
namespace ClientManager {
export interface Config {
floatingKeepAbove: boolean;
export type Config = {
keepAbove: boolean,
}
}

View File

@@ -1,17 +1,20 @@
class ClientWrapper {
public readonly kwinClient: KwinClient;
public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number;
private maximizedMode: MaximizedMode | undefined;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QmlRect | null; // workaround for issue #19
constructor(
public readonly kwinClient: KwinClient,
kwinClient: KwinClient,
constructInitialState: (client: ClientWrapper) => ClientState.State,
public transientFor: ClientWrapper | null,
private readonly rulesSignalManager: SignalManager | null,
transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null,
) {
this.kwinClient = kwinClient;
this.transientFor = transientFor;
@@ -21,7 +24,7 @@ class ClientWrapper {
}
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width.round();
this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
@@ -49,10 +52,10 @@ class ClientWrapper {
if (Clients.isOnOneOfVirtualDesktops(this.kwinClient, kwinDesktops)) {
const frame = this.kwinClient.frameGeometry;
this.kwinClient.frameGeometry = Qt.rect(
frame.x.round() + dx,
frame.y.round() + dy,
frame.width.round(),
frame.height.round(),
frame.x + dx,
frame.y + dy,
frame.width,
frame.height,
);
}
@@ -78,7 +81,6 @@ class ClientWrapper {
public setMaximize(horizontally: boolean, vertically: boolean) {
if (!this.kwinClient.maximizable) {
this.maximizedMode = MaximizedMode.Unmaximized;
return;
}
@@ -109,6 +111,16 @@ class ClientWrapper {
});
}
public setShade(shade: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.shade = shade;
});
}
public isShaded() {
return this.kwinClient.shade;
}
public getMaximizedMode() {
return this.maximizedMode;
}
@@ -142,15 +154,15 @@ class ClientWrapper {
if (!Clients.isOnVirtualDesktop(this.kwinClient, Workspace.currentDesktop)) {
return;
}
const frame = roundQtRect(this.kwinClient.frameGeometry);
if (frame.x < screenSize.x) {
frame.x = screenSize.x;
} else if (rectRight(frame) > rectRight(screenSize)) {
frame.x = rectRight(screenSize) - frame.width;
const frame = this.kwinClient.frameGeometry;
if (frame.left < screenSize.left) {
frame.x = screenSize.left;
} else if (frame.right > screenSize.right) {
frame.x = screenSize.right - frame.width;
}
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) {

View File

@@ -1,21 +1,10 @@
namespace Clients {
const prohibitedClasses = [
"ksmserver-logout-greeter",
"xwaylandvideobridge",
];
export function canTileEver(kwinClient: KwinClient) {
const shapeable = (kwinClient.moveable && kwinClient.resizeable) || kwinClient.fullScreen; // full-screen windows may become shapeable after exiting full-screen mode
return shapeable &&
!kwinClient.popupWindow &&
!prohibitedClasses.includes(kwinClient.resourceClass);
return kwinClient.moveable && kwinClient.resizeable && !kwinClient.popupWindow;
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) &&
!kwinClient.minimized &&
kwinClient.desktops.length === 1 &&
kwinClient.activities.length === 1;
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktops.length === 1 && kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
@@ -32,23 +21,22 @@ namespace Clients {
export function getKwinDesktopApprox(kwinClient: KwinClient) {
switch (kwinClient.desktops.length) {
case 0:
return Workspace.currentDesktop;
case 1:
return kwinClient.desktops[0];
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
case 0:
return Workspace.currentDesktop;
} else {
case 1:
return kwinClient.desktops[0];
}
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
return Workspace.currentDesktop;
} else {
return kwinClient.desktops[0];
}
}
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.clientGeometry.width.round() >= fullScreenArea.width &&
kwinClient.clientGeometry.height.round() >= fullScreenArea.height;
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {

View File

@@ -1,69 +1,76 @@
class DesktopManager {
// TODO: fix issue with removed and re-added screens
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private selectedScreen: Output;
private kwinScreens: Set<Output>;
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(
private readonly pinManager: PinManager,
private readonly config: Desktop.Config,
private readonly layoutConfig: LayoutConfig,
private readonly focusPasser: FocusPassing.Passer,
private readonly desktopFilter: DesktopFilter,
pinManager: PinManager,
config: Desktop.Config,
layoutConfig: LayoutConfig,
currentScreen: Output,
currentActivity: string,
currentDesktop: KwinDesktop
) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktops = new Map();
this.selectedScreen = Workspace.activeScreen;
this.kwinScreens = new Set(Workspace.screens);
this.kwinActivities = new Set(Workspace.activities);
this.kwinDesktops = new Set(Workspace.desktops);
this.addDesktop(currentScreen, currentActivity, currentDesktop);
}
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
if (!this.desktopFilter.shouldWorkOnDesktop(kwinDesktop)) {
return undefined;
}
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
public getDesktop(screen: Output, activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(screen, activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
return desktop;
} else {
return this.addDesktop(activity, kwinDesktop);
return this.addDesktop(screen, activity, kwinDesktop);
}
}
public getCurrentDesktop() {
return this.getDesktop(Workspace.currentActivity, Workspace.currentDesktop);
return this.getDesktop(Workspace.activeScreen, Workspace.currentActivity, Workspace.currentDesktop);
}
public getDesktopInCurrentActivity(kwinDesktop: KwinDesktop) {
return this.getDesktop(Workspace.currentActivity, kwinDesktop);
return this.getDesktop(Workspace.activeScreen, Workspace.currentActivity, kwinDesktop);
}
public getDesktopForClient(kwinClient: KwinClient) {
if (kwinClient.activities.length !== 1 || kwinClient.desktops.length !== 1) {
return undefined;
}
return this.getDesktop(kwinClient.activities[0], kwinClient.desktops[0]);
return this.getDesktop(kwinClient.output, kwinClient.activities[0], kwinClient.desktops[0]);
}
private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = new Desktop(
kwinDesktop,
this.pinManager,
this.config,
() => this.selectedScreen,
this.layoutConfig,
this.focusPasser,
);
private addDesktop(screen: Output, activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(screen, activity, kwinDesktop);
const desktop = new Desktop(screen, kwinDesktop, this.pinManager, this.config, this.layoutConfig);
this.desktops.set(desktopKey, desktop);
return desktop;
}
private static getDesktopKey(activity: string, kwinDesktop: KwinDesktop) {
return activity + "|" + kwinDesktop.id;
private static getDesktopKey(screen: Output, activity: string, kwinDesktop: KwinDesktop) {
return screen.name + "|" + activity + "|" + kwinDesktop.id;
}
public updateScreens() {
const newScreens = new Set(Workspace.screens);
for (const screen of this.kwinScreens) {
if (!newScreens.has(screen)) {
this.removeScreen(screen);
}
}
this.kwinScreens = newScreens;
}
public updateActivities() {
@@ -86,24 +93,32 @@ class DesktopManager {
this.kwinDesktops = newDesktops;
}
public selectScreen(screen: Output) {
this.selectedScreen = screen;
private removeScreen(kwinScreen: Output) {
for (const activity of this.kwinActivities) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(kwinScreen, activity, kwinDesktop);
}
}
}
private removeActivity(activity: string) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(activity, kwinDesktop);
for (const kwinScreen of this.kwinScreens) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(kwinScreen, activity, kwinDesktop);
}
}
}
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(activity, kwinDesktop);
for (const kwinScreen of this.kwinScreens) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(kwinScreen, activity, kwinDesktop);
}
}
}
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
private destroyDesktop(screen: Output, activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(screen, activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
desktop.destroy();
@@ -124,20 +139,23 @@ class DesktopManager {
}
public getDesktopsForClient(kwinClient: KwinClient) {
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
const desktops = this.getDesktops([kwinClient.output], kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
return desktops;
}
// empty array means all
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
public *getDesktops(screens: Output[], activities: string[], kwinDesktops: KwinDesktop[]) {
const matchedScreens = screens.length > 0 ? screens : this.kwinScreens.keys();
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
for (const matchedScreen of matchedScreens) {
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedScreen, matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
}
}
}
}

View File

@@ -1,51 +0,0 @@
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

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

View File

@@ -1,33 +1,19 @@
class World {
public readonly untileOnDrag: boolean;
private readonly desktopManager: DesktopManager;
private readonly clientManager: ClientManager;
public readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly shortcutActions: ShortcutAction[];
private readonly screenResizedDelayer: Delayer;
private readonly cursorFollowsFocus: boolean;
constructor(config: Config) {
const focusPasser = new FocusPassing.Passer();
this.workspaceSignalManager = initWorkspaceSignalHandlers(this, focusPasser);
this.cursorFollowsFocus = config.cursorFollowsFocus;
let presetWidths = {
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
prev: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
getWidths: (minWidth: number, maxWidth: number): number[] => [],
};
try {
presetWidths = new PresetWidths(config.presetWidths, config.gapsInnerHorizontal);
} catch (error: any) {
notificationInvalidPresetWidths.sendEvent();
log("failed to parse presetWidths:", error);
}
this.untileOnDrag = config.untileOnDrag;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
this.shortcutActions = registerKeyBindings(this, {
manualScrollStep: config.manualScrollStep,
presetWidths: presetWidths,
columnResizer: config.scrollingCentered ? new RawResizer(presetWidths) : new ContextualResizer(presetWidths),
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
});
this.screenResizedDelayer = new Delayer(1000, () => {
@@ -43,8 +29,6 @@ class World {
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
stackOffsetX: config.stackOffsetX,
stackOffsetY: config.stackOffsetY,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
@@ -52,7 +36,6 @@ class World {
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
untileOnDrag: config.untileOnDrag,
};
this.desktopManager = new DesktopManager(
@@ -64,13 +47,11 @@ class World {
marginRight: config.gapsOuterRight,
scroller: World.createScroller(config),
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
gestureScroll: config.gestureScroll,
gestureScrollInvert: config.gestureScrollInvert,
gestureScrollStep: config.gestureScrollStep,
},
layoutConfig,
focusPasser,
new DesktopFilter(config.tiledDesktops),
Workspace.activeScreen,
Workspace.currentActivity,
Workspace.currentDesktop,
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
@@ -91,32 +72,15 @@ class World {
}
private addExistingClients() {
for (const kwinClient of Workspace.windows) {
const kwinClients = Workspace.windows;
for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i];
this.clientManager.addClient(kwinClient);
}
}
private update() {
const currentDesktop = this.desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
currentDesktop.arrange();
this.moveCursorToFocus();
}
}
private moveCursorToFocus() {
if (this.cursorFollowsFocus && Workspace.activeWindow !== null) {
// Only move cursor for tiled windows
const tiledWindow = this.clientManager.findTiledWindow(Workspace.activeWindow);
if (tiledWindow === null) {
return;
}
const cursorAlreadyInFocus = rectContainsPoint(roundQtRect(Workspace.activeWindow.frameGeometry), Workspace.cursorPos);
if (cursorAlreadyInFocus) {
return;
}
moveCursorToFocus.call();
}
this.desktopManager.getCurrentDesktop().arrange();
}
public do(f: (clientManager: ClientManager, desktopManager: DesktopManager) => void) {
@@ -126,9 +90,10 @@ class World {
public doIfTiled(
kwinClient: KwinClient,
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
const window = this.clientManager.findTiledWindow(kwinClient);
const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
if (window === null) {
return;
}
@@ -139,32 +104,10 @@ class World {
}
public doIfTiledFocused(
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {
if (Workspace.activeWindow === null) {
return;
}
this.doIfTiled(Workspace.activeWindow, f);
}
public gestureScroll(amount: number) {
this.do((clientManager, desktopManager) => {
const currentDesktop = desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
currentDesktop.gestureScroll(amount);
}
});
}
public gestureScrollFinish() {
this.do((clientManager, desktopManager) => {
const 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);
}
});
this.doIfTiled(Workspace.activeWindow, followTransient, f);
}
public destroy() {

View File

@@ -9,7 +9,7 @@ namespace ClientState {
world.onScreenResized();
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
this.world.onScreenResized();
}

View File

@@ -7,7 +7,7 @@ namespace ClientState {
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
this.client = client;
this.config = config;
if (config.floatingKeepAbove) {
if (config.keepAbove) {
client.kwinClient.keepAbove = true;
}
if (limitHeight && client.kwinClient.tile === null) {
@@ -16,11 +16,13 @@ namespace ClientState {
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.client.kwinClient.keepAbove = false;
}
}
// TODO: move to `Tiled.restoreClientAfterTiling`
private static limitHeight(client: ClientWrapper) {
const placementArea = Workspace.clientArea(
ClientAreaOption.PlacementArea,
@@ -30,10 +32,10 @@ namespace ClientState {
const clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth;
client.place(
clientRect.x.round(),
clientRect.y.round(),
clientRect.x,
clientRect.y,
width,
Math.min(clientRect.height.round(), Math.round(placementArea.height / 2)),
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
);
}
@@ -48,7 +50,7 @@ namespace ClientState {
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {

View File

@@ -6,7 +6,7 @@ namespace ClientState {
this.state = initialState;
}
public setState(constructNewState: () => State, passFocus: FocusPassing.Type) {
public setState(constructNewState: () => State, passFocus: boolean) {
this.state.destroy(passFocus);
this.state = constructNewState();
}
@@ -15,12 +15,12 @@ namespace ClientState {
return this.state;
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.state.destroy(passFocus);
}
}
export interface State {
destroy(passFocus: FocusPassing.Type): void;
}
export type State = {
destroy(passFocus: boolean): void,
};
}

View File

@@ -11,14 +11,17 @@ namespace ClientState {
this.pinManager = pinManager;
this.desktopManager = desktopManager;
this.config = config;
if (config.floatingKeepAbove) {
if (config.keepAbove) {
kwinClient.keepAbove = true;
}
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.kwinClient.keepAbove = true;
}
this.pinManager.removeClient(this.kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
desktop.onPinsChanged();
@@ -50,15 +53,7 @@ namespace ClientState {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
});
});
manager.connect(kwinClient.minimizedChanged, () => {
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
});
})
});
manager.connect(kwinClient.desktopsChanged, () => {
@@ -66,7 +61,7 @@ namespace ClientState {
[] :
union(oldDesktops, kwinClient.desktops);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(kwinClient.activities, changedDesktops)) {
for (const desktop of desktopManager.getDesktops([kwinClient.output], kwinClient.activities, changedDesktops)) {
desktop.onPinsChanged();
}
});
@@ -78,13 +73,21 @@ namespace ClientState {
[] :
union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedActivities, kwinClient.desktops)) {
for (const desktop of desktopManager.getDesktops([kwinClient.output], changedActivities, kwinClient.desktops)) {
desktop.onPinsChanged();
}
});
oldActivities = kwinClient.activities;
});
manager.connect(kwinClient.outputChanged, () => {
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops([kwinClient.output], kwinClient.activities, kwinClient.desktops)) {
desktop.onPinsChanged();
}
});
});
return manager;
}
}

View File

@@ -3,7 +3,6 @@ namespace ClientState {
public readonly window: Window;
private readonly defaultState: Tiled.WindowState;
private readonly signalManager: SignalManager;
private static readonly maxExternalFrameGeometryChangedIntervalMs = 1000;
constructor(world: World, client: ClientWrapper, grid: Grid) {
this.defaultState = { skipSwitcher: client.kwinClient.skipSwitcher };
@@ -13,10 +12,10 @@ namespace ClientState {
const window = new Window(client, column);
this.window = window;
this.signalManager = Tiled.initSignalManager(world, window, grid.config);
this.signalManager = Tiled.initSignalManager(world, window);
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
const window = this.window;
@@ -27,7 +26,7 @@ namespace ClientState {
Tiled.restoreClientAfterTiling(client, grid.config, this.defaultState, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window, config: LayoutConfig) {
private static initSignalManager(world: World, window: Window) {
const client = window.client;
const kwinClient = client.kwinClient;
const manager = new SignalManager();
@@ -37,7 +36,7 @@ namespace ClientState {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple desktops are not supported
clientManager.floatClient(client);
clientManager.floatKwinClient(kwinClient);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
@@ -49,12 +48,12 @@ namespace ClientState {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (desktop === undefined) {
// windows on multiple activities are not supported
clientManager.floatClient(client);
clientManager.floatKwinClient(kwinClient);
return;
}
Tiled.moveWindowToGrid(window, desktop.grid);
});
});
})
manager.connect(kwinClient.minimizedChanged, () => {
console.assert(kwinClient.minimized);
@@ -69,51 +68,33 @@ namespace ClientState {
});
});
let moving = false;
let resizing = false;
let resizeStartWidth = 0;
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
let resizingBorder = false;
manager.connect(kwinClient.interactiveMoveResizeStarted, () => {
if (kwinClient.move) {
if (config.untileOnDrag) {
if (world.untileOnDrag) {
world.do((clientManager, desktopManager) => {
clientManager.floatClient(client);
clientManager.floatKwinClient(kwinClient);
});
} else {
moving = true;
}
return;
}
if (kwinClient.resize) {
resizing = true;
resizeStartWidth = window.column.getWidth();
if (config.resizeNeighborColumn) {
const resizeNeighborColumn = Tiled.getResizeNeighborColumn(window);
if (resizeNeighborColumn !== null) {
resizeNeighbor = {
column: resizeNeighborColumn,
startWidth: resizeNeighborColumn.getWidth(),
};
}
}
resizingBorder = Workspace.cursorPos.x > kwinClient.clientGeometry.right ||
Workspace.cursorPos.x < kwinClient.clientGeometry.left;
window.column.grid.onUserResizeStarted();
}
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (moving) {
moving = false;
world.do(() => window.column.grid.desktop.onLayoutChanged()); // move the dragged window back to its position
}
if (resizing) {
resizing = false;
resizeNeighbor = undefined;
window.column.grid.onUserResizeFinished();
}
});
const externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
@@ -123,12 +104,7 @@ namespace ClientState {
return;
}
const newGeometry = roundQtRect(client.kwinClient.frameGeometry);
if (rectEquals(oldGeometry, newGeometry)) {
// no real changes, nothing to do
return;
}
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
@@ -136,51 +112,23 @@ namespace ClientState {
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
if (dx !== 0 || dy !== 0) {
// TODO: instead of passing dx and dy, remember relative (to the parent) x and y for each
// transient window and use them for `moveTransients` and `ensureTransientsVisible`
client.moveTransients(dx, dy);
}
if (kwinClient.resize) {
world.do(() => {
if (newGeometry.width !== oldGeometry.width) {
window.column.onUserResizeWidth(
resizeStartWidth,
newGeometry.width - resizeStartWidth,
newGeometry.x !== oldGeometry.x,
resizeNeighbor,
);
}
if (newGeometry.height !== oldGeometry.height) {
window.column.adjustWindowHeight(
window,
newGeometry.height - oldGeometry.height,
newGeometry.y !== oldGeometry.y,
);
}
});
world.do(() => window.onUserResize(oldGeometry, resizingBorder));
} else if (
!window.column.grid.isUserResizing() &&
!client.isManipulatingGeometry(newGeometry) &&
client.getMaximizedMode() === MaximizedMode.Unmaximized &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
if (externalFrameGeometryChangedRateLimiter.acquire()) {
world.do(() => window.onFrameGeometryChanged());
}
world.do(() => window.onFrameGeometryChanged());
}
});
manager.connect(kwinClient.fullScreenChanged, () => {
world.do((clientManager, desktopManager) => {
// some clients only turn out to be untileable after exiting full-screen mode
if (!Clients.canTileEver(kwinClient)) {
clientManager.floatClient(client);
return;
}
window.onFullScreenChanged(kwinClient.fullScreen);
});
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
});
manager.connect(kwinClient.tileChanged, () => {
@@ -195,18 +143,6 @@ namespace ClientState {
return manager;
}
private static getResizeNeighborColumn(window: Window) {
const kwinClient = window.client.kwinClient;
const column = window.column;
if (Workspace.cursorPos.x > rectRightRound(kwinClient.clientGeometry)) {
return column.grid.getRightColumn(column);
} else if (Workspace.cursorPos.x < kwinClient.clientGeometry.x.round()) {
return column.grid.getLeftColumn(column);
} else {
return null;
}
}
private static moveWindowToGrid(window: Window, grid: Grid) {
if (grid === window.column.grid) {
// window already on the given grid
@@ -214,26 +150,17 @@ namespace ClientState {
}
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const passFocus = window.isFocused() ? FocusPassing.Type.OnUnfocus : FocusPassing.Type.None;
window.moveToColumn(newColumn, true, passFocus);
window.moveToColumn(newColumn);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
if (config.skipSwitcher) {
client.kwinClient.skipSwitcher = true;
}
if (client.kwinClient.fullScreen) {
if (config.maximizedKeepAbove) {
client.kwinClient.keepAbove = true;
}
} else {
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.kwinClient.keepAbove = false;
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
}
@@ -250,6 +177,7 @@ namespace ClientState {
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);
@@ -259,8 +187,8 @@ namespace ClientState {
}
namespace Tiled {
export interface WindowState {
skipSwitcher: boolean;
export type WindowState = {
skipSwitcher: boolean,
}
}
}

View File

@@ -6,7 +6,7 @@ namespace ClientState {
this.signalManager = TiledMinimized.initSignalManager(world, client);
}
public destroy(passFocus: FocusPassing.Type) {
public destroy(passFocus: boolean) {
this.signalManager.destroy();
}

View File

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

View File

@@ -1,8 +1,4 @@
{
"extends": "../tsconfig.json",
"include": [
"../extern/**/*",
"../lib/**/*",
"./**/*"
]
"include": ["../lib/**/*", "./**/*"]
}

View File

@@ -1,43 +0,0 @@
tests.register("Center focused", 5, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client0, client1, client2] = workspaceMock.createClientsWithWidths(300, 152, 300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client0));
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
Assert.assert(workspaceMock.activeWindow === client2);
Assert.columnsFillTilingArea([client0, client1, client2]);
// center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.getActualFrameGeometry());
// undo center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
// center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.getActualFrameGeometry());
// focus client1 (no scrolling should occur)
qtMock.fireShortcut("karousel-focus-left");
Assert.centered(config, tilingArea, client2, { message: "No scrolling should have occured" });
Assert.fullyVisible(client1.getActualFrameGeometry());
Assert.fullyVisible(client2.getActualFrameGeometry());
// center client1
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
// undo center client1 (no scrolling should occur, because all clients are already visible and centered)
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
});

View File

@@ -1,124 +0,0 @@
tests.register("columns squeeze side", 5, () => {
const baseTestCases = [
{ widths: [500, 500], blocked: [false, false], possible: true },
{ widths: [500, 768], blocked: [false, false], possible: true },
{ widths: [500, 500], blocked: [false, true], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, false], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, true], possible: true },
{ widths: [500, 200, 200], blocked: [true, false, true], possible: true },
{ widths: [500, 500, 500], blocked: [false, true, true], possible: false },
];
const testCasesLeft = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
name: "left " + i,
action: "karousel-columns-squeeze-left",
focus: baseTestCase.widths.length-1,
}));
const testCasesRight = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
widths: baseTestCase.widths.slice().reverse(),
blocked: baseTestCase.blocked.slice().reverse(),
name: "right " + i,
action: "karousel-columns-squeeze-right",
focus: 0,
}));
const testCases = [...testCasesLeft, ...testCasesRight];
for (const testCase of testCases) {
const assertOpt = { message: `Case: ${testCase.name}` };
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const clients = workspaceMock.createClientsWithWidths(...testCase.widths);
workspaceMock.activeWindow = clients[testCase.focus];
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
clients[i].minSize = new MockQmlSize(testCase.widths[i], 100);
}
}
if (testCase.possible) {
qtMock.fireShortcut(testCase.action);
Assert.columnsFillTilingArea(clients, assertOpt);
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
Assert.equal(clients[i].getActualFrameGeometry().width, testCase.widths[i], assertOpt);
}
}
}
const frames = clients.map(client => client.getActualFrameGeometry());
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.getActualFrameGeometry());
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], assertOpt);
}
}
});
tests.register("columns squeeze side (just scroll)", 5, () => {
const baseTestCases = [
{ focus: 0, startVisible: [true, true, false], endVisible: [true, true, false] },
{ focus: 1, startVisible: [false, true, true], endVisible: [true, true, false] },
{ focus: 2, startVisible: [false, true, true], endVisible: [false, true, true] },
];
const testCasesLeft = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
name: "left " + i,
action: "karousel-columns-squeeze-left",
scrollStart: false,
}));
const testCasesRight = baseTestCases.map((baseTestCase, i) => ({
focus: 2 - baseTestCase.focus,
startVisible: baseTestCase.startVisible.slice().reverse(),
endVisible: baseTestCase.endVisible.slice().reverse(),
name: "right " + i,
action: "karousel-columns-squeeze-right",
scrollStart: true,
}));
const testCases = [...testCasesLeft, ...testCasesRight];
for (const testCase of testCases) {
const assertMsg = `Case: ${testCase.name}`;
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
function assertVisible(clients: MockKwinClient[], visible: boolean[]) {
for (let i = 0; i < clients.length; i++) {
if (visible[i]) {
Assert.fullyVisible(clients[i].getActualFrameGeometry(), { message: assertMsg, skip: 1 });
} else {
Assert.notFullyVisible(clients[i].getActualFrameGeometry(), { message: assertMsg, skip: 1 });
}
}
}
const clients = workspaceMock.createClientsWithWidths(300, 300, 300);
for (const client of clients) {
client.minSize = new MockQmlSize(300, 100);
}
if (testCase.scrollStart) {
qtMock.fireShortcut("karousel-grid-scroll-start");
}
workspaceMock.activeWindow = clients[testCase.focus];
assertVisible(clients, testCase.startVisible);
qtMock.fireShortcut(testCase.action);
assertVisible(clients, testCase.endVisible);
const frames = clients.map(client => client.getActualFrameGeometry());
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.getActualFrameGeometry());
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], { message: assertMsg });
}
}
});

View File

@@ -1,82 +0,0 @@
tests.register("Drag tiled window, untile", 10, () => {
const config = getDefaultConfig();
config.cursorFollowsFocus = true;
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2] = workspaceMock.createClients(2);
const initialCursorPos = new MockQmlPoint(380, 20);
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), initialCursorPos), { message: "invalid test setup" });
workspaceMock.cursorPos = initialCursorPos.clone();
runOneOf(
() => { Workspace.activeWindow = client1; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
);
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), 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" });
runOneOf(
() => { Workspace.activeWindow = client2; },
() => { qtMock.fireShortcut("karousel-focus-2"); },
);
Assert.assert(!rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
runOneOf(
() => { Workspace.activeWindow = client1; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
);
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
const lastCursorPos = workspaceMock.cursorPos.clone();
Workspace.activeWindow = null;
Assert.assert(pointEquals(Workspace.cursorPos, lastCursorPos), { message: "Cursor should not have been moved" });
});
tests.register("Cursor follows focus only on matched desktops", 1, () => {
// Test that cursor follow focus only works for windows on matched desktops (tiled windows)
const config = getDefaultConfig();
config.cursorFollowsFocus = true;
config.tiledDesktops = "^Desktop 1$"; // Only work on Desktop 1
const { workspaceMock, world } = init(config);
// Create a client on Desktop 1 (matched desktop) - should be tiled
const client1 = new MockKwinClient();
client1.desktops = [workspaceMock.desktops[0]]; // Desktop 1
workspaceMock.createWindows(client1);
// Create a client on Desktop 2 (non-matched desktop) - should be floating
const client2 = new MockKwinClient();
client2.desktops = [workspaceMock.desktops[1]]; // Desktop 2
workspaceMock.createWindows(client2);
// Set initial cursor position outside both windows
const initialCursorPos = new MockQmlPoint(10, 10);
workspaceMock.cursorPos = initialCursorPos.clone();
// Test 1: Focus client1 on matched desktop (Desktop 1) - cursor should move
workspaceMock.currentDesktop = workspaceMock.desktops[0]; // Switch to Desktop 1
Workspace.activeWindow = client1;
world.do(() => {});
Assert.assert(rectContainsPoint(client1.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

@@ -1,73 +0,0 @@
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

@@ -1,34 +0,0 @@
tests.register("Drag tiled window, untile", 20, () => {
const config = getDefaultConfig();
config.untileOnDrag = true;
const { qtMock, workspaceMock, world } = init(config);
const clientManager = getClientManager(world);
const [client0, client1] = workspaceMock.createClients(2);
Assert.tiledClient(clientManager, client0);
Assert.tiledClient(clientManager, client1);
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
workspaceMock.moveWindow(client0, new MockQmlPoint(10, 10));
Assert.notTiledClient(clientManager, client0);
Assert.tiledClient(clientManager, client1);
Assert.grid(config, tilingArea, 100, [[client1]], true);
});
tests.register("Drag tiled window, keep tiled", 20, () => {
const config = getDefaultConfig();
config.untileOnDrag = false;
const { qtMock, workspaceMock, world } = init(config);
const clientManager = getClientManager(world);
const [client0, client1] = workspaceMock.createClients(2);
Assert.tiledClient(clientManager, client0);
Assert.tiledClient(clientManager, client1);
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
const move = new MockQmlPoint(10, 10);
workspaceMock.moveWindow(client0, move, move, move, move, move, move, move, move, move); // many moves in order to trigger externalFrameGeometryChangedRateLimiter
Assert.tiledClient(clientManager, client0);
Assert.tiledClient(clientManager, client1);
Assert.grid(config, tilingArea, 100, [[client0], [client1]], true);
});

View File

@@ -1,44 +0,0 @@
tests.register("External resize", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
function getClientDesiredFrame(width: number) {
return new MockQmlRect(10, 10, width, 200);
}
function getTiledFrame(width: number) {
return new MockQmlRect(
tilingArea.x + Math.round((tilingArea.width - width) / 2),
tilingArea.y,
width,
tilingArea.height,
);
}
const [client] = workspaceMock.createClientsWithFrames(getClientDesiredFrame(100));
Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(100), { message: "We should tile the window, respecting its desired width" });
function testExternalResizing() {
client.frameGeometry = getClientDesiredFrame(110);
Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(110), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(120);
Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(120), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(130);
Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(130), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(140);
Assert.equalRects(client.getActualFrameGeometry(), getTiledFrame(140), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(200);
Assert.equalRects(client.getActualFrameGeometry(), getClientDesiredFrame(200), { message: "We should give up and let the client have its desired frame" });
}
timeControl(addTime => {
testExternalResizing();
addTime(1000);
// the concession has expired, let's test again
testExternalResizing();
});
});

View File

@@ -1,10 +0,0 @@
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

@@ -1,191 +0,0 @@
tests.register("tiledKeepBelow", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = true;
config.floatingKeepAbove = false;
const { qtMock, workspaceMock, world } = init(config);
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const [client] = workspaceMock.createClients(1);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(client.keepBelow);
Assert.assert(!client.keepAbove);
});
tests.register("floatingKeepAbove", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = false;
config.floatingKeepAbove = true;
const { qtMock, workspaceMock, world } = init(config);
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const [client] = workspaceMock.createClients(1);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null);
});
Assert.assert(!client.keepBelow);
Assert.assert(client.keepAbove);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null);
});
Assert.assert(!client.keepBelow);
Assert.assert(!client.keepAbove);
});
tests.register("No layering", 10, () => {
const config = getDefaultConfig();
config.tiledKeepBelow = false;
config.floatingKeepAbove = false;
// In this mode, Karousel shouldn't change keepBelow or keepAbove.
// Except when tiling a window, keepAbove should still be cleared.
const pinGeometry = new MockQmlRect(0, 0, 200, screen.height);
const testCases = [
{ keepBelow: false, keepAbove: false },
{ keepBelow: false, keepAbove: true },
{ keepBelow: true, keepAbove: false },
{ keepBelow: true, keepAbove: true },
];
for (const testCase of testCases) {
const assertOptions = { message: JSON.stringify(testCase) };
const { qtMock, workspaceMock, world } = init(config);
const [client] = workspaceMock.createClients(1);
client.keepBelow = testCase.keepBelow;
client.keepAbove = testCase.keepAbove;
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
client.unpin();
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.assert(!client.keepAbove, assertOptions);
client.keepAbove = testCase.keepAbove;
client.pin(pinGeometry);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) === null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.equal(client.keepAbove, testCase.keepAbove, assertOptions);
qtMock.fireShortcut("karousel-window-toggle-floating");
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.findTiledWindow(client) !== null, assertOptions);
});
Assert.equal(client.keepBelow, testCase.keepBelow, assertOptions);
Assert.assert(!client.keepAbove, assertOptions);
}
});

View File

@@ -1,98 +0,0 @@
tests.register("Focus and move windows", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2, client3] = workspaceMock.createClients(3);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
Assert.assert(clientManager.hasClient(client3));
});
Assert.assert(workspaceMock.activeWindow === client3);
function testLayout(shortcutName: string, grid: MockKwinClient[][]) {
qtMock.fireShortcut(shortcutName);
Assert.grid(config, tilingArea, 100, grid, true, [], { skip: 1 });
}
function testFocus(shortcutName: string, expectedFocus: KwinClient) {
qtMock.fireShortcut(shortcutName);
Assert.assert(workspaceMock.activeWindow === expectedFocus, {
message: `wrong activeWindow: ${workspaceMock.activeWindow?.pid}`,
skip: 1,
});
};
testLayout("karousel-column-move-right", [ [client1], [client2], [client3] ]);
testLayout("karousel-window-move-left", [ [client1], [client2,client3] ]);
testLayout("karousel-window-move-left", [ [client1], [client3], [client2] ]);
testLayout("karousel-window-move-left", [ [client1,client3], [client2] ]);
testFocus("karousel-focus-right", client2);
testLayout("karousel-window-move-left", [ [client1,client3,client2] ]);
testLayout("karousel-window-move-left", [ [client2], [client1,client3] ]);
testLayout("karousel-window-move-left", [ [client2], [client1,client3] ]);
testFocus("karousel-focus-2", client3);
testFocus("karousel-focus-up", client1);
testLayout("karousel-column-move-left", [ [client1,client3], [client2] ]);
testLayout("karousel-window-move-right", [ [client3], [client1], [client2] ]);
testFocus("karousel-focus-3", client2);
testLayout("karousel-window-move-start", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-to-column-3", [ [client3], [client1,client2] ]);
testLayout("karousel-column-move-left", [ [client1,client2], [client3] ]);
testLayout("karousel-column-move-end", [ [client3], [client1,client2] ]);
testLayout("karousel-column-move-to-column-1", [ [client1,client2], [client3] ]);
testLayout("karousel-column-move-right", [ [client3], [client1,client2] ]);
testLayout("karousel-window-move-previous", [ [client3], [client2,client1] ]);
testLayout("karousel-window-move-previous", [ [client3], [client2], [client1] ]);
testLayout("karousel-window-move-previous", [ [client3,client2], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2,client3], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-previous", [ [client2], [client3], [client1] ]);
testLayout("karousel-window-move-next", [ [client2,client3], [client1] ]);
testLayout("karousel-window-move-next", [ [client3,client2], [client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client2], [client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client2,client1] ]);
testLayout("karousel-window-move-next", [ [client3], [client1,client2] ]);
testLayout("karousel-window-move-next", [ [client3], [client1], [client2] ]);
testLayout("karousel-window-move-next", [ [client3], [client1], [client2] ]);
testLayout("karousel-window-move-left", [ [client3], [client1,client2] ]);
const col1Win1 = client3;
const col2Win1 = client1;
const col2Win2 = client2;
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-down", col2Win2);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-right", col2Win2);
testFocus("karousel-focus-right", col2Win2);
testFocus("karousel-focus-2", col2Win2);
testFocus("karousel-focus-1", col1Win1);
testFocus("karousel-focus-2", col2Win2);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-end", col2Win2);
testFocus("karousel-focus-up", col2Win1);
testFocus("karousel-focus-left", col1Win1);
testFocus("karousel-focus-right", col2Win1);
testFocus("karousel-focus-2", col2Win1);
testFocus("karousel-focus-1", col1Win1);
testFocus("karousel-focus-2", col2Win1);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-end", col2Win1);
testFocus("karousel-focus-down", col2Win2);
testFocus("karousel-focus-start", col1Win1);
testFocus("karousel-focus-next", col2Win1);
testFocus("karousel-focus-next", col2Win2);
testFocus("karousel-focus-next", col2Win2);
testFocus("karousel-focus-previous", col2Win1);
testFocus("karousel-focus-previous", col1Win1);
testFocus("karousel-focus-previous", col1Win1);
});

View File

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

View File

@@ -1,341 +0,0 @@
{
function registerTests(
suffix: string,
getConfig: () => Config,
shouldKeepBelow: (tiled: boolean) => boolean,
shouldKeepAbove: (tiled: boolean) => boolean,
) {
tests.register("Maximization " + suffix, 100, () => {
const config = getConfig();
const { qtMock, workspaceMock, world } = init(config);
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(kwinClient));
});
const columnLeftX = tilingArea.x + tilingArea.width/2 - 300/2;
const columnTopY = tilingArea.y;
const columnHeight = tilingArea.height;
Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
kwinClient.fullScreen = true;
Assert.assert(kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(kwinClient.getActualFrameGeometry(), screen);
kwinClient.fullScreen = false;
Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
kwinClient.setMaximize(true, true);
Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(kwinClient.getActualFrameGeometry(), screen);
kwinClient.setMaximize(true, false);
Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(false));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(false));
Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, 0, 300, screen.height);
kwinClient.setMaximize(false, false);
Assert.assert(!kwinClient.fullScreen);
Assert.equal(kwinClient.keepBelow, shouldKeepBelow(true));
Assert.equal(kwinClient.keepAbove, shouldKeepAbove(true));
Assert.rect(kwinClient.getActualFrameGeometry(), columnLeftX, columnTopY, 300, columnHeight);
});
tests.register("Maximize with transient " + suffix, 100, () => {
const config = getConfig();
const { qtMock, workspaceMock, world } = init(config);
const parent = new MockKwinClient(new MockQmlRect(10, 20, 300, 200));
const child = new MockKwinClient(new MockQmlRect(14, 24, 50, 50), parent);
workspaceMock.createWindows(parent);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(parent));
});
runOneOf(
() => { parent.fullScreen = true; },
() => { parent.setMaximize(true, true); },
);
Assert.equal(parent.keepBelow, shouldKeepBelow(false));
Assert.equal(parent.keepAbove, shouldKeepAbove(false));
Assert.equalRects(parent.getActualFrameGeometry(), screen);
workspaceMock.createWindows(child);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(child));
});
Assert.assert(!child.fullScreen);
Assert.equal(child.keepBelow, shouldKeepBelow(false));
Assert.equal(child.keepAbove, shouldKeepAbove(false));
Assert.rect(child.getActualFrameGeometry(), 14, 24, 50, 50);
Assert.equal(parent.keepBelow, shouldKeepBelow(false));
Assert.equal(parent.keepAbove, shouldKeepAbove(false));
Assert.equalRects(parent.getActualFrameGeometry(), screen);
});
{
function assertWindowed(config: Config, clients: MockKwinClient[]) {
Assert.assert(!clients[0].fullScreen);
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
Assert.assert(!clients[1].fullScreen);
Assert.equal(clients[1].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[1].keepAbove, shouldKeepAbove(true));
Assert.assert(!clients[2].fullScreen);
Assert.equal(clients[2].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[2].keepAbove, shouldKeepAbove(true));
Assert.grid(config, tilingArea, [300, 400], [[clients[0]], [clients[1], clients[2]]], true);
}
function assertFullScreenOrMaximized(clients: MockKwinClient[]) {
Assert.assert(!clients[0].fullScreen);
Assert.equal(clients[0].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[0].keepAbove, shouldKeepAbove(true));
Assert.assert(!clients[1].fullScreen);
Assert.equal(clients[1].keepBelow, shouldKeepBelow(true));
Assert.equal(clients[1].keepAbove, shouldKeepAbove(true));
Assert.equal(clients[2].keepBelow, shouldKeepBelow(false));
Assert.equal(clients[2].keepAbove, shouldKeepAbove(false));
Assert.equalRects(clients[2].getActualFrameGeometry(), screen);
}
tests.register("Re-maximize disabled " + suffix, 100, () => {
const config = getConfig();
config.reMaximize = false;
const { qtMock, workspaceMock, world } = init(config);
const clients = workspaceMock.createClientsWithWidths(300, 400, 400);
qtMock.fireShortcut("karousel-window-move-left");
assertWindowed(config, clients);
runOneOf(
() => { clients[2].fullScreen = true; },
() => { clients[2].setMaximize(true, true); },
);
assertFullScreenOrMaximized(clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[0]; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
() => { qtMock.fireShortcut("karousel-focus-left"); },
() => { qtMock.fireShortcut("karousel-focus-start"); },
);
assertWindowed(config, clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[2]; },
() => { qtMock.fireShortcut("karousel-focus-2"); },
() => { qtMock.fireShortcut("karousel-focus-right"); },
() => { qtMock.fireShortcut("karousel-focus-end"); },
);
assertWindowed(config, clients);
runOneOf(
() => { clients[2].fullScreen = true; },
() => { clients[2].setMaximize(true, true); },
);
assertFullScreenOrMaximized(clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[1]; },
() => { qtMock.fireShortcut("karousel-focus-up"); },
);
assertWindowed(config, clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[2]; },
() => { qtMock.fireShortcut("karousel-focus-down"); },
);
assertWindowed(config, clients);
});
tests.register("Re-maximize enabled " + suffix, 100, () => {
const config = getConfig();
config.reMaximize = true;
const { qtMock, workspaceMock, world } = init(config);
const clients = workspaceMock.createClientsWithWidths(300, 400, 400);
qtMock.fireShortcut("karousel-window-move-left");
assertWindowed(config, clients);
runOneOf(
() => { clients[2].fullScreen = true; },
() => { clients[2].setMaximize(true, true); },
);
assertFullScreenOrMaximized(clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[0]; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
() => { qtMock.fireShortcut("karousel-focus-left"); },
() => { qtMock.fireShortcut("karousel-focus-start"); },
);
assertWindowed(config, clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[2]; },
() => { qtMock.fireShortcut("karousel-focus-2"); },
() => { qtMock.fireShortcut("karousel-focus-right"); },
() => { qtMock.fireShortcut("karousel-focus-end"); },
);
assertFullScreenOrMaximized(clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[1]; },
() => { qtMock.fireShortcut("karousel-focus-up"); },
);
assertWindowed(config, clients);
runOneOf(
() => { workspaceMock.activeWindow = clients[2]; },
() => { qtMock.fireShortcut("karousel-focus-down"); },
);
assertFullScreenOrMaximized(clients);
});
}
tests.register("Start full-screen " + suffix, 100, () => {
const config = getConfig();
config.reMaximize = true;
const { qtMock, workspaceMock, world } = init(config);
const [windowedClient] = workspaceMock.createClientsWithWidths(300);
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
fullScreenClient.resourceClass = "full-screen-app";
fullScreenClient.fullScreen = true;
workspaceMock.createWindows(fullScreenClient);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(windowedClient));
Assert.assert(clientManager.hasClient(fullScreenClient));
});
Assert.assert(!windowedClient.fullScreen);
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
Assert.centered(config, tilingArea, windowedClient);
Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient);
{
qtMock.fireShortcut("karousel-focus-left");
const opts = { message: "fullScreenClient is not in the grid, so we can't move focus directionally" };
Assert.assert(!windowedClient.fullScreen);
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
Assert.centered(config, tilingArea, windowedClient);
Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient, opts);
}
{
qtMock.fireShortcut("karousel-focus-1");
const opts = { message: "fullScreenClient is not in grid, so it should stay full-screen" };
Assert.assert(!windowedClient.fullScreen);
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
Assert.centered(config, tilingArea, windowedClient);
Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, windowedClient);
}
});
tests.register("Start full-screen (force tiling) " + suffix, 100, () => {
const config = getConfig();
config.reMaximize = true;
config.windowRules = '[{ "class": "full-screen-app", "tile": true }]';
const { qtMock, workspaceMock, world } = init(config);
const column1Width = 300;
const [windowedClient] = workspaceMock.createClientsWithWidths(column1Width);
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
fullScreenClient.resourceClass = "full-screen-app";
fullScreenClient.fullScreen = true;
workspaceMock.createWindows(fullScreenClient);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(windowedClient));
Assert.assert(clientManager.hasClient(fullScreenClient));
});
Assert.assert(!windowedClient.fullScreen);
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
Assert.grid(config, tilingArea, [column1Width], [[windowedClient]], false);
Assert.assert(fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(false));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(false));
Assert.equalRects(fullScreenClient.getActualFrameGeometry(), screen);
Assert.equal(Workspace.activeWindow, fullScreenClient);
let expectedColumn2Width = 0;
let expectedActiveWindow;
runOneOf(
() => {
fullScreenClient.fullScreen = false;
expectedColumn2Width = 400;
expectedActiveWindow = fullScreenClient;
},
() => {
qtMock.fireShortcut("karousel-focus-left");
expectedColumn2Width = tilingArea.width;
expectedActiveWindow = windowedClient;
},
);
const opts = { message: "fullScreenClient should be restored from full-screen mode to tiled mode" };
Assert.assert(!windowedClient.fullScreen);
Assert.equal(windowedClient.keepBelow, shouldKeepBelow(true));
Assert.equal(windowedClient.keepAbove, shouldKeepAbove(true));
Assert.assert(!fullScreenClient.fullScreen);
Assert.equal(fullScreenClient.keepBelow, shouldKeepBelow(true));
Assert.equal(fullScreenClient.keepAbove, shouldKeepAbove(true));
Assert.grid(config, tilingArea, [column1Width, expectedColumn2Width], [[windowedClient], [fullScreenClient]], false, [], opts);
Assert.equal(Workspace.activeWindow, expectedActiveWindow);
});
}
function getConfig(floatingKeepAbove: boolean) {
const config = getDefaultConfig();
config.tiledKeepBelow = !floatingKeepAbove;
config.floatingKeepAbove = floatingKeepAbove;
return config;
}
registerTests(
"(tiled below)",
getConfig.partial(false),
tiled => tiled,
tiled => false,
);
registerTests(
"(floating above)",
getConfig.partial(true),
tiled => false,
tiled => !tiled,
);
}

View File

@@ -1,37 +0,0 @@
tests.register("Pass focus", 100, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client0, client1a, client1b, client1c, client4, client5, client6] = workspaceMock.createClients(7);
workspaceMock.activeWindow = client1b;
qtMock.fireShortcut("karousel-window-move-left");
workspaceMock.activeWindow = client1c;
qtMock.fireShortcut("karousel-window-move-left");
workspaceMock.activeWindow = client1b;
workspaceMock.activeWindow = client5;
function removeWindow(client: MockKwinClient) {
runOneOf(
() => workspaceMock.removeWindow(client),
() => client.desktops = [workspaceMock.desktops[1]],
);
}
removeWindow(client5);
Assert.equal(workspaceMock.activeWindow, client4);
qtMock.fireShortcut("karousel-column-move-to-desktop-2");
Assert.equal(workspaceMock.activeWindow, client1b);
removeWindow(client1b);
Assert.equal(workspaceMock.activeWindow, client1a);
removeWindow(client1a);
Assert.equal(workspaceMock.activeWindow, client1c);
removeWindow(client1c);
Assert.equal(workspaceMock.activeWindow, client0);
removeWindow(client0);
Assert.equal(workspaceMock.activeWindow, client6);
});

View File

@@ -1,52 +0,0 @@
tests.register("Pin", 20, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const screenHalfLeft = new MockQmlRect(0, 0, screen.width/2, screen.height);
const screenHalfRight = new MockQmlRect(screen.width/2, 0, screen.width/2, screen.height);
const tilingAreaHalfLeft = new MockQmlRect(
tilingArea.x,
tilingArea.y,
screen.width/2 - config.gapsOuterLeft - config.gapsOuterRight,
tilingArea.height,
);
const tilingAreaHalfRight = new MockQmlRect(
screen.width/2 + config.gapsOuterLeft,
tilingArea.y,
screen.width/2 - config.gapsOuterLeft - config.gapsOuterRight,
tilingArea.height,
);
const [pinned, tiled1, tiled2] = workspaceMock.createClients(3);
Assert.grid(config, tilingArea, 100, [ [pinned], [tiled1], [tiled2] ], true);
pinned.pin(screenHalfLeft);
Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfLeft);
Assert.grid(config, tilingAreaHalfRight, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.unpin();
Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = true;
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = false;
Assert.equalRects(pinned.getActualFrameGeometry(), screenHalfRight);
Assert.grid(config, tilingAreaHalfLeft, 100, [ [tiled1], [tiled2] ], true);
workspaceMock.activeWindow = pinned;
qtMock.fireShortcut("karousel-window-toggle-floating");
Assert.assert(pinned.tile === null);
pinned.frameGeometry = new MockQmlRect(10, 20, 100, 200); // This is needed because the window's preferredWidth can change when pinning, because frameGeometryChanged can fire before tileChanged. TODO: Ensure pinned window keeps its preferredWidth.
Assert.grid(config, tilingArea, 100, [ [tiled1], [tiled2], [pinned] ], true);
});

View File

@@ -1,232 +0,0 @@
tests.register("Preset Widths default", 5, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
function getRect(columnWidth: number) {
return new MockQmlRect(
tilingArea.x + (tilingArea.width - columnWidth) / 2,
tilingArea.y,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(300));
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(maxWidth));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-increase"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(maxWidth));
runOneOf(
() => qtMock.fireShortcut("karousel-cycle-preset-widths"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(halfWidth));
});
tests.register("Preset Widths custom", 5, () => {
const config = getDefaultConfig();
config.presetWidths = "500px, 250px, 100px, 50%";
const { qtMock, workspaceMock, world } = init(config);
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
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(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");
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(100));
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-reverse"),
() => qtMock.fireShortcut("karousel-column-width-decrease"),
);
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
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");
Assert.equalRects(kwinClient.getActualFrameGeometry(), getRect(width100));
runOneOf(
() => 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, () => {
for (let nColumns = 1; nColumns < 10; nColumns++) {
const config = getDefaultConfig();
config.presetWidths = String(1 / nColumns);
const { qtMock, workspaceMock, world } = init(config);
let firstClient, lastClient;
for (let i = 0; i < nColumns; i++) {
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
if (i === 0) {
firstClient = kwinClient;
}
if (i === nColumns-1) {
lastClient = kwinClient;
}
qtMock.fireShortcut("karousel-cycle-preset-widths");
}
const left = tilingArea.x;
const right = rectRight(tilingArea);
const maxLeftoverPx = nColumns - 1;
const eps = Math.ceil(maxLeftoverPx / 2);
Assert.between(firstClient!.getActualFrameGeometry().x, left, left+eps, { message: `nColumns: ${nColumns}` });
Assert.between(rectRight(lastClient!.getActualFrameGeometry()), right-eps, right, { message: `nColumns: ${nColumns}` });
}
});
tests.register("Preset Widths fill screen non-uniform", 1, () => {
const config = getDefaultConfig();
config.presetWidths = String("50%, 25%");
const { qtMock, workspaceMock, world } = init(config);
const [clientThin1] = workspaceMock.createClientsWithWidths(100);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const [clientThin2] = workspaceMock.createClientsWithWidths(100);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const [clientWide] = workspaceMock.createClientsWithWidths(300);
qtMock.fireShortcut("karousel-cycle-preset-widths");
const maxWidth = tilingArea.width;
const halfWidth = maxWidth/2 - config.gapsInnerHorizontal/2;
const quarterWidth = halfWidth/2 - config.gapsInnerHorizontal/2;
const height = tilingArea.height;
const left1 = tilingArea.x;
const left2 = left1 + config.gapsInnerHorizontal + quarterWidth;
const left3 = left2 + config.gapsInnerHorizontal + quarterWidth;
Assert.rect(clientThin1.getActualFrameGeometry(), left1, tilingArea.y, quarterWidth, height);
Assert.rect(clientThin2.getActualFrameGeometry(), left2, tilingArea.y, quarterWidth, height);
Assert.rect(clientWide.getActualFrameGeometry(), left3, tilingArea.y, halfWidth, height);
Assert.equal(rectRight(clientWide.getActualFrameGeometry()), rectRight(tilingArea));
});

View File

@@ -1,30 +0,0 @@
tests.register("Stacked", 5, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [leftTop, leftBottom, rightTop, rightBottom] = workspaceMock.createClients(4);
const grid = [[leftTop, leftBottom], [rightTop, rightBottom]];
workspaceMock.activeWindow = rightBottom;
qtMock.fireShortcut("karousel-window-move-left");
workspaceMock.activeWindow = leftBottom;
qtMock.fireShortcut("karousel-window-move-left");
Assert.grid(config, tilingArea, 100, grid, true);
qtMock.fireShortcut("karousel-column-toggle-stacked");
Assert.grid(config, tilingArea, 100, grid, true, [0]);
qtMock.fireShortcut("karousel-focus-up");
Assert.grid(config, tilingArea, 100, grid, true, [0]);
qtMock.fireShortcut("karousel-focus-down");
Assert.grid(config, tilingArea, 100, grid, true, [0]);
qtMock.fireShortcut("karousel-window-move-up");
Assert.grid(config, tilingArea, 100, [[leftBottom, leftTop], [rightTop, rightBottom]], true, [0]);
qtMock.fireShortcut("karousel-window-move-down");
Assert.grid(config, tilingArea, 100, grid, true, [0]);
qtMock.fireShortcut("karousel-column-toggle-stacked");
Assert.grid(config, tilingArea, 100, grid, true);
});

View File

@@ -1,110 +0,0 @@
tests.register("User resize", 10, () => {
const config = getDefaultConfig();
config.resizeNeighborColumn = true;
const h = getWindowHeight(2);
let clientLeft: MockKwinClient, clientRightTop: MockKwinClient, clientRightBottom: MockKwinClient;
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeft, clientRightTop);
Assert.rect(clientLeft.getActualFrameGeometry(), left, tilingArea.y, leftWidth, tilingArea.height);
Assert.rect(clientRightTop.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, topHeight);
Assert.rect(clientRightBottom.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y+topHeight+gapV, rightWidth, bottomHeight);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, false, false, false, new MockQmlSize(10, 20));
assertSizes(310, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 300, h, h);
workspaceMock.resizeWindow(clientRightTop, false, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 290, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, false, false, false, new MockQmlSize(-10, 20));
assertSizes(310, 280, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, false, false, true, new MockQmlSize(0, 20));
assertSizes(310, 280, h-40, h+40);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 20));
assertSizes(310, 290, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 290, h, h);
workspaceMock.resizeWindow(clientRightTop, true, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 280, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, true, false, new MockQmlSize(-10, 20));
assertSizes(320, 270, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, false, true, new MockQmlSize(0, 20));
assertSizes(320, 270, h-40, h+40);
}
{
const { qtMock, workspaceMock, world } = init(config);
[clientLeft, clientRightTop, clientRightBottom] = workspaceMock.createClientsWithWidths(300, 300, 200);
clientRightBottom.minSize = new MockQmlSize(295, h-20);
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 20));
assertSizes(310, 295, h, h);
workspaceMock.resizeWindow(clientLeft, true, false, false, new MockQmlSize(10, 0), new MockQmlSize(-10, 0));
assertSizes(310, 295, h, h);
workspaceMock.resizeWindow(clientRightTop, true, false, false, new MockQmlSize(-5, -10), new MockQmlSize(-5, -10));
assertSizes(310, 295, h-20, h+20);
workspaceMock.resizeWindow(clientRightBottom, true, true, false, new MockQmlSize(-10, 20));
assertSizes(310, 295, h-20, h+20);
workspaceMock.resizeWindow(clientRightTop, true, true, false, new MockQmlSize(-10, 0));
assertSizes(310, 295, h-20, h+20);
// TODO
// workspaceMock.resizeWindow(clientRightBottom, true, false, true, new MockQmlSize(0, -80));
// assertSizes(310, 295, h+60, h-20);
}
{
const { qtMock, workspaceMock, world } = init(config);
const [clientLeftTop, clientLeftBottom, clientRight] = workspaceMock.createClientsWithWidths(300, 200, 300);
clientLeftBottom.minSize = new MockQmlSize(295, h-20);
function assertSizes(leftWidth: number, rightWidth: number, topHeight: number, bottomHeight: number) {
const { left, right } = getGridBounds(clientLeftTop, clientRight);
Assert.rect(clientLeftTop.getActualFrameGeometry(), left, tilingArea.y, leftWidth, topHeight);
Assert.rect(clientLeftBottom.getActualFrameGeometry(), left, tilingArea.y+topHeight+gapV, leftWidth, bottomHeight);
Assert.rect(clientRight.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, tilingArea.height);
}
workspaceMock.activeWindow = clientLeftBottom;
qtMock.fireShortcut("karousel-window-move-left");
assertSizes(300, 300, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(-10, 0));
assertSizes(295, 305, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(10, 0));
assertSizes(305, 295, h, h);
workspaceMock.resizeWindow(clientLeftTop, true, false, false, new MockQmlSize(-20, 0), new MockQmlSize(20, 0));
assertSizes(305, 295, h, h);
}
});

View File

@@ -1 +0,0 @@
tests.run();

View File

@@ -0,0 +1,36 @@
{
const testCases = [
{tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true},
{tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "ksmserver-logout-greeter", caption: "anything", shouldTile: false},
{tiledByDefault: true, resourceClass: "org.kde.plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: true, resourceClass: "plasmashell", caption: "something", shouldTile: false},
{tiledByDefault: false, resourceClass: "org.kde.kfind", caption: "something", shouldTile: true},
{tiledByDefault: false, resourceClass: "kfind", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "something", shouldTile: true},
{tiledByDefault: true, resourceClass: "zoom", caption: "zoom", shouldTile: false},
];
const enforcer = new WindowRuleEnforcer(JSON.parse(defaultWindowRules));
for (const testCase of testCases) {
const kwinClient: any = createKwinClient(testCase.tiledByDefault, testCase.resourceClass, testCase.caption);
assert(enforcer.shouldTile(kwinClient) === testCase.shouldTile, "failed case: " + JSON.stringify(testCase));
}
function createKwinClient(normalWindow: boolean, resoureClass: string, caption: string) {
return {
normalWindow: normalWindow,
transient: false,
managed: true,
pid: 100,
moveable: true,
resizeable: true,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
resourceClass: resoureClass,
caption: caption,
}
}
}

17
src/tests/tests.ts Normal file
View File

@@ -0,0 +1,17 @@
declare const process: {
exit(code?: number): void,
};
function assert(assertion: boolean, message?: string) {
if (assertion) {
return;
}
if (message != undefined) {
console.assert(assertion, message);
} else {
console.assert(assertion);
}
console.trace();
process.exit(1);
}

View File

@@ -1,10 +1,4 @@
{
"extends": "../tsconfig.json",
"include": [
"../lib/**/*",
"./utils/**/*",
"./units/**/*",
"./flows/**/*",
"./main.ts"
]
"include": ["../lib/**/*", "./**/*"]
}

View File

@@ -1,45 +0,0 @@
tests.register("PresetWidths", 1, () => {
const minWidth = 50;
const maxWidth = 800;
const spacing = 10;
const testCases = [
{ str: "100%, 50%", result: [395, 800] },
{ str: "105%, 50%", result: [395, 800] },
{ str: "100px,50 px", result: [50, 100] },
{ str: "900px,25 px", result: [50, 800] },
{ str: " 100px, 25 % , 0.1 ", result: [71, 100, 192] },
{ str: "100px, 25%, 0.1, 100px", result: [71, 100, 192] },
{ str: "100px, -25 % , 0.1 ", error: true },
{ str: "100px, 25 % , -0.1 ", error: true },
{ str: "100px, 25 % , 0.1p", error: true },
{ str: "100px, % , 0.1 ", error: true },
{ str: "100px, , 0.1 ", error: true },
{ str: "100px, 0, 0.1 ", error: true },
{ str: "100px,, 0.1 ", error: true },
{ str: "100px, 25 % , ", error: true },
{ str: "asdf", error: true },
{ str: "", error: true },
{ str: " ", error: true },
];
function assertWidths(presetWidths: PresetWidths, expectedWidths: number[]) {
let currentWidth = 0;
for (const expectedWidth of expectedWidths) {
currentWidth = presetWidths.next(currentWidth, minWidth, maxWidth);
Assert.equal(currentWidth, expectedWidth);
}
const repeatedWidth = presetWidths.next(currentWidth, minWidth, maxWidth);
Assert.equal(repeatedWidth, expectedWidths[0]);
}
for (const testCase of testCases) {
try {
const presetWidths = new PresetWidths(testCase.str, spacing);
Assert.assert(!testCase.error);
assertWidths(presetWidths, testCase.result!);
} catch (error) {
Assert.assert(testCase.error === true);
}
}
});

View File

@@ -1,41 +0,0 @@
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

@@ -1,44 +0,0 @@
tests.register("WindowRuleEnforcer", 1, () => {
screen = new MockQmlRect(0, 0, 800, 600);
Workspace = new MockWorkspace();
const testCases = [
{ tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true },
{ tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false },
{ tiledByDefault: true, resourceClass: "org.kde.plasmashell", caption: "something", shouldTile: false },
{ tiledByDefault: true, resourceClass: "plasmashell", caption: "something", shouldTile: false },
{ tiledByDefault: false, resourceClass: "org.kde.kfind", caption: "something", shouldTile: true },
{ tiledByDefault: false, resourceClass: "kfind", caption: "something", shouldTile: true },
{ tiledByDefault: true, resourceClass: "org.kde.kruler", caption: "anything", shouldTile: false },
{ tiledByDefault: true, resourceClass: "kruler", caption: "anything", shouldTile: false },
{ tiledByDefault: true, resourceClass: "zoom", caption: "something", shouldTile: true },
{ tiledByDefault: true, resourceClass: "zoom", caption: "zoom", shouldTile: false },
];
const enforcer = new WindowRuleEnforcer(JSON.parse(defaultWindowRules));
for (const testCase of testCases) {
const kwinClient: any = createKwinClient(testCase.tiledByDefault, testCase.resourceClass, testCase.caption);
Assert.assert(
enforcer.shouldTile(kwinClient) === testCase.shouldTile,
{ message: "failed case: " + JSON.stringify(testCase) },
);
}
function createKwinClient(normalWindow: boolean, resourceClass: string, caption: string) {
return {
normalWindow: normalWindow,
transient: false,
clientGeometry: new MockQmlRect(0, 0, 200, 200),
managed: true,
pid: 100,
moveable: true,
resizeable: true,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
resourceClass: resourceClass,
caption: caption,
};
}
});

View File

@@ -1,22 +0,0 @@
tests.register("RateLimiter", 1, () => {
const rateLimiter = new RateLimiter(3, 100);
function testRateLimiter() {
Assert.assert(rateLimiter.acquire());
Assert.assert(rateLimiter.acquire());
Assert.assert(rateLimiter.acquire());
Assert.assert(!rateLimiter.acquire());
Assert.assert(!rateLimiter.acquire());
}
timeControl(addTime => {
testRateLimiter();
addTime(10);
Assert.assert(!rateLimiter.acquire(), { message: "The interval hasn't expired yet" });
addTime(90);
// the rate limiter interval has expired, let's test again
testRateLimiter();
});
});

View File

@@ -1,246 +0,0 @@
tests.register("fillSpace", 1, () => {
const testCases: {
availableSpace: number,
items: { min: number, max: number }[],
expected: number[],
}[] = [
{
availableSpace: 600,
items: [],
expected: [],
},
{
availableSpace: 600,
items: [
{ min: 10, max: 600 },
{ min: 10, max: 600 },
],
expected: [300, 300],
},
{
availableSpace: 700,
items: [
{ min: 300, max: 300 },
{ min: 300, max: 300 },
],
expected: [300, 300],
},
{
availableSpace: 700,
items: [
{ min: 300, max: 300 },
{ min: 300, max: 300 },
{ min: 10, max: 900 },
],
expected: [300, 300, 100],
},
{
availableSpace: 600,
items: [
{ min: 10, max: 250 },
{ min: 10, max: 500 },
],
expected: [250, 350],
},
{
availableSpace: 600,
items: [
{ min: 10, max: 250 },
{ min: 400, max: 500 },
],
expected: [200, 400],
},
{
availableSpace: 765,
items: [
{ min: 10, max: 250 },
{ min: 10, max: 254 },
{ min: 10, max: 500 },
],
expected: [250, 254, 261],
},
{
availableSpace: 600,
items: [
{ min: 10, max: 150 },
{ min: 400, max: 500 },
],
expected: [150, 450],
},
{
availableSpace: 750,
items: [
{ min: 10, max: 250 },
{ min: 10, max: 250 },
{ min: 400, max: 500 },
{ min: 10, max: 300 },
],
expected: [117, 117, 400, 116],
},
{
availableSpace: 750,
items: [
{ min: 10, max: 250 },
{ min: 120, max: 250 },
{ min: 400, max: 500 },
{ min: 10, max: 300 },
],
expected: [115, 120, 400, 115],
},
{
availableSpace: 1200,
items: [
{ min: 10, max: 250 },
{ min: 10, max: 500 },
],
expected: [250, 500],
},
{
availableSpace: 5,
items: [
{ min: 10, max: 250 },
{ min: 10, max: 500 },
],
expected: [10, 10],
},
{
availableSpace: 800,
items: [
{ min: 114, max: 800 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 109, max: 800 },
{ min: 10, max: 800 },
],
expected: [114, 93, 93, 93, 93, 93, 111, 110],
},
{
availableSpace: 801,
items: [
{ min: 114, max: 800 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 109, max: 800 },
{ min: 10, max: 800 },
],
expected: [114, 93, 93, 93, 93, 93, 111, 111],
},
{
availableSpace: 801,
items: [
{ min: 114, max: 800 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 109, max: 800 },
{ min: 10, max: 95 },
],
expected: [121, 93, 93, 93, 93, 93, 120, 95],
},
{
availableSpace: 799,
items: [
{ min: 10, max: 86 },
{ min: 107, max: 800 },
{ min: 107, max: 800 },
{ min: 107, max: 800 },
{ min: 107, max: 800 },
{ min: 107, max: 800 },
{ min: 10, max: 91},
{ min: 105, max: 800 },
],
expected: [80, 107, 107, 107, 107, 107, 79, 105],
},
{
availableSpace: 1029,
items: [
{ min: 114, max: 800 },
{ min: 114, max: 800 },
{ min: 114, max: 800 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 10, max: 93 },
{ min: 109, max: 800 },
{ min: 10, max: 800 },
],
expected: [114, 114, 114, 93, 93, 93, 93, 93, 111, 111],
},
{
availableSpace: 602,
items: [
{ min: 10, max: 600 },
{ min: 10, max: 600 },
{ min: 10, max: 600 },
],
expected: [200, 200, 200],
},
{
availableSpace: 602,
items: [
{ min: 204, max: 600 },
{ min: 202, max: 600 },
{ min: 10, max: 600 },
],
expected: [204, 202, 196],
},
{
availableSpace: 803,
items: [
{ min: 204, max: 600 },
{ min: 10, max: 600 },
{ min: 10, max: 600 },
{ min: 10, max: 600 },
],
expected: [204, 200, 200, 199],
},
{
availableSpace: 900,
items: [
{ min: 10, max: 120 },
{ min: 10, max: 250 },
{ min: 500, max: 500 },
{ min: 300, max: 500 },
],
expected: [50, 50, 500, 300],
},
{
availableSpace: 845,
items: [
{ min: 5, max: 5 },
{ min: 10, max: 40 },
{ min: 500, max: 500 },
{ min: 300, max: 500 },
],
expected: [5, 40, 500, 300],
},
{
availableSpace: 800,
items: [
{ min: 10, max: 20 },
{ min: 220, max: 221 },
{ min: 250, max: 260 },
{ min: 300, max: 305 },
],
expected: [20, 221, 259, 300],
},
];
for (const testCase of testCases) {
const result = fillSpace(testCase.availableSpace, testCase.items);
Assert.equalArrays(
result,
testCase.expected,
{ message: JSON.stringify(testCase) },
);
}
});

View File

@@ -1,53 +0,0 @@
tests.register("math", 1, () => {
const rect = new MockQmlRect(100, 200, 10, 20);
const testCases: {
rect: QmlRect,
point: QmlPoint,
contained: boolean,
}[] = [
{
rect: rect,
point: new MockQmlPoint(100, 200),
contained: true,
},
{
rect: rect,
point: new MockQmlPoint(110, 220),
contained: true,
},
{
rect: rect,
point: new MockQmlPoint(105, 205),
contained: true,
},
{
rect: rect,
point: new MockQmlPoint(110.01, 205),
contained: false,
},
{
rect: rect,
point: new MockQmlPoint(105, 220.01),
contained: false,
},
{
rect: rect,
point: new MockQmlPoint(16, 205),
contained: false,
},
{
rect: rect,
point: new MockQmlPoint(105, 16),
contained: false,
},
];
for (const testCase of testCases) {
const result = rectContainsPoint(testCase.rect, testCase.point);
Assert.equal(
result,
testCase.contained,
{ message: JSON.stringify(testCase) },
);
}
});

View File

@@ -1,36 +0,0 @@
tests.register("Clients.canTileEver", 1, () => {
const testCases = [
{ clientProperties: { resourceClass: "app", caption: "Title" }, tileable: true },
{ clientProperties: { resourceClass: "app", caption: "Title", moveable: false }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", resizeable: false }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", normalWindow: false, popupWindow: true }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", moveable: false, resizeable: false, fullScreen: true }, tileable: true },
{ clientProperties: { resourceClass: "ksmserver-logout-greeter", caption: "Caption" }, tileable: false },
{ clientProperties: { resourceClass: "xwaylandvideobridge", caption: "" }, tileable: false },
];
for (const testCase of testCases) {
const kwinClient: any = createKwinClient(testCase.clientProperties);
Assert.assert(
Clients.canTileEver(kwinClient) === testCase.tileable,
{ message: "failed case: " + JSON.stringify(testCase) },
);
}
function createKwinClient(properties: { resourceClass: string, caption: string }) {
const defaultProperties = {
normalWindow: true,
transient: false,
managed: true,
pid: 100,
moveable: true,
resizeable: true,
fullScreen: false,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
};
return { ...defaultProperties, ...properties };
}
});

Some files were not shown because too many files have changed in this diff Show More