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
97 changed files with 1141 additions and 3911 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.

View File

@@ -1,32 +1,29 @@
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
TESTS := true
.PHONY: *
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
tests:
ifeq (${TESTS}, true)
./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 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,16 +38,12 @@ 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 (One window in the column visible, others shaded; not supported on Wayland) |
@@ -58,10 +53,7 @@ Here's the default ones:
| 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+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 |
@@ -69,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

@@ -301,19 +301,22 @@
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_presetWidths">
<widget class="QLabel" name="label_manualResizeStep">
<property name="text">
<string>Preset widths:</string>
</property>
<property name="toolTip">
<string>Widths used for cycling through widths</string>
<string>Manual resize step size:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="kcfg_presetWidths">
<property name="toolTip">
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
<widget class="QSpinBox" name="kcfg_manualResizeStep">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>

View File

@@ -25,14 +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
}
}

View File

@@ -9,7 +9,7 @@
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"Version": "0.11",
"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,6 +0,0 @@
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;
declare const notificationInvalidPresetWidths: Notification;

View File

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

View File

@@ -1,11 +1,5 @@
type 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)[]) {
@@ -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,15 +1,9 @@
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;
}
@@ -23,12 +17,13 @@ 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(),
)
@@ -40,13 +35,11 @@ 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;
}
@@ -57,11 +50,11 @@ class ContextualResizer {
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,11 +64,12 @@ 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,
)
@@ -86,4 +80,17 @@ class ContextualResizer {
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
private static findNextStep(steps: number[], evaluate: (step: number) => number) {
let bestScore = Infinity;
let bestStep = undefined;
for (const step of steps) {
const score = evaluate(step);
if (score > 0 && score < bestScore) {
bestScore = score;
bestStep = step;
}
}
return bestStep;
}
}

View File

@@ -1,31 +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 decreaseWidth(column: Column, step: number) {
column.adjustWidth(-step, true);
}
}

View File

@@ -1,22 +1,22 @@
type Config = {
gapsOuterTop: number;
gapsOuterBottom: number;
gapsOuterLeft: number;
gapsOuterRight: number;
gapsInnerHorizontal: number;
gapsInnerVertical: number;
manualScrollStep: number;
presetWidths: string;
offScreenOpacity: number;
untileOnDrag: boolean;
stackColumnsByDefault: boolean;
resizeNeighborColumn: boolean;
reMaximize: boolean;
skipSwitcher: boolean;
scrollingLazy: boolean;
scrollingCentered: boolean;
scrollingGrouped: boolean;
tiledKeepBelow: boolean;
floatingKeepAbove: boolean;
windowRules: string;
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,4 +1,12 @@
const defaultWindowRules = `[
{
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "xwaylandvideobridge",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false
@@ -27,23 +35,28 @@ const defaultWindowRules = `[
"class": "(org\\\\.kde\\\\.)?yakuake",
"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
}
@@ -86,9 +99,9 @@ const configDef = [
default: 200,
},
{
name: "presetWidths",
type: "String",
default: "50%, 100%",
name: "manualResizeStep",
type: "UInt",
default: 600,
},
{
name: "offScreenOpacity",

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 +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 @@
type KWin = {
__brand: "KWin";
readConfig(key: string, defaultValue: any): any;
};
type Workspace = {
__brand: "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<[]>;
@@ -48,17 +45,18 @@ const enum MaximizedMode {
Maximized,
}
type Tile = { __brand: "Tile" };
type Output = { __brand: "Output" };
type Tile = unknown;
type KwinClient = {
__brand: "KwinClient";
type Output = {
name: string;
};
interface 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;
@@ -83,14 +81,15 @@ type KwinClient = {
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<[]>;
@@ -98,15 +97,13 @@ type KwinClient = {
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
};
type KwinDesktop = {
__brand: "KwinDesktop";
}
interface KwinDesktop {
readonly id: string;
};
}
type ShortcutHandler = QmlObject & {
type ShortcutHandler = {
readonly activated: QSignal<[]>;
destroy(): void;
};

View File

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

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

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

View File

@@ -1,428 +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.focus();
}
public readonly focusRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
rightColumn.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 grid = dm.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
}
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.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);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getLeftColumn(column));
window.moveToColumn(newColumn, true);
}
}
public readonly windowMoveRight = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid, bottom: boolean = 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);
grid.desktop.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn, true);
}
}
// 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);
}
public readonly windowMoveEnd = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn, true);
}
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 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();
const visibleRange = desktop.getCurrentVisibleRange();
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
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, true));
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, true));
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) => {
desktopManager.getCurrentDesktop().adjustScroll(amount, false);
}
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
}
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
}
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.desktop.scrollCenterRange(column);
}
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const leftColumn = grid.getLeftColumn(column);
if (leftColumn === null) {
return;
}
grid.desktop.scrollToColumn(leftColumn);
}
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
if (column === null) {
return;
}
const rightColumn = grid.getRightColumn(column);
if (rightColumn === null) {
return;
}
grid.desktop.scrollToColumn(rightColumn);
}
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {
dm.selectScreen(Workspace.activeScreen);
}
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.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);
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 newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).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 newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
};
}
namespace Actions {
export type Config = {
manualScrollStep: number;
presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
};
columnResizer: ColumnResizer;
};
export type ColumnResizer = {
increaseWidth(column: Column): void;
decreaseWidth(column: Column): void;
};
}

View File

@@ -1,273 +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: "One window in the column visible, others shaded; not supported on Wayland",
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: "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

