19 Commits
v0.11 ... v0.12

Author SHA1 Message Date
Peter Fajdiga
465945429a bump version to 0.12 2025-03-14 12:33:19 +01:00
Peter Fajdiga
1d7636508b Column: make stack offsets configurable 2025-03-10 23:00:19 +01:00
Peter Fajdiga
47213a71f5 tests: fix tests that were using screen.width/2 2025-03-08 19:48:56 +01:00
Peter Fajdiga
75a548977c tests: Assert.grid: pass tilingArea instead of screen 2025-03-08 19:38:23 +01:00
Peter Fajdiga
d746b91a88 tests: add test for stacked columns 2025-03-08 17:34:51 +01:00
Peter Fajdiga
a0d9c49287 remove all uses of window shading 2025-03-08 17:18:27 +01:00
Peter Fajdiga
862cc445bd stack without shading 2025-03-08 17:18:27 +01:00
Peter Fajdiga
5019a5d702 re-arrange after dragging a tiled window 2025-03-08 17:18:22 +01:00
Peter Fajdiga
36c7cab137 tests: add test for dragging a tiled window 2025-03-07 20:12:37 +01:00
Peter Fajdiga
df3c1f4512 allow tiling windows that start in full-screen (fixes #79) 2025-03-07 16:48:48 +01:00
Peter Fajdiga
5f3eaf1eec MockKwinClient: make moveable and resizable dependent on fullscreen 2025-03-07 16:48:48 +01:00
Peter Fajdiga
4a680177f6 respect window rules for full-screen windows (fixes #79) 2025-03-07 16:48:48 +01:00
Peter Fajdiga
8d807c979b tests: add test for windows that start in full-screen 2025-03-07 16:48:44 +01:00
Peter Fajdiga
c8e37aeb87 Tiled: allow new windows to stay in fullScreen 2025-03-07 16:43:50 +01:00
Peter Fajdiga
ad0fe7472c Window: store initial maximized and fullScreen state 2025-03-07 16:43:50 +01:00
Peter Fajdiga
a51e45667c Assert.grid: reorder message 2025-03-07 16:43:50 +01:00
Himadri Bhattacharjee
6615fe6f93 fix: float polkit authentication window (#88)
* fix: float polkit authentication window

* fix: ignore polkit windows on X11

Co-authored-by: Peter Fajdiga <peter.fajdiga@gmail.com>

---------

Co-authored-by: Peter Fajdiga <peter.fajdiga@gmail.com>
2025-03-07 14:11:45 +01:00
Peter Fajdiga
6e69139b80 Actions.gridScrollFocused: undo if already centered 2025-01-19 15:37:50 +01:00
Peter Fajdiga
97430d5043 readme: update key bindings 2025-01-18 13:35:32 +01:00
35 changed files with 417 additions and 124 deletions

View File

@@ -51,7 +51,7 @@ Here's the default ones:
| (unassigned) | Move window to the previous position in grid |
| Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end |
| Meta+X | Toggle stacked layout for focused column (One window in the column visible, others shaded; not supported on Wayland) |
| Meta+X | Toggle stacked layout for focused column (Only the active window visible) |
| Meta+Ctrl+Shift+A | Move column left |
| Meta+Ctrl+Shift+D | Move column right |
| Meta+Ctrl+Shift+Home | Move column to start |
@@ -59,6 +59,7 @@ Here's the default ones:
| Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width |
| Meta+R | Cycle through preset column widths |
| Meta+Shift+R | Cycle through preset column widths in reverse |
| Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Ctrl+D | Squeeze right column onto the screen |

View File

@@ -35,7 +35,7 @@
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode (one window in the column visible, others shaded). Not supported on Wayland.</string>
<string>New columns start in stacked mode (only the active window visible)</string>
</property>
</widget>
</item>
@@ -280,14 +280,14 @@
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_manualScrollStep">
<widget class="QLabel" name="label_stackOffsetX">
<property name="text">
<string>Manual scroll step size:</string>
<string>Horizontal offset for stacked columns:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep">
<widget class="QSpinBox" name="kcfg_stackOffsetX">
<property name="suffix">
<string> px</string>
</property>
@@ -301,6 +301,48 @@
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_stackOffsetY">
<property name="text">
<string>Vertical offset for stacked columns:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="kcfg_stackOffsetY">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_manualScrollStep">
<property name="text">
<string>Manual scroll step size:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="kcfg_manualScrollStep">
<property name="suffix">
<string> px</string>
</property>
<property name="maximum">
<number>999</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_presetWidths">
<property name="text">
<string>Preset widths:</string>
@@ -310,7 +352,7 @@
</property>
</widget>
</item>
<item row="7" column="1">
<item row="9" column="1">
<widget class="QLineEdit" name="kcfg_presetWidths">
<property name="toolTip">
<string>Comma-separated list of widths. Supported units: "px" and "%".</string>
@@ -318,14 +360,14 @@
</widget>
</item>
<item row="8" column="0">
<item row="10" column="0">
<widget class="QLabel" name="label_offScreenOpacity">
<property name="text">
<string>Obscured window opacity:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<item row="10" column="1">
<widget class="QSpinBox" name="kcfg_offScreenOpacity">
<property name="suffix">
<string> %</string>

View File

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

View File

@@ -5,6 +5,8 @@ type Config = {
gapsOuterRight: number;
gapsInnerHorizontal: number;
gapsInnerVertical: number;
stackOffsetX: number;
stackOffsetY: number;
manualScrollStep: number;
presetWidths: string;
offScreenOpacity: number;

View File

@@ -3,6 +3,10 @@ const defaultWindowRules = `[
"class": "(org\\\\.kde\\\\.)?plasmashell",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?polkit-kde-authentication-agent-1",
"tile": false
},
{
"class": "(org\\\\.kde\\\\.)?kded6",
"tile": false
@@ -80,6 +84,16 @@ const configDef = [
type: "UInt",
default: 8,
},
{
name: "stackOffsetX",
type: "UInt",
default: 8,
},
{
name: "stackOffsetY",
type: "UInt",
default: 32,
},
{
name: "manualScrollStep",
type: "UInt",

View File

@@ -54,7 +54,6 @@ type Output = { __brand: "Output" };
type KwinClient = {
__brand: "KwinClient";
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: Readonly<QmlSize>;
readonly transient: boolean;
@@ -79,7 +78,6 @@ type KwinClient = {
skipSwitcher: boolean;
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QmlRect;
desktops: KwinDesktop[]; // empty array means all desktops

View File

@@ -306,7 +306,7 @@ class Actions {
if (firstColumn === null) {
return;
}
grid.desktop.scrollToColumn(firstColumn);
grid.desktop.scrollToColumn(firstColumn, false);
}
public readonly gridScrollEnd = (cm: ClientManager, dm: DesktopManager) => {
@@ -315,11 +315,16 @@ class Actions {
if (lastColumn === null) {
return;
}
grid.desktop.scrollToColumn(lastColumn);
grid.desktop.scrollToColumn(lastColumn, false);
}
public readonly gridScrollFocused = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
grid.desktop.scrollCenterRange(column);
const scrollAmount = Range.minus(column, grid.desktop.getCurrentVisibleRange());
if (scrollAmount !== 0) {
grid.desktop.adjustScroll(scrollAmount, true);
} else {
grid.desktop.scrollToColumn(column, true);
}
}
public readonly gridScrollLeftColumn = (cm: ClientManager, dm: DesktopManager) => {
@@ -334,7 +339,7 @@ class Actions {
return;
}
grid.desktop.scrollToColumn(leftColumn);
grid.desktop.scrollToColumn(leftColumn, false);
}
public readonly gridScrollRightColumn = (cm: ClientManager, dm: DesktopManager) => {
@@ -349,7 +354,7 @@ class Actions {
return;
}
grid.desktop.scrollToColumn(rightColumn);
grid.desktop.scrollToColumn(rightColumn, false);
}
public readonly screenSwitch = (cm: ClientManager, dm: DesktopManager) => {

View File

@@ -106,7 +106,7 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
{
name: "column-toggle-stacked",
description: "Toggle stacked layout for focused column",
comment: "One window in the column visible, others shaded; not supported on Wayland",
comment: "Only the active window visible",
defaultKeySequence: "Meta+X",
action: () => world.doIfTiledFocused(actions.columnToggleStacked),
},

View File

@@ -219,42 +219,28 @@ class Column {
}
}
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
if (this.stacked && this.windows.length() >= 2) {
this.arrangeStacked(x);
return;
}
let y = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
window.client.setShade(false);
window.arrange(x, y, this.width, window.height);
y += window.height + this.grid.config.gapsInnerVertical;
}
}
public arrangeStacked(x: number) {
const expandedWindow = this.getFocusTaker();
let collapsedHeight;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.client.setShade(false);
} else {
window.client.setShade(true);
collapsedHeight = window.client.kwinClient.frameGeometry.height;
}
}
const nWindows = this.windows.length();
const windowWidth = this.width - (nWindows - 1) * this.grid.config.stackOffsetX;
const windowHeight = this.grid.desktop.tilingArea.height - (nWindows - 1) * this.grid.config.stackOffsetY;
const nCollapsed = this.getWindowCount() - 1;
const expandedHeight = this.grid.desktop.tilingArea.height - nCollapsed * (collapsedHeight! + this.grid.config.gapsInnerVertical);
let y = this.grid.desktop.tilingArea.y;
let windowX = x;
let windowY = this.grid.desktop.tilingArea.y;
for (const window of this.windows.iterator()) {
if (window === expandedWindow) {
window.arrange(x, y, this.width, expandedHeight);
y += expandedHeight;
} else {
window.arrange(x, y, this.width, window.height);
y += collapsedHeight!;
}
y += this.grid.config.gapsInnerVertical;
window.arrange(windowX, windowY, windowWidth, windowHeight);
windowX += this.grid.config.stackOffsetX;
windowY += this.grid.config.stackOffsetY;
}
}
@@ -266,15 +252,6 @@ class Column {
this.grid.desktop.onLayoutChanged();
}
private canStack() {
for (const window of this.windows.iterator()) {
if (!window.client.kwinClient.shadeable) {
return false;
}
}
return true;
}
public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) {
this.windows.insertEnd(window);

View File

@@ -73,9 +73,8 @@ class Desktop {
}
public scrollCenterRange(range: Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), true);
const scrollAmount = Range.minus(range, this.getCurrentVisibleRange());
this.adjustScroll(scrollAmount, true);
}
public scrollCenterVisible(focusedColumn: Column) {
@@ -91,11 +90,11 @@ class Desktop {
return;
}
this.scrollToColumn(focusedColumn);
this.scrollToColumn(focusedColumn, false);
}
public scrollToColumn(column: Column) {
if (this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
public scrollToColumn(column: Column, force: boolean) {
if (force || this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
this.config.scroller.scrollToColumn(this, column);
}
}

View File

@@ -190,7 +190,7 @@ class Grid {
lastFocusedColumn.restoreToTiled();
}
this.lastFocusedColumn = column;
this.desktop.scrollToColumn(column);
this.desktop.scrollToColumn(column, false);
}
public onScreenSizeChanged() {

View File

@@ -1,6 +1,8 @@
type LayoutConfig = {
gapsInnerHorizontal: number;
gapsInnerVertical: number;
stackOffsetX: number;
stackOffsetY: number;
offScreenOpacity: number;
stackColumnsByDefault: boolean;
resizeNeighborColumn: boolean;

View File

@@ -20,6 +20,12 @@ namespace Range {
child.getRight() <= parent.getRight();
}
export function minus(a: Range, b: Range) {
const aCenter = a.getLeft() + a.getWidth() / 2;
const bCenter = b.getLeft() + b.getWidth() / 2;
return Math.round(aCenter - bCenter);
}
class Basic {
constructor(
private readonly x: number,

View File

@@ -8,11 +8,17 @@ class Window {
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height;
let maximizedMode = this.client.getMaximizedMode();
if (maximizedMode === undefined) {
maximizedMode = MaximizedMode.Unmaximized; // defaulting to unmaximized, as this is set in Tiled.prepareClientForTiling
}
this.focusedState = {
fullScreen: false,
maximizedMode: MaximizedMode.Unmaximized,
fullScreen: this.client.kwinClient.fullScreen,
maximizedMode: maximizedMode,
};
this.skipArrange = false;
this.skipArrange = this.client.kwinClient.fullScreen || maximizedMode !== MaximizedMode.Unmaximized;
this.column = column;
column.onWindowAdded(this, true);
}
@@ -54,10 +60,6 @@ class Window {
}
public focus() {
if (this.client.isShaded()) {
// workaround for KWin deactivating clients when unshading immediately after activation
this.client.setShade(false);
}
this.client.focus();
}

View File

@@ -16,6 +16,8 @@ class WindowRuleEnforcer {
!kwinClient.transient &&
kwinClient.managed &&
kwinClient.pid > -1 &&
!kwinClient.fullScreen &&
!Clients.isFullScreenGeometry(kwinClient) &&
!this.preferFloating.matches(kwinClient)
);
}

View File

@@ -35,8 +35,6 @@ class ClientManager {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (
Clients.canTileEver(kwinClient) &&
!kwinClient.fullScreen &&
!Clients.isFullScreenGeometry(kwinClient) &&
this.windowRuleEnforcer.shouldTile(kwinClient)
) {
Clients.makeTileable(kwinClient);

View File

@@ -108,16 +108,6 @@ class ClientWrapper {
});
}
public setShade(shade: boolean) {
this.manipulatingGeometry.do(() => {
this.kwinClient.shade = shade;
});
}
public isShaded() {
return this.kwinClient.shade;
}
public getMaximizedMode() {
return this.maximizedMode;
}

View File

@@ -5,8 +5,8 @@ namespace Clients {
];
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.moveable &&
kwinClient.resizeable &&
const shapeable = (kwinClient.moveable && kwinClient.resizeable) || kwinClient.fullScreen; // full-screen windows may become shapeable after exiting full-screen mode
return shapeable &&
!kwinClient.popupWindow &&
!prohibitedClasses.includes(kwinClient.resourceClass);
}

View File

@@ -40,6 +40,8 @@ class World {
const layoutConfig = {
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
stackOffsetX: config.stackOffsetX,
stackOffsetY: config.stackOffsetY,
offScreenOpacity: config.offScreenOpacity / 100.0,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,

View File

@@ -69,6 +69,7 @@ namespace ClientState {
});
});
let moving = false;
let resizing = false;
let resizeStartWidth = 0;
let resizeNeighbor: { column: Column, startWidth: number } | undefined;
@@ -78,6 +79,8 @@ namespace ClientState {
world.do((clientManager, desktopManager) => {
clientManager.floatClient(client);
});
} else {
moving = true;
}
return;
}
@@ -99,6 +102,10 @@ namespace ClientState {
});
manager.connect(kwinClient.interactiveMoveResizeFinished, () => {
if (moving) {
moving = false;
world.do(() => window.column.grid.desktop.onLayoutChanged()); // move the dragged window back to its position
}
if (resizing) {
resizing = false;
resizeNeighbor = undefined;
@@ -160,7 +167,15 @@ namespace ClientState {
});
manager.connect(kwinClient.fullScreenChanged, () => {
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
world.do((clientManager, desktopManager) => {
// some clients only turn out to be untileable after exiting full-screen mode
if (!Clients.canTileEver(kwinClient)) {
clientManager.floatClient(client);
return;
}
window.onFullScreenChanged(kwinClient.fullScreen);
});
});
manager.connect(kwinClient.tileChanged, () => {
@@ -205,7 +220,6 @@ namespace ClientState {
client.kwinClient.keepBelow = true;
}
client.kwinClient.keepAbove = false;
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
}
@@ -222,7 +236,6 @@ namespace ClientState {
if (config.offScreenOpacity < 1.0) {
client.kwinClient.opacity = 1.0;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);

View File

@@ -11,13 +11,33 @@ tests.register("Center focused", 1, () => {
Assert.assert(workspaceMock.activeWindow === client2);
Assert.columnsFillTilingArea([client0, client1, client2]);
// center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, screen, client2);
Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
qtMock.fireShortcut("karousel-focus-left");
Assert.centered(config, screen, client2, { message: "No scrolling should have occured" });
// undo center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
// center client2
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.centered(config, tilingArea, client2);
Assert.fullyVisible(client1.frameGeometry);
Assert.fullyVisible(client2.frameGeometry);
// 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);
// center client1
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
// undo center client1 (no scrolling should occur, because all clients are already visible and centered)
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.columnsFillTilingArea([client0, client1, client2]);
});

View File

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

View File

@@ -8,7 +8,7 @@ tests.register("External resize", 1, () => {
function getTiledFrame(width: number) {
return new MockQmlRect(
Math.round((screen.width - width) / 2),
tilingArea.left + Math.round((tilingArea.width - width) / 2),
tilingArea.top,
width,
tilingArea.height,

View File

@@ -12,7 +12,7 @@ tests.register("Focus and move windows", 1, () => {
function testLayout(shortcutName: string, grid: KwinClient[][]) {
qtMock.fireShortcut(shortcutName);
Assert.grid(config, screen, 100, grid, true, { skip: 1 });
Assert.grid(config, tilingArea, 100, grid, true, [], { skip: 1 });
}
function testFocus(shortcutName: string, expectedFocus: KwinClient) {

View File

@@ -6,13 +6,13 @@ tests.register("LazyScroller", 20, () => {
const { qtMock, workspaceMock, world } = init(config);
const [client1] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1]], true);
Assert.grid(config, tilingArea, 300, [[client1]], true);
const [client2] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2]], true);
Assert.grid(config, tilingArea, 300, [[client1], [client2]], true);
const [client3] = workspaceMock.createClientsWithWidths(300);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
@@ -20,7 +20,7 @@ tests.register("LazyScroller", 20, () => {
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-left"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client3.frameGeometry.right, tilingArea.right);
runOneOf(
@@ -30,18 +30,18 @@ tests.register("LazyScroller", 20, () => {
() => qtMock.fireShortcut("karousel-focus-start"),
);
workspaceMock.activeWindow = client1;
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
qtMock.fireShortcut("karousel-grid-scroll-focused");
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, screen, 300, [[client1]], true);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1]], true);
runOneOf(
() => workspaceMock.activeWindow = client2,
() => qtMock.fireShortcut("karousel-focus-2"),
() => qtMock.fireShortcut("karousel-focus-right"),
);
Assert.grid(config, screen, 300, [[client1], [client2], [client3]], false);
Assert.grid(config, tilingArea, 300, [[client1], [client2], [client3]], false);
Assert.equal(client1.frameGeometry.left, tilingArea.left);
});

View File

@@ -7,7 +7,7 @@ tests.register("Maximization", 100, () => {
Assert.assert(clientManager.hasClient(kwinClient));
});
const columnLeftX = screen.width/2 - 300/2;
const columnLeftX = tilingArea.left + tilingArea.width/2 - 300/2;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
Assert.rect(kwinClient.frameGeometry, columnLeftX, columnTopY, 300, columnHeight);
@@ -66,7 +66,7 @@ tests.register("Re-maximize disabled", 100, () => {
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column1LeftX = tilingArea.left + tilingArea.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
@@ -111,7 +111,7 @@ tests.register("Re-maximize enabled", 100, () => {
});
const columnsWidth = 300 + 400 + config.gapsInnerHorizontal;
const column1LeftX = screen.width/2 - columnsWidth/2;
const column1LeftX = tilingArea.left + tilingArea.width/2 - columnsWidth/2;
const column2LeftX = column1LeftX + 300 + config.gapsInnerHorizontal;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
@@ -143,3 +143,72 @@ tests.register("Re-maximize enabled", 100, () => {
Assert.rect(client1.frameGeometry, column1LeftX, columnTopY, 300, columnHeight);
Assert.equalRects(client2.frameGeometry, screen);
});
tests.register("Start full-screen", 100, () => {
const config = getDefaultConfig();
config.reMaximize = true;
const { qtMock, workspaceMock, world } = init(config);
const [windowedClient] = workspaceMock.createClientsWithWidths(300);
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
fullScreenClient.resourceClass = "full-screen-app";
fullScreenClient.fullScreen = true;
workspaceMock.createWindows(fullScreenClient);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(windowedClient));
Assert.assert(clientManager.hasClient(fullScreenClient));
});
Assert.centered(config, tilingArea, windowedClient);
Assert.equalRects(fullScreenClient.frameGeometry, screen);
Assert.equal(Workspace.activeWindow, fullScreenClient);
{
qtMock.fireShortcut("karousel-focus-left");
const opts = { message: "fullScreenClient is not in the grid, so we can't move focus directionally" };
Assert.centered(config, tilingArea, windowedClient, opts);
Assert.equalRects(fullScreenClient.frameGeometry, screen, opts);
Assert.equal(Workspace.activeWindow, fullScreenClient, opts);
}
{
qtMock.fireShortcut("karousel-focus-1");
const opts = { message: "fullScreenClient is not in grid, so it should stay full-screen" };
Assert.centered(config, tilingArea, windowedClient, opts);
Assert.equalRects(fullScreenClient.frameGeometry, screen, opts);
Assert.equal(Workspace.activeWindow, windowedClient);
}
});
tests.register("Start full-screen (force tiling)", 100, () => {
const config = getDefaultConfig();
config.reMaximize = true;
config.windowRules = '[{ "class": "full-screen-app", "tile": true }]';
const { qtMock, workspaceMock, world } = init(config);
const column1Width = 300;
const [windowedClient] = workspaceMock.createClientsWithWidths(column1Width);
const fullScreenClient = new MockKwinClient(new MockQmlRect(0, 0, 400, 200));
fullScreenClient.resourceClass = "full-screen-app";
fullScreenClient.fullScreen = true;
workspaceMock.createWindows(fullScreenClient);
world.do((clientManager, desktopManager) => {
Assert.assert(clientManager.hasClient(windowedClient));
Assert.assert(clientManager.hasClient(fullScreenClient));
});
Assert.equalRects(fullScreenClient.frameGeometry, screen);
Assert.equal(Workspace.activeWindow, fullScreenClient);
const column2Width = tilingArea.width;
const column1LeftX = tilingArea.left;
const column2LeftX = column1LeftX + column1Width + gapH;
const columnTopY = tilingArea.top;
const columnHeight = tilingArea.height;
qtMock.fireShortcut("karousel-focus-left");
const opts = { message: "fullScreenClient should be restored from full-screen mode to tiled mode" };
Assert.rect(windowedClient.frameGeometry, column1LeftX, columnTopY, column1Width, columnHeight, opts);
Assert.rect(fullScreenClient.frameGeometry, column2LeftX, columnTopY, column2Width, columnHeight, opts);
Assert.equal(Workspace.activeWindow, windowedClient);
});

View File

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

View File

@@ -7,7 +7,7 @@ tests.register("Preset Widths default", 1, () => {
function getRect(columnWidth: number) {
return new MockQmlRect(
(screen.width - columnWidth) / 2,
tilingArea.left + (tilingArea.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,
@@ -43,7 +43,7 @@ tests.register("Preset Widths custom", 1, () => {
function getRect(columnWidth: number) {
return new MockQmlRect(
(screen.width - columnWidth) / 2,
tilingArea.left + (tilingArea.width - columnWidth) / 2,
tilingArea.top,
columnWidth,
tilingArea.height,

View File

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

View File

@@ -1,4 +1,7 @@
tests.register("WindowRuleEnforcer", 1, () => {
screen = new MockQmlRect(0, 0, 800, 600);
Workspace = new MockWorkspace();
const testCases = [
{ tiledByDefault: true, resourceClass: "unknown", caption: "anything", shouldTile: true },
{ tiledByDefault: false, resourceClass: "unknown", caption: "anything", shouldTile: false },
@@ -25,6 +28,7 @@ tests.register("WindowRuleEnforcer", 1, () => {
return {
normalWindow: normalWindow,
transient: false,
clientGeometry: new MockQmlRect(0, 0, 200, 200),
managed: true,
pid: 100,
moveable: true,

View File

@@ -4,6 +4,7 @@ tests.register("Clients.canTileEver", 1, () => {
{ clientProperties: { resourceClass: "app", caption: "Title", moveable: false }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", resizeable: false }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", normalWindow: false, popupWindow: true }, tileable: false },
{ clientProperties: { resourceClass: "app", caption: "Caption", moveable: false, resizeable: false, fullScreen: true }, tileable: true },
{ clientProperties: { resourceClass: "ksmserver-logout-greeter", caption: "Caption" }, tileable: false },
{ clientProperties: { resourceClass: "xwaylandvideobridge", caption: "" }, tileable: false },
];
@@ -24,6 +25,7 @@ tests.register("Clients.canTileEver", 1, () => {
pid: 100,
moveable: true,
resizeable: true,
fullScreen: false,
popupWindow: false,
minimized: false,
desktops: [1],

View File

@@ -125,39 +125,51 @@ namespace Assert {
export function grid(
config: Config,
screen: QmlRect,
tilingArea: QmlRect,
columnWidth: number,
grid: KwinClient[][],
centered: boolean,
stackedColumns: number[] = [],
{ message, skip=0 }: Options = {},
) {
const nColumns = grid.length;
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
const columnsWidth = nColumns * columnWidth + (nColumns-1) * config.gapsInnerHorizontal;
const startX = centered ?
screen.x + (screen.width - columnsWidth) / 2 :
tilingArea.x + (tilingArea.width - columnsWidth) / 2 :
grid[0][0].frameGeometry.x;
// assumes uniformly sized windows within columns of uniform width
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows;
const windowHeight = (tilingArea.height - config.gapsInnerVertical * (nWindows-1)) / nWindows;
return new MockQmlRect(
startX + column * (columnWidth + config.gapsInnerHorizontal),
screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window,
tilingArea.y + (windowHeight + config.gapsInnerVertical) * window,
columnWidth,
(columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows,
(tilingArea.height - config.gapsInnerVertical * (nWindows-1)) / nWindows,
);
}
function getRectInGridStacked(column: number, window: number, nColumns: number, nWindows: number) {
const columnX = startX + column * (columnWidth + config.gapsInnerHorizontal);
return new MockQmlRect(
columnX + window * config.stackOffsetX,
tilingArea.y + window * config.stackOffsetY,
columnWidth - (nWindows-1) * config.stackOffsetX,
tilingArea.height - (nWindows-1) * config.stackOffsetY,
);
}
for (let iColumn = 0; iColumn < nColumns; iColumn++) {
const column = grid[iColumn];
const stacked = stackedColumns.includes(iColumn);
const getRect = stacked ? getRectInGridStacked : getRectInGrid;
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 },
getRect(iColumn, iWindow, nColumns, nWindows),
{ message: appendMessage(`column ${iColumn}, window ${iWindow}`, message), skip: skip+1 },
);
}
}
@@ -165,16 +177,17 @@ namespace Assert {
export function centered(
config: Config,
screen: QmlRect,
tilingArea: QmlRect,
client:KwinClient,
{ message, skip=0 }: Options = {},
) {
grid(
config,
screen,
tilingArea,
client.frameGeometry.width,
[[client]],
true,
[],
{ message: appendMessage("Window not centered", message), skip: skip+1 },
);
}
@@ -219,4 +232,26 @@ namespace Assert {
}
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options);
}
export function tiledClient(
clientManager: ClientManager,
client: KwinClient,
{ message, skip=0 }: Options = {},
) {
assert(
clientManager.findTiledWindow(client) !== null,
{ message: message, skip: skip+1 },
);
}
export function notTiledClient(
clientManager: ClientManager,
client: KwinClient,
{ message, skip=0 }: Options = {},
) {
assert(
clientManager.findTiledWindow(client) === null,
{ message: message, skip: skip+1 },
);
}
}

View File

@@ -35,7 +35,7 @@ function init(config: Config) {
function getGridBounds(clientLeft: KwinClient, clientRight: KwinClient) {
const columnsWidth = clientRight.frameGeometry.right - clientLeft.frameGeometry.left;
const left = Math.floor((screen.width - columnsWidth) / 2);
const left = tilingArea.left + Math.floor((tilingArea.width - columnsWidth) / 2);
const right = left + columnsWidth;
return { left, right };
}
@@ -44,3 +44,10 @@ function getWindowHeight(windowsInColumn: number) {
const totalGaps = (windowsInColumn-1) * gapV;
return Math.round((tilingArea.height - totalGaps) / windowsInColumn);
}
function getClientManager(world: World): ClientManager {
// don't do this outside of tests
let clientManager;
world.do((cm, dm) => clientManager = cm);
return clientManager!;
}

View File

@@ -3,18 +3,15 @@ class MockKwinClient {
private static readonly borderThickness = 10;
public readonly shadeable: boolean = false;
public caption = "App";
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public readonly transient: boolean;
public readonly move: boolean = false;
public 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 resourceClass = "app";
public readonly dock: boolean = false;
public readonly normalWindow: boolean = true;
public readonly managed: boolean = true;
@@ -28,7 +25,6 @@ class MockKwinClient {
public skipSwitcher: boolean = false;
public keepAbove: boolean = false;
public keepBelow: boolean = false;
public shade: boolean = false;
private _minimized: boolean = false;
private _desktops: KwinDesktop[] = [];
private _tile: Tile|null = null;
@@ -96,6 +92,14 @@ class MockKwinClient {
}
}
public get moveable() {
return !this._fullScreen;
}
public get resizeable() {
return !this._fullScreen;
}
public get fullScreen() {
return this._fullScreen;
}

View File

@@ -62,6 +62,28 @@ class MockWorkspace {
};
}
public moveWindow(window: MockKwinClient, ...deltas: QmlPoint[]) {
const frame = window.getFrameGeometryCopy();
window.move = true;
window.interactiveMoveResizeStarted.fire();
for (const delta of deltas) {
if (delta.x !== 0) {
frame.x += delta.x;
}
if (delta.y !== 0) {
frame.y += delta.y;
}
runOneOf(
() => window.frameGeometry.set(frame),
() => window.frameGeometry = frame,
);
}
window.move = false;
window.interactiveMoveResizeFinished.fire();
}
public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) {
const frame = window.getFrameGeometryCopy();
if (edgeResize) {
@@ -94,7 +116,7 @@ class MockWorkspace {
runOneOf(
() => window.frameGeometry.set(frame),
() => window.frameGeometry = frame,
)
);
}
window.resize = false;