23 Commits

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

* Fix cursor follow focus to only work on matched desktops

* Resolve review comments

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2025-10-21 21:06:25 +02:00
47 changed files with 747 additions and 238 deletions

View File

@@ -3,7 +3,7 @@ CHECKS := true
.PHONY: *
build: lint tests
build: lint test
tsc -p ./src/main --outFile ./package/contents/code/main.js
mkdir -p ./package/contents/config
./run-ts.sh ./src/generators/config > ./package/contents/config/main.xml
@@ -19,7 +19,7 @@ endif
lint-fix: npm-install
npx eslint ./src --fix
tests:
test:
ifeq (${CHECKS}, true)
./run-ts.sh ./src/tests
endif

8
package-lock.json generated
View File

@@ -4,6 +4,7 @@
"requires": true,
"packages": {
"": {
"name": "karousel",
"devDependencies": {
"eslint": "^9.24.0",
"typescript-eslint": "^8.30.1"
@@ -999,10 +1000,11 @@
"dev": true
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},

View File

@@ -32,7 +32,7 @@
<item>
<widget class="QCheckBox" name="kcfg_cursorFollowsFocus">
<property name="text">
<string>Cursor follows focus</string>
<string>Cursor follows focus (experimental)</string>
</property>
<property name="toolTip">
<string>When a window gains focus, move the cursor to it</string>
@@ -149,14 +149,14 @@
<item>
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
<string>Set "Keep Below" for tiled windows</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
<string>Set "Keep Above" for floating windows</string>
</property>
</widget>
</item>
@@ -458,6 +458,27 @@
<string>Window Rules</string>
</attribute>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_desktops">
<property name="text">
<string>Tiled desktops:</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="kcfg_tiledDesktops">
<property name="toolTip">
<string>Regex string to match desktops by desktop name"</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="kcfg_windowRules">
<property name="tabChangesFocus">

View File

@@ -16,6 +16,16 @@ Item {
qmlBase.karouselInstance.destroy();
}
Notification {
id: notificationInvalidTiledDesktops
componentName: "plasma_workspace"
eventId: "notification"
title: "Karousel"
text: "Your Tiled Desktops regex is malformed, please review your Karousel configuration"
flags: Notification.Persistent
urgency: Notification.HighUrgency
}
Notification {
id: notificationInvalidWindowRules
componentName: "plasma_workspace"

View File

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

View File

@@ -2,6 +2,7 @@ declare const Qt: Qt;
declare const KWin: KWin;
declare const Workspace: Workspace;
declare const qmlBase: QmlObject;
declare const notificationInvalidTiledDesktops: Notification;
declare const notificationInvalidWindowRules: Notification;
declare const notificationInvalidPresetWidths: Notification;
declare const moveCursorToFocus: DBusCall;

View File

@@ -13,8 +13,8 @@ class ContextualResizer {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
@@ -50,8 +50,8 @@ class ContextualResizer {
return;
}
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange, true);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange, true);
const leftVisibleColumn = grid.getLeftmostVisibleColumn(visibleRange);
const rightVisibleColumn = grid.getRightmostVisibleColumn(visibleRange);
if (leftVisibleColumn === null || rightVisibleColumn === null) {
console.assert(false); // should at least see self
return;
@@ -86,4 +86,22 @@ class ContextualResizer {
column.setWidth(newWidth, true);
desktop.scrollCenterVisible(column);
}
public maximizeWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const maxWidth = presetWidths[presetWidths.length-1];
column.setWidth(maxWidth, true);
desktop.scrollCenterVisible(column);
}
public minimizeWidth(column: Column) {
const grid = column.grid;
const desktop = grid.desktop;
const presetWidths = this.presetWidths.getWidths(column.getMinWidth(), column.getMaxWidth());
const minWidth = presetWidths[0];
column.setWidth(minWidth, true);
desktop.scrollCenterVisible(column);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,11 @@ class Actions {
};
public readonly focusStart = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
@@ -71,7 +75,11 @@ class Actions {
};
public readonly focusEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
@@ -184,6 +192,15 @@ class Actions {
this.config.columnResizer.decreaseWidth(column);
};
public readonly columnWidthMaximize = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.maximizeWidth(column);
};
public readonly columnWidthMinimize = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
this.config.columnResizer.minimizeWidth(column);
};
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const nextWidth = this.config.presetWidths.next(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
@@ -196,8 +213,11 @@ class Actions {
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const visibleRange = desktop.getCurrentVisibleRange();
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange, true));
const visibleColumns = Array.from(desktop.grid.getVisibleColumns(visibleRange));
const availableSpace = desktop.tilingArea.width;
const gapsWidth = desktop.grid.config.gapsInnerHorizontal * (visibleColumns.length-1);
@@ -219,7 +239,7 @@ class Actions {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getLeftColumn(currentVisibleColumns[0]);
@@ -246,7 +266,7 @@ class Actions {
return;
}
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange, true));
const currentVisibleColumns = Array.from(grid.getVisibleColumns(visibleRange));
console.assert(currentVisibleColumns.includes(focusedColumn), "should at least contain the focused column");
const targetColumn = grid.getRightColumn(currentVisibleColumns[currentVisibleColumns.length-1]);
@@ -297,11 +317,18 @@ class Actions {
};
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
desktopManager.getCurrentDesktop().adjustScroll(amount, false);
const desktop = desktopManager.getCurrentDesktop();
if (desktop !== undefined) {
desktop.adjustScroll(amount, false);
}
};
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
@@ -310,7 +337,11 @@ class Actions {
};
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
@@ -328,8 +359,12 @@ class Actions {
};
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getLeftmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) {
return;
}
@@ -343,8 +378,12 @@ class Actions {
};
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange(), true);
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const column = grid.getRightmostVisibleColumn(grid.desktop.getCurrentVisibleRange());
if (column === null) {
return;
}
@@ -362,7 +401,11 @@ class Actions {
};
public readonly focus = (columnIndex: number, cm: ClientManager, dm: DesktopManager) => {
const grid = dm.getCurrentDesktop().grid;
const desktop = dm.getCurrentDesktop();
if (desktop === undefined) {
return;
}
const grid = desktop.grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return;
@@ -396,7 +439,11 @@ class Actions {
if (kwinDesktop === undefined) {
return;
}
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
if (newDesktop === undefined) {
return;
}
const newGrid = newDesktop.grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
@@ -408,7 +455,11 @@ class Actions {
if (kwinDesktop === undefined) {
return;
}
const newGrid = dm.getDesktopInCurrentActivity(kwinDesktop).grid;
const newDesktop = dm.getDesktopInCurrentActivity(kwinDesktop);
if (newDesktop === undefined) {
return;
}
const newGrid = newDesktop.grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
@@ -422,12 +473,14 @@ namespace Actions {
presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
};
};
columnResizer: ColumnResizer;
}
export interface ColumnResizer {
increaseWidth(column: Column): void;
decreaseWidth(column: Column): void;
maximizeWidth(column: Column): void;
minimizeWidth(column: Column): void;
}
}