@@ -2,8 +2,7 @@ type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence?: string;
action: () => void;
defaultKeySequence: string;
};
type NumKeyBinding = {
@@ -12,7 +11,6 @@ type NumKeyBinding = {
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: (i: number) => void;
};
function catchWrap(f: () => void) {
@@ -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());
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);
}
@@ -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) {
@@ -203,17 +174,9 @@ class Column {
window.focus();
}
public isFocused() {
const lastFocusedWindow = this.grid.getLastFocusedWindow();
if (lastFocusedWindow === null) {
return false;
}
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
}
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;
}
@@ -275,13 +238,18 @@ class Column {
return true;
}
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
} else {
this.windows.insertStart(window);
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);
}
@@ -298,7 +266,7 @@ class Column {
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);

View File

@@ -1,5 +1,9 @@
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 dirty: boolean;
private dirtyScroll: boolean;
@@ -7,25 +11,23 @@ class Desktop {
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,
) {
constructor(screen: Output, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = true;
this.screen = screen;
this.kwinDesktop = kwinDesktop;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
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,7 +57,7 @@ class Desktop {
)
}
public scrollIntoView(range: Range) {
public scrollIntoView(range: Desktop.Range) {
const left = range.getLeft();
const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange();
@@ -72,10 +74,10 @@ class Desktop {
this.setScroll(targetScrollX, false);
}
public scrollCenterRange(range: Range) {
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), true);
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
public scrollCenterVisible(focusedColumn: Column) {
@@ -95,13 +97,13 @@ class Desktop {
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), 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() {
@@ -125,6 +127,40 @@ class Desktop {
this.setScroll(this.scrollX + dx, force);
}
public equalizeVisibleColumnsWidths() {
const visibleRange = this.getCurrentVisibleRange();
const visibleColumns = Array.from(this.grid.getVisibleColumns(visibleRange, true));
let remainingWidth = this.tilingArea.width - (visibleColumns.length-1) * this.grid.config.gapsInnerHorizontal;
let remainingColumns = visibleColumns.length;
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() {
// TODO (optimization): only arrange visible windows
this.updateArea();
@@ -135,10 +171,6 @@ class Desktop {
this.dirty = false;
}
public forceArrange() {
this.dirty = true;
}
public onLayoutChanged() {
this.dirty = true;
this.dirtyScroll = true;
@@ -157,14 +189,48 @@ class Desktop {
namespace Desktop {
export type Config = {
marginTop: number;
marginBottom: number;
marginLeft: number;
marginRight: number;
scroller: Desktop.Scroller;
clamper: Desktop.Clamper;
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 {
private left: Column;
private right: Column;
@@ -176,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;
@@ -187,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;
@@ -205,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();
}
@@ -239,9 +305,9 @@ namespace Desktop {
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
};
}
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
};
}
}

View File

@@ -1,3 +1,5 @@
import Range = Desktop.Range;
class Grid {
public readonly desktop: Desktop;
public readonly config: LayoutConfig;
@@ -22,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();
@@ -42,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() {
@@ -57,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);
}
@@ -104,19 +106,19 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
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, fullyVisible: boolean) {
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;
@@ -125,14 +127,29 @@ class Grid {
return last;
}
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
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);
@@ -145,11 +162,11 @@ 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();
@@ -158,14 +175,14 @@ class Grid {
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 (passFocus && columnToFocus !== null) {
@@ -176,8 +193,8 @@ class Grid {
}
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();
@@ -186,7 +203,7 @@ class Grid {
public onColumnFocused(column: Column) {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn !== null && lastFocusedColumn !== column) {
if (lastFocusedColumn !== null) {
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;

View File

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

View File

@@ -1,41 +0,0 @@
type 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();
}
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

@@ -14,16 +14,16 @@ class Window {
};
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this, true);
column.onWindowAdded(this);
}
public moveToColumn(targetColumn: Column, bottom: boolean) {
public moveToColumn(targetColumn: Column) {
if (targetColumn === this.column) {
return;
}
this.column.onWindowRemoved(this, this.isFocused() && targetColumn.grid !== this.column.grid);
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) {
@@ -66,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);
}
@@ -83,6 +75,7 @@ class Window {
}
this.client.setFullScreen(false);
this.client.setMaximize(false, false);
this.column.grid.desktop.onLayoutChanged();
}
public onMaximizedChanged(maximizedMode: MaximizedMode) {
@@ -114,6 +107,31 @@ 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, true);
@@ -127,7 +145,7 @@ class Window {
namespace Window {
export type State = {
fullScreen: boolean;
maximizedMode: MaximizedMode;
};
fullScreen: boolean,
maximizedMode: MaximizedMode,
}
}

View File

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

View File

@@ -11,12 +11,14 @@ class WindowRuleEnforcer {
}
public shouldTile(kwinClient: KwinClient) {
return this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!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)
)
);
}
@@ -28,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) {

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}";
` :
"";
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,
);
}
}
namespace ShortcutAction {
export type 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--;
}
}
type Range = {
start: number,
end: number,
n: number,
};
type Fencepost = {
value: number,
nMin: number,
nMax: number,
}
}

View File

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

@@ -2,6 +2,10 @@ 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)
});
@@ -13,7 +17,7 @@ function initWorkspaceSignalHandlers(world: World) {
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}
@@ -30,25 +34,25 @@ function initWorkspaceSignalHandlers(world: World) {
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,21 +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;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (
Clients.canTileEver(kwinClient) &&
!kwinClient.fullScreen &&
!Clients.isFullScreenGeometry(kwinClient) &&
this.windowRuleEnforcer.shouldTile(kwinClient)
) {
Clients.makeTileable(kwinClient);
console.assert(Clients.canTileNow(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
console.assert(desktop !== undefined);
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
} 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);
}
@@ -68,7 +58,7 @@ class ClientManager {
}
private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient || kwinClient.transientFor === null) {
if (!kwinClient.transient) {
return null;
}
@@ -178,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;
}
@@ -217,6 +207,6 @@ class ClientManager {
namespace ClientManager {
export type Config = {
floatingKeepAbove: boolean;
};
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;

View File

@@ -1,21 +1,10 @@
namespace Clients {
const prohibitedClasses = [
"ksmserver-logout-greeter",
"xwaylandvideobridge",
];
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable &&
kwinClient.resizeable &&
!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) {
@@ -47,8 +36,7 @@ namespace Clients {
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.clientGeometry.width === fullScreenArea.width &&
kwinClient.clientGeometry.height === fullScreenArea.height;
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {

View File

@@ -1,66 +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,
public readonly layoutConfig: LayoutConfig,
pinManager: PinManager,
config: Desktop.Config,
layoutConfig: LayoutConfig,
currentScreen: Output,
currentActivity: string,
currentDesktop: KwinDesktop,
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(currentActivity, currentDesktop);
this.addDesktop(currentScreen, currentActivity, currentDesktop);
}
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
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,
);
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() {
@@ -83,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();
@@ -121,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,4 +1,6 @@
class PinManager {
// TODO: per-screen
private readonly pinnedClients: Set<KwinClient>;
constructor() {
@@ -17,7 +19,7 @@ class PinManager {
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;
}

View File

@@ -1,30 +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;
constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
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.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, () => {
@@ -47,7 +36,6 @@ class World {
skipSwitcher: config.skipSwitcher,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
untileOnDrag: config.untileOnDrag,
};
this.desktopManager = new DesktopManager(
@@ -61,6 +49,7 @@ class World {
clamper: config.scrollingLazy ? new EdgeClamper() : new CenterClamper(),
},
layoutConfig,
Workspace.activeScreen,
Workspace.currentActivity,
Workspace.currentDesktop,
);
@@ -101,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;
}
@@ -114,12 +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);
this.doIfTiled(Workspace.activeWindow, followTransient, f);
}
public destroy() {

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) {
@@ -18,9 +18,11 @@ namespace ClientState {
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,
@@ -48,7 +50,7 @@ namespace ClientState {
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {

View File

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

View File

@@ -11,7 +11,7 @@ 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);
@@ -19,6 +19,9 @@ namespace ClientState {
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,7 +12,7 @@ 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: boolean) {
@@ -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,7 +48,7 @@ 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);
@@ -70,13 +69,12 @@ namespace ClientState {
});
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);
});
}
return;
@@ -84,16 +82,8 @@ namespace ClientState {
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();
}
});
@@ -101,12 +91,10 @@ namespace ClientState {
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (resizing) {
resizing = false;
resizeNeighbor = undefined;
window.column.grid.onUserResizeFinished();
}
});
let externalFrameGeometryChangedRateLimiter = new RateLimiter(4, Tiled.maxExternalFrameGeometryChangedIntervalMs);
manager.connect(kwinClient.frameGeometryChanged, (oldGeometry: QmlRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
@@ -124,38 +112,18 @@ 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.left !== oldGeometry.left,
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());
}
});
@@ -175,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 > kwinClient.clientGeometry.right) {
return column.grid.getRightColumn(column);
} else if (Workspace.cursorPos.x < kwinClient.clientGeometry.left) {
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
@@ -194,7 +150,7 @@ namespace ClientState {
}
const newColumn = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
window.moveToColumn(newColumn, true);
window.moveToColumn(newColumn);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
@@ -204,7 +160,6 @@ namespace ClientState {
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.kwinClient.keepAbove = false;
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
@@ -233,7 +188,7 @@ namespace ClientState {
namespace Tiled {
export type WindowState = {
skipSwitcher: boolean;
};
skipSwitcher: boolean,
}
}
}

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,23 +0,0 @@
tests.register("Center focused", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [client0, client1, client2] = workspaceMock.createClientsWithWidths(300, 152, 300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client0));
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
Assert.assert(workspaceMock.activeWindow === client2);
Assert.columnsFillTilingArea([client0, client1, client2]);
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, screen, client2);
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
qtMock.fireShortcut("karousel-focus-left");
Assert.centered(config, screen, client2, { message: "No scrolling should have occured" });
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
});

View File

@@ -1,124 +0,0 @@
tests.register("columns squeeze side", 1, () => {
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].frameGeometry.width, testCase.widths[i], assertOpt);
}
}
}
const frames = clients.map(client => client.frameGeometry);
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry);
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], assertOpt);
}
}
});
tests.register("columns squeeze side (just scroll)", 1, () => {
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: KwinClient[], visible: boolean[]) {
for (let i = 0; i < clients.length; i++) {
if (visible[i]) {
Assert.fullyVisible(clients[i].frameGeometry, { message: assertMsg, skip: 1 });
} else {
Assert.notFullyVisible(clients[i].frameGeometry, { 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.frameGeometry);
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry);
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], { message: assertMsg });
}
}
});

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(
Math.round((screen.width - width) / 2),
tilingArea.top,
width,
tilingArea.height,
);
}
const [client] = workspaceMock.createClientsWithFrames(getClientDesiredFrame(100));
Assert.equalRects(client.frameGeometry, getTiledFrame(100), { message: "We should tile the window, respecting its desired width" });
function testExternalResizing() {
client.frameGeometry = getClientDesiredFrame(110);
Assert.equalRects(client.frameGeometry, getTiledFrame(110), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(120);
Assert.equalRects(client.frameGeometry, getTiledFrame(120), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(130);
Assert.equalRects(client.frameGeometry, getTiledFrame(130), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(140);
Assert.equalRects(client.frameGeometry, getTiledFrame(140), { message: "We should re-arrange the window, respecting its new desired width" });
client.frameGeometry = getClientDesiredFrame(200);
Assert.equalRects(client.frameGeometry, 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,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: KwinClient[][]) {
qtMock.fireShortcut(shortcutName);
Assert.grid(config, screen, 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, screen, 300, [[client1]], true);
const [client2] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2]], true);
const [client3] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-left"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
workspaceMock.activeWindow = client1;
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, screen, 300, [[client1]], true);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
});

View File

@@ -1,145 +0,0 @@
tests.register("Maximization", 100, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(kwinClient));
});
const columnLeftX = screen.width/2 - 300/2;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
kwinClient.fullScreen = true;
Assert.equalRects(kwinClient.frameGeometry, screen);
kwinClient.fullScreen = false;
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
kwinClient.setMaximize(true, true);
Assert.equalRects(kwinClient.frameGeometry, screen);
kwinClient.setMaximize(true, false);
Assert.rect(kwinClient.frameGeometry, columnLeftX, 0, 300, screen.height);
kwinClient.setMaximize(false, false);
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
});
tests.register("Maximize with transient", 100, () => {
const config = getDefaultConfig();
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.equalRects(parent.frameGeometry, screen);
workspaceMock.createWindows(child);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(child));
});
Assert.rect(child.frameGeometry, 14, 24, 50, 50);
Assert.equalRects(parent.frameGeometry, screen);
});
tests.register("Re-maximize disabled", 100, () => {
const config = getDefaultConfig();
config.reMaximize = false;
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => client2.fullScreen = true,
() => client2.setMaximize(true, true),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
() => qtMock.fireShortcut("karousel-focus-end"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
});
tests.register("Re-maximize enabled", 100, () => {
const config = getDefaultConfig();
config.reMaximize = true;
const { qtMock, workspaceMock, world } = init(config);
const [client1, client2] = workspaceMock.createClientsWithWidths(300, 400);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(client1));
Assert.assert(clientManager.hasClient(client2));
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => client2.fullScreen = true,
() => client2.setMaximize(true, true),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
runOneOf(
() => workspaceMock.activeWindow = client1,
() => qtMock.fireShortcut("karousel-focus-1"),
() => qtMock.fireShortcut("karousel-focus-left"),
() => qtMock.fireShortcut("karousel-focus-start"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.rect(client2.frameGeometry, column2LeftX, columnTopY, 400, columnHeight);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
() => qtMock.fireShortcut("karousel-focus-end"),
);
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
});

View File

@@ -1,40 +0,0 @@
tests.register("Pass focus", 20, () => {
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);
removeWindow(client6);
Assert.equal(workspaceMock.activeWindow, null);
});

View File

@@ -1,39 +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 [pinned, tiled1, tiled2] = workspaceMock.createClients(3);
Assert.grid(config, screen, 100, [ [pinned], [tiled1], [tiled2] ], true);
pinned.pin(screenHalfLeft);
Assert.equalRects(pinned.frameGeometry, screenHalfLeft);
Assert.grid(config, screenHalfRight, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.unpin();
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screen, 100, [ [tiled1], [tiled2] ], true);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = true;
Assert.grid(config, screen, 100, [ [tiled1], [tiled2] ], true);
pinned.minimized = false;
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, 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, screen, 100, [ [tiled1], [tiled2], [pinned] ], true);
});

View File

@@ -1,134 +0,0 @@
tests.register("Preset Widths default", 1, () => {
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(
(screen.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(300);
Assert.equalRects(kwinClient.frameGeometry, getRect(300));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
});
tests.register("Preset Widths custom", 1, () => {
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(
(screen.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,
);
}
const [kwinClient] = workspaceMock.createClientsWithWidths(200);
Assert.equalRects(kwinClient.frameGeometry, getRect(200));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
});
tests.register("Preset Widths fill screen uniform", 1, () => {
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.left;
const right = tilingArea.right;
const maxLeftoverPx = nColumns - 1;
const eps = Math.ceil(maxLeftoverPx / 2);
Assert.between(firstClient!.frameGeometry.left, left, left+eps, { message: `nColumns: ${nColumns}` });
Assert.between(lastClient!.frameGeometry.right, 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.left;
const left2 = left1 + config.gapsInnerHorizontal + quarterWidth;
const left3 = left2 + config.gapsInnerHorizontal + quarterWidth;
Assert.rect(clientThin1.frameGeometry, left1, tilingArea.top, quarterWidth, height);
Assert.rect(clientThin2.frameGeometry, left2, tilingArea.top, quarterWidth, height);
Assert.rect(clientWide.frameGeometry, left3, tilingArea.top, halfWidth, height);
Assert.equal(clientWide.frameGeometry.right, tilingArea.right);
});

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.frameGeometry, left, tilingArea.top, leftWidth, tilingArea.height);
Assert.rect(clientRightTop.frameGeometry, left+leftWidth+gapH, tilingArea.top, rightWidth, topHeight);
Assert.rect(clientRightBottom.frameGeometry, left+leftWidth+gapH, tilingArea.top+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.frameGeometry, left, tilingArea.top, leftWidth, topHeight);
Assert.rect(clientLeftBottom.frameGeometry, left, tilingArea.top+topHeight+gapV, leftWidth, bottomHeight);
Assert.rect(clientRight.frameGeometry, left+leftWidth+gapH, tilingArea.top, 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,40 +0,0 @@
tests.register("WindowRuleEnforcer", 1, () => {
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,
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,34 +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: "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,
popupWindow: false,
minimized: false,
desktops: [1],
activities: [1],
};
return { ...defaultProperties, ...properties };
}
});

View File

@@ -1,222 +0,0 @@
namespace Assert {
type Options = {
message?: string,
skip?: number,
}
export function assert(
assertion: boolean,
{ message, skip=0 }: Options = {},
) {
if (assertion) {
return;
}
if (message != undefined) {
console.assert(assertion, message);
} else {
console.assert(assertion);
}
console.log(getStackTrace(skip+1));
console.log("Random branches:");
if (runLog !== undefined) {
for (const message of runLog) {
console.log(" " + message);
}
}
process.exit(1);
}
function getStackTrace(skip: number) {
return new Error().stack!.split("\n").slice(skip+2).join("\n");
}
function appendMessage(base: string, message?: string) {
if (message === undefined) {
return base;
}
return `${base}
Message: ${message}`;
}
function buildMessage(actual: any, expected: any, header: string, message?: string) {
return appendMessage(
`${header}
Expected: ${expected}
Actual: ${actual}`,
message,
);
}
export function equal(
actual: any,
expected: any,
{ message, skip=0 }: Options = {},
) {
assert(
expected === actual,
{
message: buildMessage(actual, expected, "Values not equal", message),
skip: skip + 1,
},
);
}
export function equalArrays(
actual: any[],
expected: any[],
{ message, skip=0 }: Options = {},
) {
assert(
actual.length === expected.length && actual.every((item, index) => item === expected[index]),
{
message: buildMessage(actual, expected, "Arrays not equal", message),
skip: skip + 1,
},
);
}
export function between(
actual: any,
min: any,
max: any,
{ message, skip=0 }: Options = {},
) {
assert(
actual >= min && actual <= max,
{
message: buildMessage(actual, `[${min}, ${max}]`, "Value not in range", message),
skip: skip + 1,
},
);
}
export function equalRects(
actual: QmlRect,
expected: QmlRect,
{ message, skip=0 }: Options = {},
) {
assert(
rectEquals(expected, actual),
{
message: buildMessage(actual, expected, "QmlRect not equal", message),
skip: skip + 1,
},
);
}
export function rect(
actual: QmlRect,
x: number,
y: number,
width: number,
height: number,
{ message, skip=0 }: Options = {},
) {
equalRects(
actual,
new MockQmlRect(x, y, width, height),
{ message: message, skip: skip+1 },
);
}
export function grid(
config: Config,
screen: QmlRect,
columnWidth: number,
grid: KwinClient[][],
centered: boolean,
{ message, skip=0 }: Options = {},
) {
const nColumns = grid.length;
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
const columnsWidth = nColumns * columnWidth + (nColumns-1) * config.gapsInnerHorizontal;
const startX = centered ?
screen.x + (screen.width - columnsWidth) / 2 :
grid[0][0].frameGeometry.x;
// assumes uniformly sized windows within columns of uniform width
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows;
return new MockQmlRect(
startX + column * (columnWidth + config.gapsInnerHorizontal),
screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window,
columnWidth,
(columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows,
);
}
for (let iColumn = 0; iColumn < nColumns; iColumn++) {
const column = grid[iColumn];
const nWindows = column.length;
for (let iWindow = 0; iWindow < nWindows; iWindow++) {
const window = column[iWindow];
equalRects(
window.frameGeometry,
getRectInGrid(iColumn, iWindow, nColumns, nWindows),
{ message: appendMessage(`window ${iWindow}, column ${iColumn}`, message), skip: skip+1 },
);
}
}
}
export function centered(
config: Config,
screen: QmlRect,
client:KwinClient,
{ message, skip=0 }: Options = {},
) {
grid(
config,
screen,
client.frameGeometry.width,
[[client]],
true,
{ message: appendMessage("Window not centered", message), skip: skip+1 },
);
}
export function fullyVisible(
rect: QmlRect,
{ message, skip=0 }: Options = {},
) {
assert(
rect.left >= tilingArea.left && rect.right <= tilingArea.right,
{
message: appendMessage(`Rect ${rect} not fully visible`, message),
skip: skip + 1,
},
);
}
export function notFullyVisible(
rect: QmlRect,
{ message, skip=0 }: Options = {},
) {
assert(
rect.left < tilingArea.left || rect.right > tilingArea.right,
{
message: appendMessage(`Rect ${rect} is fully visible, but shouldn't be`, message),
skip: skip + 1,
},
);
}
export function columnsFillTilingArea(
columns: KwinClient[],
{ message, skip=0 }: Options = {},
) {
const options = { message: message, skip: skip+1 };
let x = tilingArea.left;
for (const column of columns) {
const width = column.frameGeometry.width;
fullyVisible(column.frameGeometry, options);
rect(column.frameGeometry, x, tilingArea.top, width, tilingArea.height, options);
x += width + gapH;
}
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options);
}
}

