4 Commits
v0.10 ... kwin5

Author SHA1 Message Date
Peter Fajdiga
21eacd7ba4 Makefile: package: rename package dir to karousel 2024-10-27 14:44:35 +01:00
Peter Fajdiga
5004417285 bump package to 0.7.2 2024-10-27 13:51:52 +01:00
Peter Fajdiga
a110aee7ce ClientWrapper: implement workaround for Qt5 JS bug 2024-09-10 23:33:56 +02:00
Peter Fajdiga
817ea64171 DesktopManager: fix getDesktopForClient 2024-08-30 12:14:40 +02:00
123 changed files with 1749 additions and 4233 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:

1
.gitignore vendored
View File

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

View File

@@ -1,32 +1,32 @@
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
TESTS := true
.PHONY: *
build: tests
tsc -p ./src/main --outFile ./package/contents/code/main.js
TSC_SCRIPT_FLAGS = --lib es2020 ./src/extern/qt.d.ts
VERSION = $(shell grep '"Version":' ./package/metadata.json | grep -o '[0-9\.]*')
config:
mkdir -p ./package/contents/config
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
tsc ${TSC_SCRIPT_FLAGS} ./src/config/definition.ts ./generators/config/kcfg.ts --outFile /dev/stdout | node - > ./package/contents/config/main.xml
tests:
ifeq (${TESTS}, true)
./run-ts.sh ./src/tests
endif
build:
tsc --outFile ./package/contents/code/main.js
install: build
kpackagetool6 --type=KWin/Script --install=./package || kpackagetool6 --type=KWin/Script --upgrade=./package
install: build config
kpackagetool5 --type=KWin/Script -i ./package || kpackagetool5 --type=KWin/Script -u ./package
uninstall:
kpackagetool6 --type=KWin/Script --remove=karousel
kpackagetool5 --type=KWin/Script -r ./package
package: build
package: build config
tar -czf ./karousel_${subst .,_,${VERSION}}.tar.gz ./package --transform s/package/karousel/
docs-key-bindings-bbcode:
@./run-ts.sh ./src/generators/docs/keyBindingsBbcode
logs:
journalctl -t kwin_x11 -g '^qml:|^file://.*karousel' -f
docs-key-bindings-markdown:
@./run-ts.sh ./src/generators/docs/keyBindingsMarkdown
docs-key-bindings-bbcode:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsBbcode.ts --outFile /dev/stdout | node -
docs-key-bindings-table:
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsTable.ts --outFile /dev/stdout | node -
docs-key-bindings-fmt:
@./run-ts.sh ./src/generators/docs/keyBindingsFmt
@tsc ${TSC_SCRIPT_FLAGS} ./src/keyBindings/definition.ts ./generators/docs/keyBindings.ts ./generators/docs/keyBindingsFmt.ts --outFile /dev/stdout | node -

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
@@ -13,7 +19,7 @@ Similar window managers include [PaperWM](https://github.com/paperwm/PaperWM),
## Dependencies
Karousel requires the following QML modules:
- QtQuick 6.0
- QtQuick 2.15
- org.kde.kwin 3.0
- org.kde.notification 1.0
@@ -55,7 +61,6 @@ Here's the default ones:
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end |
| Meta+Ctrl+Return | Move Karousel grid to the current screen |
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |

View File

@@ -1,11 +1,22 @@
type DocsKeyBinding = {
type KeyBinding = {
name: string;
description: string;
keySequence: string;
};
comment?: string;
defaultKeySequence: string;
action: string;
}
function formatDescription(item: {description: string, comment?: string}) {
const suffix = item.comment === undefined ? "" : ` (${item.comment})`;
return `${applyMacro(item.description, "N")}${suffix}`;
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: string;
}
function formatComment(comment: string | undefined) {
return comment === undefined ? "" : ` (${comment})`;
}
function printCols(...columns: (string[] | string)[]) {
@@ -54,15 +65,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

@@ -0,0 +1,12 @@
console.log(`[list]`);
for (const binding of keyBindings) {
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

@@ -0,0 +1,14 @@
const colLeft = [
...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: KeyBinding) => `${binding.description}${formatComment(binding.comment)}`),
...numKeyBindings.map((binding: NumKeyBinding) => `${binding.description}N${formatComment(binding.comment)}`),
];
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,7 @@
import QtQuick 6.0
import QtQuick 2.15
import org.kde.kwin 3.0
import org.kde.notification 1.0
import "../code/main.js" as Karousel
import "./main.js" as Karousel
Item {
id: qmlBase
@@ -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

@@ -121,13 +121,6 @@
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_noLayering">
<property name="text">
<string>No layering</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -301,19 +294,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

@@ -1,5 +1,4 @@
{
"KPackageStructure": "KWin/Script",
"KPlugin": {
"Name": "Karousel",
"Description": "Scrollable tiling extension for KWin",
@@ -9,13 +8,13 @@
"Name": "Peter Fajdiga"
}],
"Id": "karousel",
"Version": "0.10",
"ServiceTypes": ["KWin/Script"],
"Version": "0.7.2",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"
},
"X-Plasma-API": "declarativescript",
"X-Plasma-API-Minimum-Version": "6.0",
"X-Plasma-MainScript": "ui/main.qml",
"X-Plasma-MainScript": "code/main.qml",
"X-KDE-ConfigModule": "kwin/effects/configs/kcm_kwin4_genericscripted"
}

View File

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

336
src/Actions.ts Normal file
View File