View File

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

View File

@@ -79,7 +79,7 @@ class Column {
public getMinWidth() {
let maxMinWidth = Column.minWidth;
for (const window of this.windows.iterator()) {
const minWidth = window.client.kwinClient.minSize.width;
const minWidth = window.client.kwinClient.minSize.width.ceil();
if (minWidth > maxMinWidth) {
maxMinWidth = minWidth;
}

View File

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

View File

@@ -106,7 +106,7 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal;
}
public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
public getLeftmostVisibleColumn(visibleRange: Range) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
return column;
@@ -115,7 +115,7 @@ class Grid {
return null;
}
public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
public getRightmostVisibleColumn(visibleRange: Range) {
let last = null;
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
@@ -127,7 +127,7 @@ class Grid {
return last;
}
public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
public *getVisibleColumns(visibleRange: Range) {
for (const column of this.columns.iterator()) {
if (Range.contains(visibleRange, column)) {
yield column;

View File

@@ -7,7 +7,7 @@ class Window {
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height;
this.height = client.kwinClient.frameGeometry.height.round();
let maximizedMode = this.client.getMaximizedMode();
if (maximizedMode === undefined) {
@@ -123,7 +123,7 @@ class Window {
public onFrameGeometryChanged() {
const newGeometry = this.client.kwinClient.frameGeometry;
this.column.setWidth(newGeometry.width, true);
this.column.setWidth(newGeometry.width.round(), true);
this.column.grid.desktop.onLayoutChanged();
}

View File

@@ -0,0 +1,30 @@
class DesktopFilter {
private readonly desktopRegex: RegExp | null; // null means all desktops
constructor(desktopsConfig: string) {
this.desktopRegex = DesktopFilter.parseDesktopConfig(desktopsConfig);
}
public shouldWorkOnDesktop(kwinDesktop: KwinDesktop): boolean {
if (this.desktopRegex === null) {
return true; // Work on all desktops
}
return this.desktopRegex.test(kwinDesktop.name);
}
private static parseDesktopConfig(config: string): RegExp | null {
const trimmed = config.trim();
if (trimmed.length === 0) {
return null; // Empty config means work on all desktops
}
try {
return new RegExp(`^${trimmed}$`);
} catch (e) {
notificationInvalidTiledDesktops.sendEvent();
log(`Invalid regex pattern in tiledDesktops config: ${trimmed}. Working on all desktops.`);
return null; // Invalid regex means work on all desktops as fallback
}
}
}

View File

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

View File

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

View File

@@ -31,16 +31,16 @@ class ClientManager {
console.assert(!this.hasClient(kwinClient));
let constructState: (client: ClientWrapper) => ClientState.State;
let desktop: Desktop | undefined;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (
Clients.canTileEver(kwinClient) &&
this.windowRuleEnforcer.shouldTile(kwinClient)
this.windowRuleEnforcer.shouldTile(kwinClient) &&
(desktop = this.desktopManager.getDesktopForClient(kwinClient)) !== undefined
) {
Clients.makeTileable(kwinClient);
console.assert(Clients.canTileNow(kwinClient));
const desktop = this.desktopManager.getDesktopForClient(kwinClient);
console.assert(desktop !== undefined);
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, desktop!.grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);

View File

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

View File

@@ -47,8 +47,8 @@ namespace Clients {
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = Workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.output, getKwinDesktopApprox(kwinClient));
return kwinClient.clientGeometry.width >= fullScreenArea.width &&
kwinClient.clientGeometry.height >= fullScreenArea.height;
return kwinClient.clientGeometry.width.round() >= fullScreenArea.width &&
kwinClient.clientGeometry.height.round() >= fullScreenArea.height;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, kwinDesktop: KwinDesktop) {

View File

@@ -9,8 +9,7 @@ class DesktopManager {
private readonly config: Desktop.Config,
private readonly layoutConfig: LayoutConfig,
private readonly focusPasser: FocusPassing.Passer,
currentActivity: string,
currentDesktop: KwinDesktop,
private readonly desktopFilter: DesktopFilter,
) {
this.pinManager = pinManager;
this.config = config;
@@ -19,10 +18,12 @@ class DesktopManager {
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) {
if (!this.desktopFilter.shouldWorkOnDesktop(kwinDesktop)) {
return undefined;
}
const desktopKey = DesktopManager.getDesktopKey(activity, kwinDesktop);
const desktop = this.desktops.get(desktopKey);
if (desktop !== undefined) {

View File

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

View File

@@ -70,8 +70,7 @@ class World {
},
layoutConfig,
focusPasser,
Workspace.currentActivity,
Workspace.currentDesktop,
new DesktopFilter(config.tiledDesktops),
);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
@@ -98,13 +97,21 @@ class World {
}
private update() {
this.desktopManager.getCurrentDesktop().arrange();
this.moveCursorToFocus();
const currentDesktop = this.desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
currentDesktop.arrange();
this.moveCursorToFocus();
}
}
private moveCursorToFocus() {
if (this.cursorFollowsFocus && Workspace.activeWindow !== null) {
const cursorAlreadyInFocus = rectContainsPoint(Workspace.activeWindow.frameGeometry, Workspace.cursorPos);
// Only move cursor for tiled windows
const tiledWindow = this.clientManager.findTiledWindow(Workspace.activeWindow);
if (tiledWindow === null) {
return;
}
const cursorAlreadyInFocus = rectContainsPoint(roundQtRect(Workspace.activeWindow.frameGeometry), Workspace.cursorPos);
if (cursorAlreadyInFocus) {
return;
}
@@ -141,11 +148,23 @@ class World {
}
public gestureScroll(amount: number) {
this.do((clientManager, desktopManager) => desktopManager.getCurrentDesktop().gestureScroll(amount));
this.do((clientManager, desktopManager) => {
const currentDesktop = desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
currentDesktop.gestureScroll(amount);
}
});
}
public gestureScrollFinish() {
this.do((clientManager, desktopManager) => desktopManager.getCurrentDesktop().gestureScrollFinish());
this.do((clientManager, desktopManager) => {
const focusedWindow = Workspace.activeWindow === null ? null : clientManager.findTiledWindow(Workspace.activeWindow);
const currentDesktop = desktopManager.getCurrentDesktop();
if (currentDesktop !== undefined) {
console.assert(focusedWindow === null || focusedWindow.column.grid.desktop === currentDesktop);
currentDesktop.gestureScrollFinish(focusedWindow);
}
});
}
public destroy() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,32 +5,78 @@ tests.register("Drag tiled window, untile", 10, () => {
const [client1, client2] = workspaceMock.createClients(2);
const initialCursorPos = new MockQmlPoint(380, 20);
Assert.assert(rectContainsPoint(client1.frameGeometry, initialCursorPos), { message: "invalid test setup" });
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), initialCursorPos), { message: "invalid test setup" });
workspaceMock.cursorPos = initialCursorPos.clone();
runOneOf(
() => { Workspace.activeWindow = client1; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
);
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos), { message: "Cursor should not have been moved because it was already within the focused client" });
runOneOf(
() => { Workspace.activeWindow = client2; },
() => { qtMock.fireShortcut("karousel-focus-2"); },
);
Assert.assert(!rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
Assert.assert(rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
runOneOf(
() => { Workspace.activeWindow = client1; },
() => { qtMock.fireShortcut("karousel-focus-1"); },
);
Assert.assert(rectContainsPoint(client1.frameGeometry, Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.frameGeometry, Workspace.cursorPos));
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos));
Assert.assert(!rectContainsPoint(client2.getActualFrameGeometry(), Workspace.cursorPos));
const lastCursorPos = workspaceMock.cursorPos.clone();
Workspace.activeWindow = null;
Assert.assert(pointEquals(Workspace.cursorPos, lastCursorPos), { message: "Cursor should not have been moved" });
});
tests.register("Cursor follows focus only on matched desktops", 1, () => {
// Test that cursor follow focus only works for windows on matched desktops (tiled windows)
const config = getDefaultConfig();
config.cursorFollowsFocus = true;
config.tiledDesktops = "^Desktop 1$"; // Only work on Desktop 1
const { workspaceMock, world } = init(config);
// Create a client on Desktop 1 (matched desktop) - should be tiled
const client1 = new MockKwinClient();
client1.desktops = [workspaceMock.desktops[0]]; // Desktop 1
workspaceMock.createWindows(client1);
// Create a client on Desktop 2 (non-matched desktop) - should be floating
const client2 = new MockKwinClient();
client2.desktops = [workspaceMock.desktops[1]]; // Desktop 2
workspaceMock.createWindows(client2);
// Set initial cursor position outside both windows
const initialCursorPos = new MockQmlPoint(10, 10);
workspaceMock.cursorPos = initialCursorPos.clone();
// Test 1: Focus client1 on matched desktop (Desktop 1) - cursor should move
workspaceMock.currentDesktop = workspaceMock.desktops[0]; // Switch to Desktop 1
Workspace.activeWindow = client1;
world.do(() => {});
Assert.assert(rectContainsPoint(client1.getActualFrameGeometry(), Workspace.cursorPos),
{ message: "Cursor should have moved to tiled window on matched desktop" });
// Test 2: Switch to non-matched desktop (Desktop 2) and focus client2 - cursor should NOT move
workspaceMock.cursorPos = initialCursorPos.clone();
workspaceMock.currentDesktop = workspaceMock.desktops[1]; // Switch to Desktop 2
Workspace.activeWindow = client2;
world.do(() => {});
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos),
{ message: "Cursor should NOT move on non-matched desktop" });
// Test 3: Even if we focus client1 (tiled) while on Desktop 2, cursor should NOT move
// because the current desktop is not matched
workspaceMock.cursorPos = initialCursorPos.clone();
workspaceMock.currentDesktop = workspaceMock.desktops[1]; // Stay on Desktop 2
Workspace.activeWindow = client1;
world.do(() => {});
Assert.assert(pointEquals(Workspace.cursorPos, initialCursorPos),
{ message: "Cursor should NOT move even for tiled window when current desktop is not matched" });
});

View File

@@ -0,0 +1,73 @@
tests.register("Desktop filtering", 1, () => {
// Test 1: Default config should work on all desktops
const config1 = getDefaultConfig();
const { workspaceMock: wm1, world: world1 } = init(config1);
const client1 = new MockKwinClient();
client1.desktops = [wm1.desktops[0]];
wm1.createWindows(client1);
world1.do((clientManager) => {
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on desktop1 with default config (*)" });
});
});
tests.register("Desktop filtering - specific desktop", 1, () => {
// Test 2: Specific desktop name - should work only on matching desktop
const config2 = getDefaultConfig();
config2.tiledDesktops = "^Desktop 1$";
const { workspaceMock: wm2, world: world2 } = init(config2);
const client1 = new MockKwinClient();
client1.desktops = [wm2.desktops[0]]; // Desktop 1
wm2.createWindows(client1);
world2.do((clientManager) => {
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on Desktop 1" });
});
wm2.removeWindow(client1);
const client2 = new MockKwinClient();
client2.desktops = [wm2.desktops[1]]; // Desktop 2
wm2.createWindows(client2);
world2.do((clientManager) => {
Assert.notTiledClient(clientManager, client2, { message: "Client should NOT be tiled on Desktop 2" });
});
});
tests.register("Desktop filtering - multiple desktops", 1, () => {
// Test 3: Multiple desktop names using regex alternation
const config3 = getDefaultConfig();
config3.tiledDesktops = "^Desktop [12]$";
const { workspaceMock: wm3, world: world3 } = init(config3);
const client1 = new MockKwinClient();
client1.desktops = [wm3.desktops[0]]; // Desktop 1
wm3.createWindows(client1);
world3.do((clientManager) => {
Assert.tiledClient(clientManager, client1, { message: "Client should be tiled on Desktop 1" });
});
wm3.removeWindow(client1);
const client2 = new MockKwinClient();
client2.desktops = [wm3.desktops[1]]; // Desktop 2
wm3.createWindows(client2);
world3.do((clientManager) => {
Assert.tiledClient(clientManager, client2, { message: "Client should be tiled on Desktop 2" });
});
});
tests.register("Desktop filtering - windows on multiple desktops", 1, () => {
// Test 4: Windows on multiple desktops should not be tiled (fallback to floating)
const config4 = getDefaultConfig();
config4.tiledDesktops = ".*";
const { workspaceMock: wm4, world: world4 } = init(config4);
const client1 = new MockKwinClient();
client1.desktops = [wm4.desktops[0], wm4.desktops[1]]; // Multiple desktops
wm4.createWindows(client1);
world4.do((clientManager) => {
Assert.notTiledClient(clientManager, client1, { message: "Client on multiple desktops should not be tiled" });
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,9 +6,9 @@ tests.register("User resize", 10, () => {
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);
Assert.rect(clientLeft.getActualFrameGeometry(), left, tilingArea.y, leftWidth, tilingArea.height);
Assert.rect(clientRightTop.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, topHeight);
Assert.rect(clientRightBottom.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y+topHeight+gapV, rightWidth, bottomHeight);
}
{
@@ -89,9 +89,9 @@ tests.register("User resize", 10, () => {
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);
Assert.rect(clientLeftTop.getActualFrameGeometry(), left, tilingArea.y, leftWidth, topHeight);
Assert.rect(clientLeftBottom.getActualFrameGeometry(), left, tilingArea.y+topHeight+gapV, leftWidth, bottomHeight);
Assert.rect(clientRight.getActualFrameGeometry(), left+leftWidth+gapH, tilingArea.y, rightWidth, tilingArea.height);
}
workspaceMock.activeWindow = clientLeftBottom;

View File

@@ -0,0 +1,41 @@
tests.register("DesktopFilter", 1, () => {
const desktop1 = { __brand: "KwinDesktop" as const, id: "1", name: "Desktop 1" };
const desktop2 = { __brand: "KwinDesktop" as const, id: "2", name: "Work" };
const desktop3 = { __brand: "KwinDesktop" as const, id: "3", name: "Desktop 2" };
// Test 1: Empty config means all desktops
let filter = new DesktopFilter("");
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Empty config should work on desktop1" });
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Empty config should work on desktop2" });
// Test 2: Whitespace only means all desktops
filter = new DesktopFilter(" \n \n ");
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Whitespace only should work on desktop1" });
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Whitespace only should work on desktop2" });
// Test 3: Match all regex pattern
filter = new DesktopFilter(".*");
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Regex '.*' should work on desktop1" });
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Regex '.*' should work on desktop2" });
// Test 4: Partial match without anchors
filter = new DesktopFilter("Work");
Assert.assert(!filter.shouldWorkOnDesktop(desktop1), { message: "Should not work on desktop1" });
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Should work on desktop2 containing 'Work'" });
// Test 5: Regex alternation for multiple desktops
filter = new DesktopFilter("Desktop 1|Work");
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Should work on desktop1" });
Assert.assert(filter.shouldWorkOnDesktop(desktop2), { message: "Should work on desktop2" });
Assert.assert(!filter.shouldWorkOnDesktop(desktop3), { message: "Should not work on desktop3" });
// Test 6: Regex pattern with character class
filter = new DesktopFilter("Desktop [12]");
Assert.assert(filter.shouldWorkOnDesktop(desktop1), { message: "Should work on desktop1" });
Assert.assert(!filter.shouldWorkOnDesktop(desktop2), { message: "Should not work on desktop2" });
Assert.assert(filter.shouldWorkOnDesktop(desktop3), { message: "Should work on desktop3" });
// Test 7: Case-sensitive matching
filter = new DesktopFilter("work");
Assert.assert(!filter.shouldWorkOnDesktop(desktop2), { message: "Should not work on desktop2 (case mismatch)" });
});

View File

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

View File

@@ -2,6 +2,7 @@ let Qt: Qt;
let KWin: KWin;
let Workspace: Workspace;
let qmlBase: QmlObject;
let notificationInvalidTiledDesktops: Notification;
let notificationInvalidWindowRules: Notification;
let notificationInvalidPresetWidths: Notification;
let moveCursorToFocus: DBusCall;
@@ -33,7 +34,7 @@ function init(config: Config) {
__brand: "QmlObject",
call: () => {
Assert.assert(Workspace.activeWindow !== null, { message: "moveCursorToFocus should never be called if there's no focused window" });
const frame = Workspace.activeWindow!.frameGeometry;
const frame = (Workspace.activeWindow! as MockKwinClient).getActualFrameGeometry();
workspaceMock.cursorPos.x = Math.floor(frame.x + frame.width/2);
workspaceMock.cursorPos.y = Math.floor(frame.y + frame.height/2);
},
@@ -43,9 +44,9 @@ function init(config: Config) {
return { qtMock, workspaceMock, world };
}
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) {
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left;
const left = tilingArea.left + Math.floor((tilingArea.width - columnsWidth) / 2);
function getGridBounds(clientLeft: MockKwinClient, clientRight: MockKwinClient) {
const columnsWidth = rectRight(clientRight.getActualFrameGeometry()) - clientLeft.getActualFrameGeometry().x;
const left = tilingArea.x + Math.floor((tilingArea.width - columnsWidth) / 2);
const right = left + columnsWidth;
return { left, right };
}

View File

@@ -4,7 +4,7 @@ class MockKwinClient {
private static readonly borderThickness = 10;
public caption = "App";
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public minSize: Readonly<QmlSize> = new MockQmlSize(randomJitter(), randomJitter());
public readonly transient: boolean;
public move = false;
public resize = false;
@@ -52,6 +52,7 @@ class MockKwinClient {
this.windowedFrameGeometry = _frameGeometry.clone();
this.transient = transientFor !== null;
this._desktops = [Workspace.currentDesktop];
this.activities = [Workspace.currentActivity];
}
setMaximize(vertically: boolean, horizontally: boolean) {
@@ -159,10 +160,22 @@ class MockKwinClient {
);
}
public get frameGeometry() {
// for assertions
public getActualFrameGeometry() {
return this._frameGeometry;
}
// for Karousel
public get frameGeometry() {
return new MockQmlRect(
this._frameGeometry.x + randomJitter(),
this._frameGeometry.y + randomJitter(),
this._frameGeometry.width + randomJitter(),
this._frameGeometry.height + randomJitter(),
this.frameGeometryChanged.fire.bind(this.frameGeometryChanged),
);
}
public set frameGeometry(frameGeometry: MockQmlRect) {
const oldFrameGeometry = this._frameGeometry;
this._frameGeometry = new MockQmlRect(

View File

@@ -49,22 +49,6 @@ class MockQmlRect {
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;

View File

@@ -3,8 +3,8 @@ class MockWorkspace {
public activities = ["test-activity"];
public desktops: KwinDesktop[] = [
{ __brand: "KwinDesktop", id: "desktop1" },
{ __brand: "KwinDesktop", id: "desktop2" },
{ __brand: "KwinDesktop", id: "desktop1", name: "Desktop 1" },
{ __brand: "KwinDesktop", id: "desktop2", name: "Desktop 2" },
];
public currentActivity = this.activities[0];
public activeScreen: Output = { __brand: "Output" };
@@ -75,7 +75,7 @@ class MockWorkspace {
frame.y += delta.y;
}
runOneOf(
() => window.frameGeometry.set(frame),
() => window.getActualFrameGeometry().set(frame),
() => window.frameGeometry = frame,
);
}
@@ -88,8 +88,8 @@ class MockWorkspace {
const frame = window.getFrameGeometryCopy();
if (edgeResize) {
this.cursorPos = new MockQmlPoint(
leftEdge ? frame.left : frame.right,
topEdge ? frame.top : frame.bottom,
leftEdge ? frame.x : rectRight(frame),
topEdge ? frame.y : rectBottom(frame),
);
} else {
this.cursorPos = new MockQmlPoint(
@@ -114,7 +114,7 @@ class MockWorkspace {
}
}
runOneOf(
() => window.frameGeometry.set(frame),
() => window.getActualFrameGeometry().set(frame),
() => window.frameGeometry = frame,
);
}

View File

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