View File

@@ -1,26 +0,0 @@
class TestRunner {
private readonly tests: TestRunner.Test[] = [];
public register(name: string, count: number, f: () => void) {
this.tests.push({ name: name, count: count, f: f });
}
public run() {
for (const test of this.tests) {
console.log("Running test " + test.name);
for (let i = 0; i < test.count; i++) {
test.f();
}
}
}
}
namespace TestRunner {
export type Test = {
name: string,
count: number,
f: () => void,
}
}
const tests = new TestRunner();

View File

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

View File

@@ -1,3 +0,0 @@
declare const process: {
exit(code?: number): void,
};

View File

@@ -1,46 +0,0 @@
let Qt: Qt;
let KWin: KWin;
let Workspace: Workspace;
let qmlBase: QmlObject;
let notificationInvalidWindowRules: Notification;
let notificationInvalidPresetWidths: Notification;
let screen: MockQmlRect;
let tilingArea: MockQmlRect;
let gapH: number;
let gapV: number;
let runLog: string[];
function init(config: Config) {
screen = new MockQmlRect(0, 0, 800, 600);
tilingArea = new MockQmlRect(
config.gapsOuterLeft,
config.gapsOuterTop,
screen.width - config.gapsOuterLeft - config.gapsOuterRight,
screen.height - config.gapsOuterTop - config.gapsOuterBottom,
);
gapH = config.gapsInnerHorizontal;
gapV = config.gapsInnerVertical;
runLog = [];
const qtMock = new MockQt();
const workspaceMock = new MockWorkspace();
Qt = qtMock;
Workspace = workspaceMock;
const world = new World(config);
return { qtMock, workspaceMock, world };
}
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) {
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left;
const left = Math.floor((screen.width - columnsWidth) / 2);
const right = left + columnsWidth;
return { left, right };
}
function getWindowHeight(windowsInColumn: number) {
const totalGaps = (windowsInColumn-1) * gapV;
return Math.round((tilingArea.height - totalGaps) / windowsInColumn);
}