@@ -0,0 +1,336 @@
namespace Actions {
export function init(world: World, config: Config) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
prevColumn.focus();
});
},
focusRight: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
nextColumn.focus();
});
},
focusUp: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
}
prevWindow.focus();
});
},
focusDown: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
}
nextWindow.focus();
});
},
focusStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
},
focusEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
},
windowMoveLeft: () => {
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);
}
});
},
windowMoveRight: () => {
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);
}
});
},
windowMoveUp: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowUp(window);
});
},
windowMoveDown: () => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveWindowDown(window);
});
},
windowMoveStart: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
});
},
windowMoveEnd: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
});
},
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
world.do((clientManager, desktopManager) => {
clientManager.toggleFloatingClient(kwinClient);
});
},
columnMoveLeft: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnLeft(column);
});
},
columnMoveRight: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.moveColumnRight(column);
});
},
columnMoveStart: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(null);
});
},
columnMoveEnd: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
});
},
columnToggleStacked: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
column.toggleStacked();
});
},
columnWidthIncrease: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.increaseWidth(column, config.manualResizeStep);
});
},
columnWidthDecrease: () => {
world.doIfTiledFocused(false, (clientManager, desktopManager, window, column, grid) => {
config.columnResizer.decreaseWidth(column, config.manualResizeStep);
});
},
columnsWidthEqualize: () => {
world.do((clientManager, desktopManager) => {
desktopManager.getCurrentDesktop().equalizeVisibleColumnsWidths();
});
},
gridScrollLeft: () => {
gridScroll(world, -config.manualScrollStep);
},
gridScrollRight: () => {
gridScroll(world, config.manualScrollStep);
},
gridScrollStart: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
});
},
gridScrollEnd: () => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
});
},
gridScrollFocused: () => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
grid.desktop.scrollCenterRange(column);
})
},
gridScrollLeftColumn: () => {
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);
});
},
gridScrollRightColumn: () => {
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);
});
},
};
}
export function initNum(world: World) {
return {
focusColumn: (columnIndex: number) => {
world.do((clientManager, desktopManager) => {
const grid = desktopManager.getCurrentDesktop().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
}
targetColumn.focus();
});
},
windowMoveToColumn: (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();
});
},
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return;
}
if (targetColumn.isAfter(column)) {
column.moveAfter(targetColumn);
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
});
},
columnMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
});
},
tailMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (clientManager, desktopManager, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = desktopManager.getDesktopInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
});
},
};
}
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,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(!column.isVisible(visibleRange, true) || 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(!column.isVisible(visibleRange, true) || 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

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

22
src/config/config.ts Normal file
View File

@@ -0,0 +1,22 @@
type Config = {
gapsOuterTop: number,
gapsOuterBottom: number,
gapsOuterLeft: number,
gapsOuterRight: number,
gapsInnerHorizontal: number,
gapsInnerVertical: number,
manualScrollStep: number,
manualResizeStep: number,
offScreenOpacity: number,
untileOnDrag: boolean,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
skipSwitcher: boolean,
scrollingLazy: boolean,
scrollingCentered: boolean,
scrollingGrouped: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string,
};

View File

@@ -1,35 +1,46 @@
const defaultWindowRules = `[
{
"class": "(org\\\\.kde\\\\.)?plasmashell",
"class": "ksmserver-logout-greeter",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kded6",
"class": "kcalc",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kcalc",
"class": "org.kde.kcalc",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kfind",
"class": "kfind",
"tile": true
},
{
"class": "(org\\\\.kde\\\\.)?kruler",
"class": "org.kde.kfind",
"tile": true
},
{
"class": "kruler",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?krunner",
"class": "org.kde.kruler",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?yakuake",
"class": "krunner",
"tile": false
},
{
"class": "steam",
"caption": "Steam Big Picture Mode",
"class": "org.kde.krunner",
"tile": false
},
{
"class": "yakuake",
"tile": false
},
{
"class": "org.kde.yakuake",
"tile": false
},
{
@@ -38,12 +49,22 @@ const defaultWindowRules = `[
"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
}
@@ -53,32 +74,32 @@ const configDef = [
{
name: "gapsOuterTop",
type: "UInt",
default: 16,
default: 18,
},
{
name: "gapsOuterBottom",
type: "UInt",
default: 16,
default: 18,
},
{
name: "gapsOuterLeft",
type: "UInt",
default: 16,
default: 18,
},
{
name: "gapsOuterRight",
type: "UInt",
default: 16,
default: 18,
},
{
name: "gapsInnerHorizontal",
type: "UInt",
default: 8,
default: 18,
},
{
name: "gapsInnerVertical",
type: "UInt",
default: 8,
default: 18,
},
{
name: "manualScrollStep",
@@ -86,9 +107,9 @@ const configDef = [
default: 200,
},
{
name: "presetWidths",
type: "String",
default: "50%, 100%",
name: "manualResizeStep",
type: "UInt",
default: 600,
},
{
name: "offScreenOpacity",
@@ -145,11 +166,6 @@ const configDef = [
type: "Bool",
default: false,
},
{
name: "noLayering",
type: "Bool",
default: false,
},
{
name: "windowRules",
type: "String",

View File

@@ -1,7 +1,3 @@
function init() {
return new World(loadConfig());
}
function loadConfig(): Config {
const config: any = {};
for (const entry of configDef) {

View File

@@ -1,6 +1,6 @@
declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidWindowRules: Notification;
declare const notificationInvalidPresetWidths: Notification;
type Notification = {
sendEvent(): void;
};

79
src/extern/kwin.d.ts vendored Normal file
View File

@@ -0,0 +1,79 @@
declare const KWin: {
readConfig(key: string, defaultValue: any): any;
registerShortcut(name: string, description: string, keySequence: string, callback: () => void): void;
};
declare const workspace: {
readonly desktops: number;
readonly currentDesktop: number;
readonly currentActivity: string;
activeClient: KwinClient;
readonly currentDesktopChanged: QSignal<[]>
readonly clientAdded: QSignal<[KwinClient]>;
readonly clientRemoved: QSignal<[KwinClient]>;
readonly clientMinimized: QSignal<[KwinClient]>;
readonly clientUnminimized: QSignal<[KwinClient]>;
readonly clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
readonly clientActivated: QSignal<[KwinClient]>;
readonly numberDesktopsChanged: QSignal<[]>;
readonly currentActivityChanged: QSignal<[]>;
readonly virtualScreenSizeChanged: QSignal<[]>;
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
clientList(): KwinClient[];
};
const enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}
type Tile = unknown;
interface KwinClient {
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: QmlSize;
readonly transient: boolean;
readonly transientFor: KwinClient;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
readonly screen: number;
readonly resourceClass: QByteArray;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
opacity: number;
fullScreen: boolean;
activities: string[]; // empty array means all activities
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktop: number; // -1 means all desktops
tile: Tile;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly moveResizedChanged: QSignal<[]>;
readonly moveResizeCursorChanged: QSignal<[]>;
readonly clientStartUserMovedResized: QSignal<[]>;
readonly frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
}

42
src/extern/qt.d.ts vendored Normal file
View File

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

View File

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

View File

@@ -1,7 +0,0 @@
console.log(`[list]`);
for (const binding of keyBindings) {
console.log(` [*] ${binding.keySequence}${binding.description}`);
}
console.log(`[/list]`);

View File

@@ -1,9 +0,0 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../extern/**/*",
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

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

View File

@@ -1,9 +0,0 @@
{
"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

@@ -1,9 +0,0 @@
{
"extends": "../../../tsconfig.json",
"include": [
"../../../extern/**/*",
"../../../lib/**/*",
"../keyBindings.ts",
"./**/*"
]
}

View File

@@ -0,0 +1,218 @@
const keyBindings: KeyBinding[] = [
{
name: "window-toggle-floating",
description: "Toggle floating",
defaultKeySequence: "Meta+Space",
action: "windowToggleFloating",
},
{
name: "focus-left",
description: "Move focus left",
defaultKeySequence: "Meta+A",
action: "focusLeft",
},
{
name: "focus-right",
description: "Move focus right",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+D",
action: "focusRight",
},
{
name: "focus-up",
description: "Move focus up",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+W",
action: "focusUp",
},
{
name: "focus-down",
description: "Move focus down",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+S",
action: "focusDown",
},
{
name: "focus-start",
description: "Move focus to start",
defaultKeySequence: "Meta+Home",
action: "focusStart",
},
{
name: "focus-end",
description: "Move focus to end",
defaultKeySequence: "Meta+End",
action: "focusEnd",
},
{
name: "window-move-left",
description: "Move window left",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+A",
action: "windowMoveLeft",
},
{
name: "window-move-right",
description: "Move window right",
comment: "Moves window out of and into columns",
defaultKeySequence: "Meta+Shift+D",
action: "windowMoveRight",
},
{
name: "window-move-up",
description: "Move window up",
defaultKeySequence: "Meta+Shift+W",
action: "windowMoveUp",
},
{
name: "window-move-down",
description: "Move window down",
defaultKeySequence: "Meta+Shift+S",
action: "windowMoveDown",
},
{
name: "window-move-start",
description: "Move window to start",
defaultKeySequence: "Meta+Shift+Home",
action: "windowMoveStart",
},
{
name: "window-move-end",
description: "Move window to end",
defaultKeySequence: "Meta+Shift+End",
action: "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: "columnToggleStacked",
},
{
name: "column-move-left",
description: "Move column left",
defaultKeySequence: "Meta+Ctrl+Shift+A",
action: "columnMoveLeft",
},
{
name: "column-move-right",
description: "Move column right",
defaultKeySequence: "Meta+Ctrl+Shift+D",
action: "columnMoveRight",
},
{
name: "column-move-start",
description: "Move column to start",
defaultKeySequence: "Meta+Ctrl+Shift+Home",
action: "columnMoveStart",
},
{
name: "column-move-end",
description: "Move column to end",
defaultKeySequence: "Meta+Ctrl+Shift+End",
action: "columnMoveEnd",
},
{
name: "column-width-increase",
description: "Increase column width",
defaultKeySequence: "Meta+Ctrl++",
action: "columnWidthIncrease",
},
{
name: "column-width-decrease",
description: "Decrease column width",
defaultKeySequence: "Meta+Ctrl+-",
action: "columnWidthDecrease",
},
{
name: "columns-width-equalize",
description: "Equalize widths of visible columns",
defaultKeySequence: "Meta+Ctrl+X",
action: "columnsWidthEqualize",
},
{
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: "gridScrollFocused",
},
{
name: "grid-scroll-left-column",
description: "Scroll one column to the left",
defaultKeySequence: "Meta+Alt+A",
action: "gridScrollLeftColumn",
},
{
name: "grid-scroll-right-column",
description: "Scroll one column to the right",
defaultKeySequence: "Meta+Alt+D",
action: "gridScrollRightColumn",
},
{
name: "grid-scroll-left",
description: "Scroll left",
defaultKeySequence: "Meta+Alt+PgUp",
action: "gridScrollLeft",
},
{
name: "grid-scroll-right",
description: "Scroll right",
defaultKeySequence: "Meta+Alt+PgDown",
action: "gridScrollRight",
},
{
name: "grid-scroll-start",
description: "Scroll to start",
defaultKeySequence: "Meta+Alt+Home",
action: "gridScrollStart",
},
{
name: "grid-scroll-end",
description: "Scroll to end",
defaultKeySequence: "Meta+Alt+End",
action: "gridScrollEnd",
},
];
const numKeyBindings: NumKeyBinding[] = [
{
name: "focus-",
description: "Move focus to column ",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultModifiers: "Meta",
fKeys: false,
action: "focusColumn",
},
{
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: "windowMoveToColumn",
},
{
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: "columnMoveToColumn",
},
{
name: "column-move-to-desktop-",
description: "Move column to desktop ",
defaultModifiers: "Meta+Ctrl+Shift",
fKeys: true,
action: "columnMoveToDesktop",
},
{
name: "tail-move-to-desktop-",
description: "Move this and all following columns to desktop ",
defaultModifiers: "Meta+Ctrl+Shift+Alt",
fKeys: true,
action: "tailMoveToDesktop",
},
];

70
src/keyBindings/loader.ts Normal file
View File

@@ -0,0 +1,70 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence: string;
action: keyof ReturnType<typeof Actions.init>;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: keyof ReturnType<typeof Actions.initNum>;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(name: string, description: string, keySequence: string, callback: () => void) {
KWin.registerShortcut(
"karousel-" + name,
"Karousel: " + description,
keySequence,
catchWrap(callback),
);
}
function registerNumKeyBindings(name: string, description: string, modifiers: string, fKeys: boolean, callback: (i: number) => void) {
const numPrefix = fKeys ? "F" : "";
const n = fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
const numKey = String(i + 1);
const keySequence = i < n ?
modifiers + "+" + numPrefix + numKey :
"";
registerKeyBinding(
name + numKey,
description + numKey,
keySequence,
() => callback(i),
);
}
}
function registerKeyBindings(world: World, config: Config) {
const actions = Actions.init(world, {
manualScrollStep: config.manualScrollStep,
manualResizeStep: config.manualResizeStep,
columnResizer: config.scrollingCentered ? new RawResizer() : new ContextualResizer(),
});
for (const binding of keyBindings) {
registerKeyBinding(binding.name, binding.description, binding.defaultKeySequence, actions[binding.action]);
}
const numActions = Actions.initNum(world);
for (const binding of numKeyBindings) {
registerNumKeyBindings(binding.name, binding.description, binding.defaultModifiers, binding.fKeys, numActions[binding.action]);
}
}

View File

@@ -5,39 +5,46 @@ 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.onColumnMoved(this, prevColumn);
} else {
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];
window.client.kwinClient.desktop = targetGrid.desktop.desktopNumber;
}
}
}
public isToTheLeftOf(other: Column) {
return this.gridX < other.gridX;
public moveAfter(prevColumn: Column|null) {
if (prevColumn === this) {
return;
}
this.grid.onColumnMoved(this, prevColumn);
}
public isToTheRightOf(other: Column) {
public isAfter(other: Column) {
return this.gridX > other.gridX;
}
public isBefore(other: Column) {
return this.gridX < other.gridX;
}
public moveWindowUp(window: Window) {
this.windows.moveBack(window);
this.grid.desktop.onLayoutChanged();
@@ -56,19 +63,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 +132,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) {
@@ -277,13 +255,8 @@ class Column {
}
}
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);
} else {
this.windows.insertStart(window);
}
public onWindowAdded(window: Window) {
this.windows.insertEnd(window);
if (this.width === 0) {
this.setWidth(window.client.preferredWidth, false);
}
@@ -300,7 +273,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,8 @@
class Desktop {
public readonly grid: Grid;
public readonly desktopNumber: number;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyScroll: boolean;
@@ -7,29 +10,26 @@ 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(desktopNumber: number, 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.desktopNumber = desktopNumber;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(this.getScreen(), kwinDesktop);
this.tilingArea = Desktop.getTilingArea(this.clientArea, kwinDesktop, pinManager, config);
this.clientArea = Desktop.getClientArea(desktopNumber);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.getScreen(), this.kwinDesktop);
if (rectEquals(newClientArea, this.clientArea) && !this.dirtyPins) {
const newClientArea = Desktop.getClientArea(this.desktopNumber);
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.kwinDesktop, this.pinManager, this.config);
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
this.dirty = true;
this.dirtyScroll = true;
this.dirtyPins = false;
@@ -37,12 +37,12 @@ class Desktop {
this.autoAdjustScroll();
}
private static getClientArea(screen: Output, kwinDesktop: KwinDesktop) {
return Workspace.clientArea(ClientAreaOption.PlacementArea, screen, kwinDesktop);
private static getClientArea(desktopNumber: number) {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
}
private static getTilingArea(clientArea: QmlRect, kwinDesktop: KwinDesktop, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(kwinDesktop, clientArea);
private static getTilingArea(clientArea: QmlRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
@@ -75,7 +75,7 @@ class Desktop {
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) {
@@ -125,6 +125,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();
@@ -153,19 +187,19 @@ 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;
@@ -217,8 +251,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;
@@ -235,10 +269,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();
}
@@ -269,9 +303,9 @@ namespace Desktop {
export type Scroller = {
scrollToColumn(desktop: Desktop, column: Column): void;
};
}
export type Clamper = {
clampScrollX(desktop: Desktop, x: number): number;
};
}
}

View File

@@ -24,18 +24,6 @@ class Grid {
});
}
public moveColumn(column: Column, leftColumn: Column|null) {
if (column === leftColumn) {
return;
}
const movedLeft = leftColumn === null ? true : column.isToTheRightOf(leftColumn);
const firstMovedColumn = movedLeft ? column : this.getRightColumn(column);
this.columns.move(column, leftColumn);
this.columnsSetX(firstMovedColumn);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public moveColumnLeft(column: Column) {
this.columns.moveBack(column);
this.columnsSetX(column);
@@ -44,11 +32,11 @@ class Grid {
}
public moveColumnRight(column: Column) {
const rightColumn = this.columns.getNext(column);
if (rightColumn === null) {
const nextColumn = this.columns.getNext(column);
if (nextColumn === null) {
return;
}
this.moveColumnLeft(rightColumn);
this.moveColumnLeft(nextColumn);
}
public getWidth() {
@@ -59,11 +47,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);
}
@@ -162,11 +150,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();
@@ -175,14 +163,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) {
@@ -192,9 +180,18 @@ class Grid {
}
}
public onColumnMoved(column: Column, prevColumn: Column|null) {
const movedLeft = prevColumn === null ? true : column.isAfter(prevColumn);
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
this.columnsSetX(firstMovedColumn);
this.desktop.onLayoutChanged();
this.desktop.autoAdjustScroll();
}
public onColumnWidthChanged(column: Column) {
const rightColumn = this.columns.getNext(column);
this.columnsSetX(rightColumn);
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
this.desktop.onLayoutChanged();
if (!this.userResize) {
this.desktop.autoAdjustScroll();
@@ -203,7 +200,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

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

View File

@@ -10,20 +10,21 @@ class Window {
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedMode: MaximizedMode.Unmaximized,
maximizedHorizontally: false,
maximizedVertically: false,
};
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, false);
this.column = targetColumn;
targetColumn.onWindowAdded(this, bottom);
targetColumn.onWindowAdded(this);
}
public arrange(x: number, y: number, width: number, height: number) {
@@ -36,11 +37,8 @@ class Window {
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedMode !== MaximizedMode.Unmaximized) {
this.client.setMaximize(
this.focusedState.maximizedMode === MaximizedMode.Horizontally || this.focusedState.maximizedMode === MaximizedMode.Maximized,
this.focusedState.maximizedMode === MaximizedMode.Vertically || this.focusedState.maximizedMode === MaximizedMode.Maximized,
);
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
maximized = true;
}
if (this.focusedState.fullScreen) {
@@ -78,8 +76,8 @@ class Window {
this.column.grid.desktop.onLayoutChanged();
}
public onMaximizedChanged(maximizedMode: MaximizedMode) {
const maximized = maximizedMode !== MaximizedMode.Unmaximized;
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
const maximized = horizontally || vertically;
this.skipArrange = maximized;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
@@ -88,7 +86,8 @@ class Window {
this.client.kwinClient.keepAbove = maximized;
}
if (this.isFocused()) {
this.focusedState.maximizedMode = maximizedMode;
this.focusedState.maximizedHorizontally = horizontally;
this.focusedState.maximizedVertically = vertically;
}
this.column.grid.desktop.onLayoutChanged();
}
@@ -107,6 +106,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);
@@ -120,7 +144,8 @@ class Window {
namespace Window {
export type State = {
fullScreen: boolean;
maximizedMode: MaximizedMode;
};
fullScreen: boolean,
maximizedHorizontally: boolean,
maximizedVertically: boolean,
}
}

View File

@@ -1,56 +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 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,31 +0,0 @@
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 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);
}
}

View File

@@ -1,22 +0,0 @@
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;
};

View File

@@ -1 +0,0 @@
declare const console: Console;

112
src/lib/extern/kwin.ts vendored
View File

@@ -1,112 +0,0 @@
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 windows: KwinClient[];
readonly cursorPos: Readonly<QmlPoint>;
activeWindow: KwinClient|null;
readonly currentDesktopChanged: QSignal<[]>;
readonly windowAdded: QSignal<[KwinClient]>;
readonly windowRemoved: QSignal<[KwinClient]>;
readonly windowActivated: QSignal<[KwinClient|null]>;
readonly screensChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly currentActivityChanged: QSignal<[]>;
readonly virtualScreenSizeChanged: QSignal<[]>;
clientArea(option: ClientAreaOption, output: Output, kwinDesktop: KwinDesktop): QmlRect;
};
const enum ClientAreaOption {
PlacementArea,
MovementArea,
MaximizeArea,
MaximizeFullArea,
FullScreenArea,
WorkArea,
FullArea,
ScreenArea,
}
const enum MaximizedMode {
Unmaximized,
Vertically,
Horizontally,
Maximized,
}
type Tile = { __brand: "Tile" };
type Output = { __brand: "Output" };
type KwinClient = {
__brand: "KwinClient";
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
readonly transientFor: KwinClient | null;
readonly clientGeometry: Readonly<QmlRect>;
readonly move: boolean;
readonly resize: boolean;
readonly moveable: boolean;
readonly resizeable: boolean;
readonly fullScreenable: boolean;
readonly maximizable: boolean;
readonly output: Output;
readonly resourceClass: string;
readonly dock: boolean;
readonly normalWindow: boolean;
readonly managed: boolean;
readonly popupWindow: boolean;
readonly pid: number;
fullScreen: boolean;
activities: string[]; // empty array means all activities
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops
tile: Tile|null;
opacity: number;
readonly fullScreenChanged: QSignal<[]>;
readonly desktopsChanged: QSignal<[]>;
readonly activitiesChanged: QSignal<[]>;
readonly minimizedChanged: QSignal<[]>;
readonly maximizedAboutToChange: QSignal<[MaximizedMode]>;
readonly captionChanged: QSignal<[]>;
readonly tileChanged: QSignal<[]>;
readonly interactiveMoveResizeStarted: QSignal<[]>;
readonly interactiveMoveResizeFinished: QSignal<[]>;
readonly frameGeometryChanged: QSignal<[oldGeometry: QmlRect]>;
setMaximize(vertically: boolean, horizontally: boolean): void;
};
type KwinDesktop = {
__brand: "KwinDesktop";
readonly id: string;
};
type ShortcutHandler = QmlObject & {
readonly activated: QSignal<[]>;
destroy(): void;
};

View File

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

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

@@ -1,56 +0,0 @@
type Console = {
__brand: "Console";
log(...args: any[]): 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 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
};
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 & {
interval: number;
readonly triggered: QSignal<[]>;
restart(): void;
destroy(): void;
};

View File

@@ -1,421 +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 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(Desktop.RangeImpl.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 (!focusedColumn.isVisible(visibleRange, true)) {
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 (!focusedColumn.isVisible(visibleRange, true)) {
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(Desktop.RangeImpl.fromRanges(firstColumn, lastColumn));
return true;
}
public readonly gridScrollLeft = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, -this.config.manualScrollStep);
}
public readonly gridScrollRight = (cm: ClientManager, dm: DesktopManager) => {
this.gridScroll(dm, this.config.manualScrollStep);
}
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
const grid = desktopManager.getCurrentDesktop().grid;
grid.desktop.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 };
columnResizer: ColumnResizer;
};
export type ColumnResizer = {
increaseWidth(column: Column): void;
decreaseWidth(column: Column): void;
};
}

View File

@@ -1,267 +0,0 @@
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: "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),
},
];
}
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)),
},
];
}

View File

@@ -1,68 +0,0 @@
type KeyBinding = {
name: string;
description: string;
comment?: string;
defaultKeySequence?: string;
action: () => void;
};
type NumKeyBinding = {
name: string;
description: string;
comment?: string;
defaultModifiers: string;
fKeys: boolean;
action: (i: number) => void;
};
function catchWrap(f: () => void) {
return () => {
try {
f();
} catch (error: any) {
log(error);
log(error.stack);
}
};
}
function registerKeyBinding(shortcutActions: ShortcutAction[], keyBinding: KeyBinding) {
shortcutActions.push(new ShortcutAction(
keyBinding,
catchWrap(keyBinding.action),
));
}
function registerNumKeyBindings(shortcutActions: ShortcutAction[], numKeyBinding: NumKeyBinding) {
const numPrefix = numKeyBinding.fKeys ? "F" : "";
const n = numKeyBinding.fKeys ? 12 : 9;
for (let i = 0; i < 12; i++) {
const numKey = String(i + 1);
const keySequence = i < n ?
numKeyBinding.defaultModifiers + "+" + numPrefix + numKey :
"";
shortcutActions.push(new ShortcutAction(
{
name: applyMacro(numKeyBinding.name, numKey),
description: applyMacro(numKeyBinding.description, numKey),
defaultKeySequence: keySequence,
},
catchWrap(() => numKeyBinding.action(i)),
));
}
}
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 numKeyBinding of getNumKeyBindings(world, actions)) {
registerNumKeyBindings(shortcutActions, numKeyBinding);
}
return shortcutActions;
}