View File

@@ -1,217 +0,0 @@
class MockKwinClient {
public readonly __brand = "KwinClient";
private static readonly borderThickness = 10;
public readonly shadeable: boolean = false;
public caption = "App";
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public readonly transient: boolean;
public readonly move: boolean = false;
public resize: boolean = false;
public readonly moveable: boolean = true;
public readonly resizeable: boolean = true;
public readonly fullScreenable: boolean = true;
public readonly maximizable: boolean = true;
public readonly output: Output = { __brand: "Output" };
public readonly resourceClass = "app";
public readonly dock: boolean = false;
public readonly normalWindow: boolean = true;
public readonly managed: boolean = true;
public readonly popupWindow: boolean = false;
public readonly pid = 1;
private _maximizedVertically: boolean = false;
private _maximizedHorizontally: boolean = false;
private _fullScreen: boolean = false;
public activities: string[] = [];
public skipSwitcher: boolean = false;
public keepAbove: boolean = false;
public keepBelow: boolean = false;
public shade: boolean = false;
private _minimized: boolean = false;
private _desktops: KwinDesktop[] = [];
private _tile: Tile|null = null;
public opacity: number = 1.0;
public readonly fullScreenChanged = new MockQSignal<[]>();
public readonly desktopsChanged = new MockQSignal<[]>();
public readonly activitiesChanged = new MockQSignal<[]>();
public readonly minimizedChanged = new MockQSignal<[]>();
public readonly maximizedAboutToChange = new MockQSignal<[MaximizedMode]>();
public readonly captionChanged = new MockQSignal<[]>();
public readonly tileChanged = new MockQSignal<[]>();
public readonly interactiveMoveResizeStarted = new MockQSignal<[]>();
public readonly interactiveMoveResizeFinished = new MockQSignal<[]>();
public readonly frameGeometryChanged = new MockQSignal<[oldGeometry: QmlRect]>();
private windowedFrameGeometry: MockQmlRect;
private windowed: boolean = true;
private hasBorder: boolean = true;
constructor(
private _frameGeometry: MockQmlRect = new MockQmlRect(10, 10, 100, 200),
public readonly transientFor: MockKwinClient|null = null,
) {
this.windowedFrameGeometry = _frameGeometry.clone();
this.transient = transientFor !== null;
this._desktops = [Workspace.currentDesktop];
}
setMaximize(vertically: boolean, horizontally: boolean) {
this.windowed = !(vertically || horizontally);
if (vertically === this._maximizedVertically && horizontally === this._maximizedHorizontally) {
return;
}
this._maximizedVertically = vertically;
this._maximizedHorizontally = horizontally;
this.maximizedAboutToChange.fire(
vertically ? (
horizontally ? MaximizedMode.Maximized : MaximizedMode.Vertically
) : (
horizontally ? MaximizedMode.Horizontally : MaximizedMode.Unmaximized
)
);
this.frameGeometry = new MockQmlRect(
horizontally ? 0 : this.windowedFrameGeometry.x,
vertically ? 0 : this.windowedFrameGeometry.y,
horizontally ? screen.width : this.windowedFrameGeometry.width,
vertically ? screen.height : this.windowedFrameGeometry.height,
);
}
public get clientGeometry() {
if (this.hasBorder) {
return new MockQmlRect(
this.frameGeometry.x + MockKwinClient.borderThickness,
this.frameGeometry.y + MockKwinClient.borderThickness,
this.frameGeometry.width - 2 * MockKwinClient.borderThickness,
this.frameGeometry.height - 2 * MockKwinClient.borderThickness,
);
} else {
return this.frameGeometry;
}
}
public get fullScreen() {
return this._fullScreen;
}
public set fullScreen(fullScreen: boolean) {
const oldFullScreen = this._fullScreen;
this.hasBorder = !fullScreen;
runReorder(
() => {
this._fullScreen = fullScreen;
if (fullScreen !== oldFullScreen) {
this.fullScreenChanged.fire();
}
},
() => {
if (oldFullScreen && !fullScreen) {
// when switching from full-screen to windowed, Kwin sometimes first adds the frame before changing the frameGeometry to the final value
if (rectEquals(this.frameGeometry, this.windowedFrameGeometry)) {
// already has windowed frame geometry, don't undo that
return;
}
runOneOf(
() => this.frameGeometry = new MockQmlRect(
0,
0,
screen.width + 2 * MockKwinClient.borderThickness,
screen.height + 2 * MockKwinClient.borderThickness,
),
() => this.frameGeometry = new MockQmlRect(
-MockKwinClient.borderThickness,
-MockKwinClient.borderThickness,
screen.width + 2 * MockKwinClient.borderThickness,
screen.height + 2 * MockKwinClient.borderThickness,
),
() => {},
);
}
},
() => {
this.windowed = !fullScreen;
if (fullScreen) {
this.frameGeometry = screen;
} else {
this.frameGeometry = this.windowedFrameGeometry;
}
},
);
}
public get frameGeometry() {
return this._frameGeometry;
}
public set frameGeometry(frameGeometry: MockQmlRect) {
const oldFrameGeometry = this._frameGeometry;
this._frameGeometry = new MockQmlRect(
frameGeometry.x,
frameGeometry.y,
frameGeometry.width,
frameGeometry.height,
this.frameGeometryChanged.fire.bind(this.frameGeometryChanged),
);
if (this.windowed) {
this.windowedFrameGeometry = this._frameGeometry.clone();
}
if (!rectEquals(frameGeometry, oldFrameGeometry)) {
this.frameGeometryChanged.fire(oldFrameGeometry);
}
}
public get minimized() {
return this._minimized;
}
public set minimized(minimized: boolean) {
this._minimized = minimized;
this.minimizedChanged.fire();
}
public get desktops() {
return this._desktops;
}
public set desktops(desktops: KwinDesktop[]) {
this._desktops = desktops;
this.desktopsChanged.fire();
if (Workspace.activeWindow === this && !desktops.includes(Workspace.currentDesktop)) {
Workspace.activeWindow = null;
};
}
public get tile() {
return this._tile;
}
public set tile(tile: Tile|null) {
this._tile = tile;
this.tileChanged.fire();
}
public pin(geometry: MockQmlRect) {
runMaybe(() => this.frameGeometry = geometry);
this.tile = { __brand: "Tile" };
this.frameGeometry = geometry;
}
public unpin() {
this.tile = null;
}
public getFrameGeometryCopy() {
return this._frameGeometry.clone();
}
public toString() {
return `MockKwinClient("${this.caption}")`;
}
}

View File

@@ -1,19 +0,0 @@
class MockQSignal<T extends unknown[]> {
public readonly __brand = "QSignal";
private readonly handlers: Set<(...args: [...T]) => void> = new Set();
public connect(handler: (...args: [...T]) => void) {
this.handlers.add(handler);
};
public disconnect(handler: (...args: [...T]) => void) {
this.handlers.delete(handler);
};
public fire(...args: [...T]) {
for (const handler of this.handlers) {
handler(...args);
}
}
}

View File

@@ -1,8 +0,0 @@
class MockQmlPoint {
public readonly __brand = "QmlPoint";
constructor(
public x: number,
public y: number,
) {}
}

View File

@@ -1,89 +0,0 @@
class MockQmlRect {
public readonly __brand = "QmlRect";
constructor(
private _x: number,
private _y: number,
private _width: number,
private _height: number,
private readonly onChanged: (oldRect: MockQmlRect) => void = () => {},
) {}
public get x() {
return this._x;
}
public set x(x: number) {
const oldRect = this.clone();
this._x = x;
this.onChanged(oldRect);
}
public get y() {
return this._y;
}
public set y(y: number) {
const oldRect = this.clone();
this._y = y;
this.onChanged(oldRect);
}
public get width() {
return this._width;
}
public set width(width: number) {
const oldRect = this.clone();
this._width = width;
this.onChanged(oldRect);
}
public get height() {
return this._height;
}
public set height(height: number) {
const oldRect = this.clone();
this._height = height;
this.onChanged(oldRect);
}
public get top() {
return this.y;
}
public get bottom() {
return this.y + this.height;
}
public get left() {
return this.x;
}
public get right() {
return this.x + this.width;
}
public set(target: QmlRect) {
const oldRect = this.clone();
this._x = target.x;
this._y = target.y;
this._width = target.width;
this._height = target.height;
this.onChanged(oldRect);
}
public clone() {
return new MockQmlRect(
this._x,
this._y,
this._width,
this._height,
);
}
public toString() {
return `(${this.x} ${this.y} ${this.width} ${this.height})`;
}
}