View File

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

View File

@@ -1,19 +0,0 @@
class ClientMatcher {
private readonly regex: RegExp;
constructor(regex: RegExp) {
this.regex = regex;
}
public matches(kwinClient: KwinClient) {
return this.regex.test(ClientMatcher.getClientString(kwinClient));
}
public static getClientString(kwinClient: KwinClient) {
return ClientMatcher.getRuleString(kwinClient.resourceClass, kwinClient.caption);
}
public static getRuleString(ruleClass: string, ruleCaption: string) {
return ruleClass + "\0" + ruleCaption;
}
}

View File

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

View File

@@ -1,93 +0,0 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: RegExp;
constructor(windowRules: WindowRule[]) {
const [floatRegex, tileRegex, followCaptionRegex] = WindowRuleEnforcer.createWindowRuleRegexes(windowRules);
this.preferFloating = new ClientMatcher(floatRegex);
this.preferTiling = new ClientMatcher(tileRegex);
this.followCaption = followCaptionRegex;
}
public shouldTile(kwinClient: KwinClient) {
return this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!this.preferFloating.matches(kwinClient)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.test(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = Clients.canTileNow(kwinClient) && enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
clientManager.tileKwinClient(kwinClient, desktop.grid);
} else {
clientManager.floatKwinClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleRegexes(windowRules: WindowRule[]) {
const floatRegexes: string[] = [];
const tileRegexes: string[] = [];
const followCaptionRegexes: string[] = [];
for (const windowRule of windowRules) {
const ruleClass = WindowRuleEnforcer.parseRegex(windowRule.class);
const ruleCaption = WindowRuleEnforcer.parseRegex(windowRule.caption);
const ruleString = ClientMatcher.getRuleString(
WindowRuleEnforcer.wrapParens(ruleClass),
WindowRuleEnforcer.wrapParens(ruleCaption)
);
(windowRule.tile ? tileRegexes : floatRegexes).push(ruleString);
if (ruleCaption !== ".*") {
followCaptionRegexes.push(ruleClass);
}
}
return [
WindowRuleEnforcer.joinRegexes(floatRegexes),
WindowRuleEnforcer.joinRegexes(tileRegexes),
WindowRuleEnforcer.joinRegexes(followCaptionRegexes),
];
}
private static parseRegex(rawRule: string | undefined) {
if (rawRule === undefined || rawRule === "" || rawRule === ".*") {
return ".*";
} else {
return rawRule;
}
}
private static joinRegexes(regexes: string[]) {
if (regexes.length === 0) {
return new RegExp("a^"); // match nothing
}
if (regexes.length === 1) {
return new RegExp("^(" + regexes[0] + ")$");
}
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^(" + joinedRegexes + ")$");
}
private static wrapParens(str: string) {
return "(" + str + ")";
}
}

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,37 +0,0 @@
class ShortcutAction {
private readonly shortcutHandler: ShortcutHandler;
constructor(keyBinding: ShortcutAction.KeyBinding, f: () => void) {
this.shortcutHandler = ShortcutAction.initShortcutHandler(keyBinding);
this.shortcutHandler.activated.connect(f);
}
public destroy() {
this.shortcutHandler.destroy();
}
private static initShortcutHandler(keyBinding: ShortcutAction.KeyBinding) {
const sequenceLine = keyBinding.defaultKeySequence !== undefined ?
` sequence: "${keyBinding.defaultKeySequence}";
` :
"";
return <ShortcutHandler>Qt.createQmlObject(
`import QtQuick 6.0
import org.kde.kwin 3.0
ShortcutHandler {
name: "karousel-${keyBinding.name}";
text: "Karousel: ${keyBinding.description}";
${sequenceLine}}`,
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 landmarks = buildLandmarks(items);
if (landmarks.length === 1) {
return [{
start: landmarks[0].value,
end: landmarks[0].value,
n: items.length,
}];
}
const ranges: Range[] = [];
let n = 0;
for (let i = 1; i < landmarks.length; i++) {
const startLandmark = landmarks[i-1];
const endLandmark = landmarks[i];
n = n - startLandmark.nMax + startLandmark.nMin;
ranges.push({
start: startLandmark.value,
end: endLandmark.value,
n: n,
});
}
return ranges;
}
function buildLandmarks(items: { min: number, max: number }[]) {
const landmarks = new Map<number, Landmark>();
for (const item of items) {
mapGetOrInit(landmarks, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
mapGetOrInit(landmarks, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
}
const array = Array.from(landmarks.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 Landmark = {
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

@@ -1,20 +0,0 @@
function clamp(value: number, min: number, max: number) {
if (value < min) {
return min;
}
if (value > max) {
return max;
}
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;
}

View File

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

View File

@@ -1,56 +0,0 @@
function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(Workspace.windowAdded, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(Workspace.windowRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(Workspace.windowActivated, (kwinClient: KwinClient|null) => {
if (kwinClient === null) {
return;
}
world.do((clientManager, desktopManager) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(Workspace.currentDesktopChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.currentActivityChanged, () => {
world.do(() => {}); // re-arrange desktop
});
manager.connect(Workspace.screensChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.selectScreen(Workspace.activeScreen);
});
});
manager.connect(Workspace.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateActivities();
});
});
manager.connect(Workspace.desktopsChanged, () => {
world.do((clientManager, desktopManager) => {
desktopManager.updateDesktops();
});
});
manager.connect(Workspace.virtualScreenSizeChanged, () => {
world.onScreenResized();
});
return manager;
}

View File

@@ -1,61 +0,0 @@
namespace Clients {
const prohibitedClasses = [
"ksmserver-logout-greeter",
"xwaylandvideobridge",
];
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable &&
kwinClient.resizeable &&
!kwinClient.popupWindow &&
!prohibitedClasses.includes(kwinClient.resourceClass);
}
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) &&
!kwinClient.minimized &&
kwinClient.desktops.length === 1 &&
kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
if (kwinClient.desktops.length !== 1) {
kwinClient.desktops = [Workspace.currentDesktop];
}
if (kwinClient.activities.length !== 1) {
kwinClient.activities = [Workspace.currentActivity];
}
}
export function getKwinDesktopApprox(kwinClient: KwinClient) {
switch (kwinClient.desktops.length) {
case 0:
return Workspace.currentDesktop;
case 1:
return kwinClient.desktops[0];
default:
if (kwinClient.desktops.includes(Workspace.currentDesktop)) {
return Workspace.currentDesktop;
} else {
return kwinClient.desktops[0];
}
}
}
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.clientGeometry.width === fullScreenArea.width &&
kwinClient.clientGeometry.height === fullScreenArea.height;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.includes(kwinDesktop);
}
export function isOnOneOfVirtualDesktops(kwinClient: KwinClient, kwinDesktops: KwinDesktop[]) {
return kwinClient.desktops.length === 0 || kwinClient.desktops.some(d => kwinDesktops.includes(d));
}
}

View File

@@ -1,142 +0,0 @@
class DesktopManager {
private readonly desktops: Map<string, Desktop>; // key is activityId|desktopId
private selectedScreen: Output;
private kwinActivities: Set<string>;
private kwinDesktops: Set<KwinDesktop>;
constructor(
private readonly pinManager: PinManager,
private readonly config: Desktop.Config,
public readonly layoutConfig: LayoutConfig,
currentActivity: string,
currentDesktop: KwinDesktop,
) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktops = new Map();
this.selectedScreen = Workspace.activeScreen;
this.kwinActivities = new Set(Workspace.activities);
this.kwinDesktops = new Set(Workspace.desktops);
this.addDesktop(currentActivity, currentDesktop);
}
public getDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
return desktop;
} else {
return this.addDesktop(activity, kwinDesktop);
}
}
public getCurrentDesktop() {
return this.getDesktop(Workspace.currentActivity, Workspace.currentDesktop);
}
public getDesktopInCurrentActivity(kwinDesktop: KwinDesktop) {
return this.getDesktop(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]);
}
private addDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = new Desktop(
kwinDesktop,
this.pinManager,
this.config,
() => this.selectedScreen,
this.layoutConfig,
);
this.desktops.set(desktopKey, desktop);
return desktop;
}
private static getDesktopKey(activity: string, kwinDesktop: KwinDesktop) {
return activity + "|" + kwinDesktop.id;
}
public updateActivities() {
const newActivities = new Set(Workspace.activities);
for (const activity of this.kwinActivities) {
if (!newActivities.has(activity)) {
this.removeActivity(activity);
}
}
this.kwinActivities = newActivities;
}
public updateDesktops() {
const newDesktops = new Set(Workspace.desktops);
for (const desktop of this.kwinDesktops) {
if (!newDesktops.has(desktop)) {
this.removeKwinDesktop(desktop);
}
}
this.kwinDesktops = newDesktops;
}
public selectScreen(screen: Output) {
this.selectedScreen = screen;
}
private removeActivity(activity: string) {
for (const kwinDesktop of this.kwinDesktops) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private removeKwinDesktop(kwinDesktop: KwinDesktop) {
for (const activity of this.kwinActivities) {
this.destroyDesktop(activity, kwinDesktop);
}
}
private destroyDesktop(activity: string, kwinDesktop: KwinDesktop) {
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
desktop.destroy();
this.desktops.delete(desktopKey);
}
}
public destroy() {
for (const desktop of this.desktops.values()) {
desktop.destroy();
}
}
public *getAllDesktops() {
for (const desktop of this.desktops.values()) {
yield desktop;
}
}
public getDesktopsForClient(kwinClient: KwinClient) {
const desktops = this.getDesktops(kwinClient.activities, kwinClient.desktops); // workaround for QTBUG-109880
return desktops;
}
// empty array means all
public *getDesktops(activities: string[], kwinDesktops: KwinDesktop[]) {
const matchedActivities = activities.length > 0 ? activities : this.kwinActivities.keys();
const matchedDesktops = kwinDesktops.length > 0 ? kwinDesktops : this.kwinDesktops.keys();
for (const matchedActivity of matchedActivities) {
for (const matchedDesktop of matchedDesktops) {
const desktopKey = DesktopManager.getDesktopKey(matchedActivity, matchedDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {
yield desktop;
}
}
}
}
}