View File

@@ -1,8 +0,0 @@
class MockQmlSize {
public readonly __brand = "QmlSize";
constructor(
public width: number,
public height: number,
) {}
}

View File

@@ -1,13 +0,0 @@
class MockQmlTimer {
public readonly __brand = "QmlObject";
public interval = 0;
public readonly triggered = new MockQSignal<[]>();
public restart() {
// no need to wait in tests, just fire immediately
this.triggered.fire();
};
public destroy() {}
}

View File

@@ -1,47 +0,0 @@
class MockQt {
public readonly __brand = "Qt";
private shortcuts = new Map<string, MockShortcutHandler>();
public point(x: number, y: number) {
return new MockQmlPoint(x, y);
}
public rect(x: number, y: number, width: number, height: number) {
return new MockQmlRect(x, y, width, height);
}
public createQmlObject(qml: string, parent: QmlObject): QmlObject {
if (qml.includes("Timer")) {
return new MockQmlTimer();
} else if (qml.includes("ShortcutHandler")) {
const shortcutName = MockQt.extractShortcutName(qml);
const shortcutHandler = new MockShortcutHandler();
this.shortcuts.set(shortcutName, shortcutHandler);
return shortcutHandler;
} else {
throw new Error("Unexpected qml string: " + qml);
}
}
public fireShortcut(shortcutName: string) {
const shortcutHandler = this.shortcuts.get(shortcutName);
if (shortcutHandler === undefined) {
Assert.assert(false);
return;
}
shortcutHandler.activated.fire();
}
private static extractShortcutName(qml: string) {
const nameLine = qml.split("\n").find((line) => line.trimStart().startsWith("name:"));
if (nameLine === undefined) {
Assert.assert(false);
return "";
}
return nameLine.substring(
nameLine.indexOf('"') + 1,
nameLine.lastIndexOf('"'),
);
}
}

View File

@@ -1,7 +0,0 @@
class MockShortcutHandler {
public readonly __brand = "QmlObject";
public readonly activated: MockQSignal<[]> = new MockQSignal<[]>();
public destroy() {}
}

View File

@@ -1,112 +0,0 @@
class MockWorkspace {
public readonly __brand = "Workspace";
public activities = ["test-activity"];
public desktops: KwinDesktop[] = [
{ __brand: "KwinDesktop", id: "desktop1" },
{ __brand: "KwinDesktop", id: "desktop2" }
];
public currentDesktop = this.desktops[0];
public currentActivity = this.activities[0];
public activeScreen: Output = { __brand: "Output" };
public readonly windows: MockKwinClient[] = [];
public cursorPos = new MockQmlPoint(0, 0);
private _activeWindow: KwinClient|null = null;
public readonly currentDesktopChanged = new MockQSignal<[]>();
public readonly windowAdded = new MockQSignal<[KwinClient]>();
public readonly windowRemoved = new MockQSignal<[KwinClient]>();
public readonly windowActivated = new MockQSignal<[KwinClient|null]>();
public readonly screensChanged = new MockQSignal<[]>();
public readonly activitiesChanged = new MockQSignal<[]>();
public readonly desktopsChanged = new MockQSignal<[]>();
public readonly currentActivityChanged = new MockQSignal<[]>();
public readonly virtualScreenSizeChanged = new MockQSignal<[]>();
public clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop) {
return screen;
}
public createWindows(...kwinClients: MockKwinClient[]) {
for (const kwinClient of kwinClients) {
this.windows.push(kwinClient);
this.windowAdded.fire(kwinClient);
this.activeWindow = kwinClient;
}
}
public createClients(n: number) {
return this.createClientsWithWidths(...Array(n).fill(100));
}
public createClientsWithFrames(...frames: MockQmlRect[]) {
const clients = frames.map(rect => new MockKwinClient(rect));
clients.forEach((client, index) => client.caption = `Client ${index}`);
this.createWindows(...clients);
return clients;
}
public createClientsWithWidths(...widths: number[]) {
return this.createClientsWithFrames(...widths.map(width => new MockQmlRect(randomInt(100), randomInt(100), width, 100+randomInt(400))));
}
public removeWindow(window: MockKwinClient) {
runReorder(
() => this.windows.splice(this.windows.indexOf(window), 1),
() => this.windowRemoved.fire(window),
);
if (window === this.activeWindow) {
const windows = this.windows.filter(w => w.desktops.includes(this.currentDesktop));
Workspace.activeWindow = windows.length > 0 ? randomItem(windows) : null;
};
}
public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) {
const frame = window.getFrameGeometryCopy();
if (edgeResize) {
this.cursorPos = new MockQmlPoint(
leftEdge ? frame.left : frame.right,
topEdge ? frame.top : frame.bottom,
);
} else {
this.cursorPos = new MockQmlPoint(
Math.round(frame.x + frame.width/2),
Math.round(frame.y + frame.height/2),
);
}
window.resize = true;
window.interactiveMoveResizeStarted.fire();
for (const delta of deltas) {
if (delta.width !== 0) {
frame.width += delta.width;
if (leftEdge) {
frame.x -= delta.width;
}
}
if (delta.height !== 0) {
frame.height += delta.height;
if (topEdge) {
frame.y -= delta.height;
}
}
runOneOf(
() => window.frameGeometry.set(frame),
() => window.frameGeometry = frame,
)
}
window.resize = false;
window.interactiveMoveResizeFinished.fire();
}
public get activeWindow() {
return this._activeWindow;
}
public set activeWindow(activeWindow: KwinClient|null) {
this._activeWindow = activeWindow;
this.windowActivated.fire(activeWindow);
}
}