View File

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

6
src/main.ts Normal file
View File

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

View File

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

View File

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

5
src/rules/WindowRule.ts Normal file
View File

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

View File

@@ -0,0 +1,90 @@
class WindowRuleEnforcer {
private readonly preferFloating: ClientMatcher;
private readonly preferTiling: ClientMatcher;
private readonly followCaption: Set<string>;
constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = WindowRuleEnforcer.createWindowRuleMaps(windowRules);
this.preferFloating = new ClientMatcher(mapFloat);
this.preferTiling = new ClientMatcher(mapTile);
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]);
}
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
);
}
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.has(kwinClient.resourceClass)) {
return null;
}
const enforcer = this;
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
world.do((clientManager, desktopManager) => {
const desktop = desktopManager.getDesktopForClient(kwinClient);
if (shouldTile && desktop !== undefined) {
clientManager.tileClient(kwinClient, desktop.grid);
} else {
clientManager.untileClient(kwinClient);
}
});
});
return manager;
}
private static createWindowRuleMaps(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>();
const mapTile = new Map<string, string[]>();
for (const windowRule of windowRules) {
const map = windowRule.tile ? mapTile : mapFloat;
let captions = map.get(windowRule.class);
if (captions === undefined) {
captions = [];
map.set(windowRule.class, captions);
}
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption);
}
}
return [
WindowRuleEnforcer.createWindowRuleRegexMap(mapFloat),
WindowRuleEnforcer.createWindowRuleRegexMap(mapTile),
];
}
private static createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
const regexMap = new Map<string, RegExp>;
for (const [k, v] of windowRuleMap) {
regexMap.set(k, WindowRuleEnforcer.joinRegexes(v));
}
return regexMap;
}
private static joinRegexes(regexes: string[]) {
if (regexes.length === 0) {
return new RegExp("");
}
if (regexes.length === 1) {
return new RegExp("^" + regexes[0] + "$");
}
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^" + joinedRegexes + "$");
}
private static wrapParens(str: string) {
return "(" + str + ")";
}
}

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,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, grid, { 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,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,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, [ [pinned], [tiled1], [tiled2] ]);
pinned.pin(screenHalfLeft);
Assert.equalRects(pinned.frameGeometry, screenHalfLeft);
Assert.grid(config, screenHalfRight, [ [tiled1], [tiled2] ]);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
pinned.unpin();
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screen, [ [tiled1], [tiled2] ]);
pinned.pin(screenHalfRight);
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
pinned.minimized = true;
Assert.grid(config, screen, [ [tiled1], [tiled2] ]);
pinned.minimized = false;
Assert.equalRects(pinned.frameGeometry, screenHalfRight);
Assert.grid(config, screenHalfLeft, [ [tiled1], [tiled2] ]);
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, [ [tiled1], [tiled2], [pinned] ]);
});

View File

@@ -1,119 +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));
});
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));
});
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

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

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,200 +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,
grid: KwinClient[][],
{ message, skip=0 }: Options = {},
) {
// assumes uniformly sized windows within columns of width 100
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
const columnsWidth = nColumns * 100 + (nColumns-1) * config.gapsInnerHorizontal;
const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows;
return new MockQmlRect(
screen.x + column * (100 + config.gapsInnerHorizontal) + (screen.width-columnsWidth) / 2,
screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window,
100,
(columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows,
);
}
const nColumns = grid.length;
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 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,192 +0,0 @@
class MockKwinClient {
public readonly __brand = "KwinClient";
private static readonly borderThickness = 10;
public readonly shadeable: boolean = false;
public readonly 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 _fullScreen: boolean = false;
public activities: string[] = [];
public skipSwitcher: boolean = false;
public keepAbove: boolean = false;
public keepBelow: boolean = false;
public shade: boolean = false;
public _minimized: boolean = false;
public desktops: KwinDesktop[] = [];
public _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 = false;
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;
}
setMaximize(vertically: boolean, horizontally: boolean) {
this.windowed = !(vertically || 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 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();
}
}

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,99 +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 windows = [];
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.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));
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 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);
}
}

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