View File

@@ -1,45 +0,0 @@
function runMaybe(f: () => void) {
if (Math.random() < 0.5) {
f();
}
}
function runOneOf(...fs: (() => void)[]) {
const index = randomInt(fs.length);
runLog.push(`${getStackFrame(1)} - Chose ${index}`);
fs[index]();
}
function runReorder(...fs: (() => void)[]) {
const fis = fs.map((f, index) => ({ f: f, index: index }));
shuffle(fis);
const indexes = fis.map((fi) => fi.index);
runLog.push(`${getStackFrame(1)} - Order ${indexes}`);
for (const fi of fis) {
fi.f();
}
}
function randomInt(n: number) {
return Math.floor(Math.random() * n);
}
function randomItem(items: any[]) {
Assert.assert(items.length > 0);
const index = randomInt(items.length);
return items[index];
}
function shuffle(items: any[]) {
for (let n = items.length; n > 1; n--) {
const i = n-1;
const j = randomInt(n);
[items[i], items[j]] = [items[j], items[i]];
}
}
function getStackFrame(index: number) {
return new Error().stack!.split("\n")[index+2].substring(7);
}

View File

@@ -1,13 +0,0 @@
function timeControl(f: (addTime: (ms: number) => void) => void) {
const originalDateNow = Date.now;
let currentTime = Date.now();
Date.now = () => currentTime;
function addTime(ms: number) {
currentTime += ms;
}
f(addTime);
Date.now = originalDateNow;
}