37 Commits
v0.10 ... v0.11

Author SHA1 Message Date
Peter Fajdiga
47f4bbd9b6 bump version to 0.11 2025-01-18 13:24:43 +01:00
Peter Fajdiga
2d4ad73d16 tests: always use MockQSignal<[]> for signals without parameters 2025-01-15 20:59:43 +01:00
Peter Fajdiga
bb4e4f8ebd tests: add more events and assertions to passFocus 2025-01-15 20:56:16 +01:00
Peter Fajdiga
0742975334 MockWorkspace: unset activeWindow when the active windows is removed 2025-01-15 19:13:22 +01:00
Peter Fajdiga
64457429d0 pass focus when moving a window to another desktop 2025-01-15 19:01:24 +01:00
Peter Fajdiga
02154f2f5e MockKwinClient: unset activeWindow when the active windows moves to an inactive desktop 2025-01-15 14:22:45 +01:00
Peter Fajdiga
0a2bb4f65d MockKwinClient: fire desktopsChanged signal 2025-01-15 14:08:24 +01:00
Peter Fajdiga
6f207e59c4 MockKwinClient: make underscored properties private 2025-01-15 11:56:13 +01:00
Peter Fajdiga
4c987b6c5b pass focus when moving a column to another desktop 2025-01-15 11:48:26 +01:00
Peter Fajdiga
bca0158df9 tests: give mock clients numbered captions 2025-01-15 11:47:26 +01:00
Peter Fajdiga
9feeb0f23e prevent unnecessary scrolling on focus change (fixes #67) 2025-01-15 10:40:46 +01:00
Peter Fajdiga
0241846ea5 MockKwinClient: only fire maximizedAboutToChange if there's an actual change 2025-01-14 19:34:14 +01:00
Peter Fajdiga
3bf3f16f49 MockKwinClient: initialize windowed to true 2025-01-14 19:31:27 +01:00
Peter Fajdiga
782a6db56d readme: prepend "Then" 2025-01-09 17:51:25 +01:00
Peter Fajdiga
93b6850ffd readme: mention org.kde.notification in Installation 2025-01-09 17:46:25 +01:00
Peter Fajdiga
5f0c637d1a Actions.gridScroll: simplify 2024-12-23 20:03:57 +01:00
Himadri Bhattacharjee
8829d0b291 add ability to cycle preset widths in reverse (#75) (resolves #74)
* feat: add ability to cycle preset widths in reverse

* tests: add tests for cycling widths in reverse
2024-12-22 16:57:54 +01:00
Peter Fajdiga
d37b4bc5d1 tests: add test case for no layering 2024-11-08 16:05:18 +01:00
Peter Fajdiga
ead29e5e69 rename file 3-feature_request.md 2024-11-08 10:15:26 +01:00
Peter Fajdiga
ff75d931f6 Update issue templates 2024-11-08 10:13:51 +01:00
Peter Fajdiga
d00d514d30 stop setting keepAbove in destroy methods of Floating and Pinned 2024-11-05 21:48:35 +01:00
Peter Fajdiga
3b919909dc prevent unpinned windows from retaining keepAbove 2024-11-05 21:46:26 +01:00
Peter Fajdiga
0004b6f921 fillSpace: rename to fenceposts 2024-11-05 19:37:46 +01:00
Peter Fajdiga
24265c56f9 tests: lazyScroller: add steps 2024-10-27 23:37:19 +01:00
Peter Fajdiga
dcbc0a474d Range: reorder code 2024-10-27 23:32:39 +01:00
Peter Fajdiga
88f170f5c1 Range.Basic: unexport 2024-10-27 23:31:31 +01:00
Peter Fajdiga
78ab48ee09 Range.Basic: define properties in constructor 2024-10-27 23:27:14 +01:00
Peter Fajdiga
b2d81796f8 replace Range.Basic.fromRanges with Range.fromRanges 2024-10-27 23:20:17 +01:00
Peter Fajdiga
7d27331ce5 replace Range.Basic.contains with Range.contains 2024-10-27 23:18:21 +01:00
Peter Fajdiga
55e1037a7b extract Range into Range.ts 2024-10-27 23:14:13 +01:00
Peter Fajdiga
7820c7d00e replace Column.isVisible with RangeImpl.contains 2024-10-27 23:08:52 +01:00
Peter Fajdiga
3d8ca0bc14 Grid: remove function 2024-10-27 22:46:45 +01:00
Peter Fajdiga
eaf68b87f9 tests: create LazyScroller test 2024-10-27 22:29:00 +01:00
Peter Fajdiga
b2dfad6042 tests: Assert.grid: add centered parameter 2024-10-27 21:13:14 +01:00
Peter Fajdiga
054808cb38 tests: Assert.centeredGrid: add width parameter 2024-10-27 20:29:11 +01:00
Peter Fajdiga
97059fa4f7 readme: add installation instructions 2024-10-27 16:20:07 +01:00
Peter Fajdiga
5e7959c7f4 readme: update key bindings 2024-10-27 14:36:55 +01:00
30 changed files with 571 additions and 139 deletions

View File

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

View File

@@ -22,6 +22,13 @@ Karousel requires the following QML modules:
- Doesn't support windows on all desktops - Doesn't support windows on all desktops
- Doesn't support windows on multiple activities - Doesn't support windows on multiple activities
## Installation
First install the _org.kde.notification_ QML module (_qml-module-org-kde-notifications_ package on Ubuntu).
Then download the [latest release](https://github.com/peterfajdiga/karousel/releases/latest) and extract it into _~/.local/share/kwin/scripts/_.
Or clone the repo and run `make install` (requires node and tsc).
## Key bindings ## Key bindings
The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts. The key bindings can be configured in KDE System Settings among KWin's own keyboard shortcuts.
Here's the default ones: Here's the default ones:
@@ -32,12 +39,16 @@ Here's the default ones:
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) | | Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) | | Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) | | Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| (unassigned) | Move focus to the next window in grid |
| (unassigned) | Move focus to the previous window in grid |
| Meta+Home | Move focus to start | | Meta+Home | Move focus to start |
| Meta+End | Move focus to end | | Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) | | Meta+Shift+A | Move window left (Moves window out of and into columns) |
| Meta+Shift+D | Move window right (Moves window out of and into columns) | | Meta+Shift+D | Move window right (Moves window out of and into columns) |
| Meta+Shift+W | Move window up | | Meta+Shift+W | Move window up |
| Meta+Shift+S | Move window down | | Meta+Shift+S | Move window down |
| (unassigned) | Move window to the next position in grid |
| (unassigned) | Move window to the previous position in grid |
| Meta+Shift+Home | Move window to start | | Meta+Shift+Home | Move window to start |
| Meta+Shift+End | Move window to end | | 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 (One window in the column visible, others shaded; not supported on Wayland) |
@@ -47,7 +58,10 @@ Here's the default ones:
| Meta+Ctrl+Shift+End | Move column to end | | Meta+Ctrl+Shift+End | Move column to end |
| Meta+Ctrl++ | Increase column width | | Meta+Ctrl++ | Increase column width |
| Meta+Ctrl+- | Decrease column width | | Meta+Ctrl+- | Decrease column width |
| Meta+R | Cycle through preset column widths |
| Meta+Ctrl+X | Equalize widths of visible columns | | Meta+Ctrl+X | Equalize widths of visible columns |
| Meta+Ctrl+A | Squeeze left column onto the screen (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Ctrl+D | Squeeze right column onto the screen |
| Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) | | Meta+Alt+Return | Center focused window (Scrolls so that the focused window is centered in the screen) |
| Meta+Alt+A | Scroll one column to the left | | Meta+Alt+A | Scroll one column to the left |
| Meta+Alt+D | Scroll one column to the right | | Meta+Alt+D | Scroll one column to the right |

View File

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

View File

@@ -11,6 +11,12 @@ class PresetWidths {
return nextIndex >= 0 ? widths[nextIndex] : widths[0]; return nextIndex >= 0 ? widths[nextIndex] : widths[0];
} }
public prev(currentWidth: number, minWidth: number, maxWidth: number) {
const widths = this.getWidths(minWidth, maxWidth).reverse();
const nextIndex = widths.findIndex(width => width < currentWidth);
return nextIndex >= 0 ? widths[nextIndex] : widths[0];
}
public getWidths(minWidth: number, maxWidth: number) { public getWidths(minWidth: number, maxWidth: number) {
const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth)); const widths = this.presets.map(f => clamp(f(maxWidth), minWidth, maxWidth));
widths.sort((a, b) => a - b); widths.sort((a, b) => a - b);

View File

@@ -9,7 +9,7 @@ class ContextualResizer {
const visibleRange = desktop.getCurrentVisibleRange(); const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth(); const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth(); const maxWidth = column.getMaxWidth();
if(!column.isVisible(visibleRange, true) || column.getWidth() >= maxWidth) { if(!Range.contains(visibleRange, column) || column.getWidth() >= maxWidth) {
return; return;
} }
@@ -46,7 +46,7 @@ class ContextualResizer {
const visibleRange = desktop.getCurrentVisibleRange(); const visibleRange = desktop.getCurrentVisibleRange();
const minWidth = column.getMinWidth(); const minWidth = column.getMinWidth();
const maxWidth = column.getMaxWidth(); const maxWidth = column.getMaxWidth();
if(!column.isVisible(visibleRange, true) || column.getWidth() <= minWidth) { if(!Range.contains(visibleRange, column) || column.getWidth() <= minWidth) {
return; return;
} }

View File

@@ -189,6 +189,11 @@ class Actions {
column.setWidth(nextWidth, true); column.setWidth(nextWidth, true);
} }
public readonly cyclePresetWidthsReverse = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
const nextWidth = this.config.presetWidths.prev(column.getWidth(), column.getMinWidth(), column.getMaxWidth());
column.setWidth(nextWidth, true);
}
public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => { public readonly columnsWidthEqualize = (cm: ClientManager, dm: DesktopManager) => {
const desktop = dm.getCurrentDesktop(); const desktop = dm.getCurrentDesktop();
const visibleRange = desktop.getCurrentVisibleRange(); const visibleRange = desktop.getCurrentVisibleRange();
@@ -202,7 +207,7 @@ class Actions {
); );
visibleColumns.forEach((column, index) => column.setWidth(widths[index], true)); visibleColumns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges( desktop.scrollCenterRange(Range.fromRanges(
visibleColumns[0], visibleColumns[0],
visibleColumns[visibleColumns.length - 1], visibleColumns[visibleColumns.length - 1],
)); ));
@@ -210,7 +215,7 @@ class Actions {
public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => { public readonly columnsSqueezeLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange(); const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!focusedColumn.isVisible(visibleRange, true)) { if (!Range.contains(visibleRange, focusedColumn)) {
return; return;
} }
@@ -237,7 +242,7 @@ class Actions {
public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => { public readonly columnsSqueezeRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange(); const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!focusedColumn.isVisible(visibleRange, true)) { if (!Range.contains(visibleRange, focusedColumn)) {
return; return;
} }
@@ -279,7 +284,7 @@ class Actions {
const widths = fillSpace(availableSpace - gapsWidth, columnConstraints); const widths = fillSpace(availableSpace - gapsWidth, columnConstraints);
columns.forEach((column, index) => column.setWidth(widths[index], true)); columns.forEach((column, index) => column.setWidth(widths[index], true));
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(firstColumn, lastColumn)); desktop.scrollCenterRange(Range.fromRanges(firstColumn, lastColumn));
return true; return true;
} }
@@ -292,8 +297,7 @@ class Actions {
} }
private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => { private readonly gridScroll = (desktopManager: DesktopManager, amount: number) => {
const grid = desktopManager.getCurrentDesktop().grid; desktopManager.getCurrentDesktop().adjustScroll(amount, false);
grid.desktop.adjustScroll(amount, false);
} }
public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => { public readonly gridScrollStart = (cm: ClientManager, dm: DesktopManager) => {
@@ -410,7 +414,10 @@ class Actions {
namespace Actions { namespace Actions {
export type Config = { export type Config = {
manualScrollStep: number; manualScrollStep: number;
presetWidths: { next: (currentWidth: number, minWidth: number, maxWidth: number) => number }; presetWidths: {
next: (currentWidth: number, minWidth: number, maxWidth: number) => number;
prev: (currentWidth: number, minWidth: number, maxWidth: number) => number
};
columnResizer: ColumnResizer; columnResizer: ColumnResizer;
}; };

View File

@@ -152,6 +152,12 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
defaultKeySequence: "Meta+R", defaultKeySequence: "Meta+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidths), action: () => world.doIfTiledFocused(actions.cyclePresetWidths),
}, },
{
name: "cycle-preset-widths-reverse",
description: "Cycle through preset column widths in reverse",
defaultKeySequence: "Meta+Shift+R",
action: () => world.doIfTiledFocused(actions.cyclePresetWidthsReverse),
},
{ {
name: "columns-width-equalize", name: "columns-width-equalize",
description: "Equalize widths of visible columns", description: "Equalize widths of visible columns",

View File

@@ -21,7 +21,7 @@ class Column {
if (targetGrid === this.grid) { if (targetGrid === this.grid) {
this.grid.moveColumn(this, leftColumn); this.grid.moveColumn(this, leftColumn);
} else { } else {
this.grid.onColumnRemoved(this, false); this.grid.onColumnRemoved(this, this.isFocused());
this.grid = targetGrid; this.grid = targetGrid;
targetGrid.onColumnAdded(this, leftColumn); targetGrid.onColumnAdded(this, leftColumn);
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
@@ -203,9 +203,17 @@ class Column {
window.focus(); window.focus();
} }
public isFocused() {
const lastFocusedWindow = this.grid.getLastFocusedWindow();
if (lastFocusedWindow === null) {
return false;
}
return lastFocusedWindow.column === this && lastFocusedWindow.isFocused();
}
public arrange(x: number, visibleRange: Range, forceOpaque: boolean) { public arrange(x: number, visibleRange: Range, forceOpaque: boolean) {
if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) { if (this.grid.config.offScreenOpacity < 1.0 && !forceOpaque) {
const opacity = this.isVisible(visibleRange, true) ? 100 : this.grid.config.offScreenOpacity; const opacity = Range.contains(visibleRange, this) ? 100 : this.grid.config.offScreenOpacity;
for (const window of this.windows.iterator()) { for (const window of this.windows.iterator()) {
window.client.kwinClient.opacity = opacity; window.client.kwinClient.opacity = opacity;
} }
@@ -267,16 +275,6 @@ class Column {
return true; return true;
} }
public isVisible(visibleRange: Desktop.Range, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= visibleRange.getLeft() &&
this.getRight() <= visibleRange.getRight();
} else {
return this.getRight() + this.grid.config.gapsInnerHorizontal > visibleRange.getLeft() &&
this.getLeft() - this.grid.config.gapsInnerHorizontal < visibleRange.getRight();
}
}
public onWindowAdded(window: Window, bottom: boolean) { public onWindowAdded(window: Window, bottom: boolean) {
if (bottom) { if (bottom) {
this.windows.insertEnd(window); this.windows.insertEnd(window);

View File

@@ -55,7 +55,7 @@ class Desktop {
) )
} }
public scrollIntoView(range: Desktop.Range) { public scrollIntoView(range: Range) {
const left = range.getLeft(); const left = range.getLeft();
const right = range.getRight(); const right = range.getRight();
const initialVisibleRange = this.getCurrentVisibleRange(); const initialVisibleRange = this.getCurrentVisibleRange();
@@ -72,7 +72,7 @@ class Desktop {
this.setScroll(targetScrollX, false); this.setScroll(targetScrollX, false);
} }
public scrollCenterRange(range: Desktop.Range) { public scrollCenterRange(range: Range) {
const windowCenter = range.getLeft() + range.getWidth() / 2; const windowCenter = range.getLeft() + range.getWidth() / 2;
const screenCenter = this.scrollX + this.tilingArea.width / 2; const screenCenter = this.scrollX + this.tilingArea.width / 2;
this.adjustScroll(Math.round(windowCenter - screenCenter), true); this.adjustScroll(Math.round(windowCenter - screenCenter), true);
@@ -95,13 +95,13 @@ class Desktop {
} }
public scrollToColumn(column: Column) { public scrollToColumn(column: Column) {
if (this.dirtyScroll || !column.isVisible(this.getCurrentVisibleRange(), true)) { if (this.dirtyScroll || !Range.contains(this.getCurrentVisibleRange(), column)) {
this.config.scroller.scrollToColumn(this, column); this.config.scroller.scrollToColumn(this, column);
} }
} }
private getVisibleRange(scrollX: number) { private getVisibleRange(scrollX: number) {
return new Desktop.RangeImpl(scrollX, this.tilingArea.width); return Range.create(scrollX, this.tilingArea.width);
} }
public getCurrentVisibleRange() { public getCurrentVisibleRange() {
@@ -135,6 +135,10 @@ class Desktop {
this.dirty = false; this.dirty = false;
} }
public forceArrange() {
this.dirty = true;
}
public onLayoutChanged() { public onLayoutChanged() {
this.dirty = true; this.dirty = true;
this.dirtyScroll = true; this.dirtyScroll = true;
@@ -161,40 +165,6 @@ namespace Desktop {
clamper: Desktop.Clamper; clamper: Desktop.Clamper;
}; };
export type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
};
export class RangeImpl {
private readonly x: number;
private readonly width: number;
constructor(x: number, width: number) {
this.x = x;
this.width = width;
}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
public static fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new RangeImpl(left, right - left);
}
}
export class ColumnRange { export class ColumnRange {
private left: Column; private left: Column;
private right: Column; private right: Column;
@@ -206,7 +176,7 @@ namespace Desktop {
this.width = initialColumn.getWidth(); this.width = initialColumn.getWidth();
} }
public addNeighbors(visibleRange: Desktop.Range, gap: number) { public addNeighbors(visibleRange: Range, gap: number) {
const grid = this.left.grid; const grid = this.left.grid;
const columnRange = this; const columnRange = this;

View File

@@ -1,5 +1,3 @@
import Range = Desktop.Range;
class Grid { class Grid {
public readonly desktop: Desktop; public readonly desktop: Desktop;
public readonly config: LayoutConfig; public readonly config: LayoutConfig;
@@ -106,19 +104,19 @@ class Grid {
this.width = x - this.config.gapsInnerHorizontal; this.width = x - this.config.gapsInnerHorizontal;
} }
public getLeftmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) { public getLeftmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) { if (Range.contains(visibleRange, column)) {
return column; return column;
} }
} }
return null; return null;
} }
public getRightmostVisibleColumn(visibleRange: Desktop.Range, fullyVisible: boolean) { public getRightmostVisibleColumn(visibleRange: Range, fullyVisible: boolean) {
let last = null; let last = null;
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) { if (Range.contains(visibleRange, column)) {
last = column; last = column;
} else if (last !== null) { } else if (last !== null) {
break; break;
@@ -127,29 +125,14 @@ class Grid {
return last; return last;
} }
public *getVisibleColumns(visibleRange: Desktop.Range, fullyVisible: boolean) { public *getVisibleColumns(visibleRange: Range, fullyVisible: boolean) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
if (column.isVisible(visibleRange, fullyVisible)) { if (Range.contains(visibleRange, column)) {
yield column; yield column;
} }
} }
} }
public getVisibleColumnsWidth(visibleRange: Desktop.Range, fullyVisible: boolean) {
let width = 0;
let nVisible = 0;
for (const column of this.getVisibleColumns(visibleRange, fullyVisible)) {
width += column.getWidth();
nVisible++;
}
if (nVisible > 0) {
width += (nVisible-1) * this.config.gapsInnerHorizontal;
}
return width;
}
public arrange(x: number, visibleRange: Range) { public arrange(x: number, visibleRange: Range) {
for (const column of this.columns.iterator()) { for (const column of this.columns.iterator()) {
column.arrange(x, visibleRange, this.userResize); column.arrange(x, visibleRange, this.userResize);

41
src/lib/layout/Range.ts Normal file
View File

@@ -0,0 +1,41 @@
type Range = {
getLeft(): number;
getRight(): number;
getWidth(): number;
};
namespace Range {
export function create(x: number, width: number) {
return new Basic(x, width);
}
export function fromRanges(leftRange: Range, rightRange: Range) {
const left = leftRange.getLeft();
const right = rightRange.getRight();
return new Basic(left, right - left);
}
export function contains(parent: Range, child: Range) {
return child.getLeft() >= parent.getLeft() &&
child.getRight() <= parent.getRight();
}
class Basic {
constructor(
private readonly x: number,
private readonly width: number,
) {}
public getLeft() {
return this.x;
}
public getRight() {
return this.x + this.width;
}
public getWidth() {
return this.width;
}
}
}

View File

@@ -21,7 +21,7 @@ class Window {
if (targetColumn === this.column) { if (targetColumn === this.column) {
return; return;
} }
this.column.onWindowRemoved(this, false); this.column.onWindowRemoved(this, this.isFocused() && targetColumn.grid !== this.column.grid);
this.column = targetColumn; this.column = targetColumn;
targetColumn.onWindowAdded(this, bottom); targetColumn.onWindowAdded(this, bottom);
} }
@@ -66,6 +66,14 @@ class Window {
} }
public onFocused() { public onFocused() {
if (this.column.grid.config.reMaximize && (
this.focusedState.maximizedMode !== MaximizedMode.Unmaximized ||
this.focusedState.fullScreen
)) {
// We need to maximize/fullscreen this window, but we can't do it here.
// We need to do it in `arrange` to ensure it happens after placement.
this.column.grid.desktop.forceArrange();
}
this.column.onWindowFocused(this); this.column.onWindowFocused(this);
} }
@@ -75,7 +83,6 @@ class Window {
} }
this.client.setFullScreen(false); this.client.setFullScreen(false);
this.client.setMaximize(false, false); this.client.setMaximize(false, false);
this.column.grid.desktop.onLayoutChanged();
} }
public onMaximizedChanged(maximizedMode: MaximizedMode) { public onMaximizedChanged(maximizedMode: MaximizedMode) {

View File

@@ -26,38 +26,38 @@ function fillSpace(availableSpace: number, items: { min: number, max: number }[]
} }
function buildRanges(items: { min: number, max: number }[]) { function buildRanges(items: { min: number, max: number }[]) {
const landmarks = buildLandmarks(items); const fenceposts = extractFenceposts(items);
if (landmarks.length === 1) { if (fenceposts.length === 1) {
return [{ return [{
start: landmarks[0].value, start: fenceposts[0].value,
end: landmarks[0].value, end: fenceposts[0].value,
n: items.length, n: items.length,
}]; }];
} }
const ranges: Range[] = []; const ranges: Range[] = [];
let n = 0; let n = 0;
for (let i = 1; i < landmarks.length; i++) { for (let i = 1; i < fenceposts.length; i++) {
const startLandmark = landmarks[i-1]; const startFencepost = fenceposts[i-1];
const endLandmark = landmarks[i]; const endFencepost = fenceposts[i];
n = n - startLandmark.nMax + startLandmark.nMin; n = n - startFencepost.nMax + startFencepost.nMin;
ranges.push({ ranges.push({
start: startLandmark.value, start: startFencepost.value,
end: endLandmark.value, end: endFencepost.value,
n: n, n: n,
}); });
} }
return ranges; return ranges;
} }
function buildLandmarks(items: { min: number, max: number }[]) { function extractFenceposts(items: { min: number, max: number }[]) {
const landmarks = new Map<number, Landmark>(); const fenceposts = new Map<number, Fencepost>();
for (const item of items) { for (const item of items) {
mapGetOrInit(landmarks, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++; mapGetOrInit(fenceposts, item.min, { value: item.min, nMin: 0, nMax: 0 }).nMin++;
mapGetOrInit(landmarks, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++; mapGetOrInit(fenceposts, item.max, { value: item.max, nMin: 0, nMax: 0 }).nMax++;
} }
const array = Array.from(landmarks.values()); const array = Array.from(fenceposts.values());
array.sort((a, b) => a.value - b.value); array.sort((a, b) => a.value - b.value);
return array; return array;
} }
@@ -90,7 +90,7 @@ function fillSpace(availableSpace: number, items: { min: number, max: number }[]
n: number, n: number,
}; };
type Landmark = { type Fencepost = {
value: number, value: number,
nMin: number, nMin: number,
nMax: number, nMax: number,

View File

@@ -11,6 +11,7 @@ class World {
let presetWidths = { let presetWidths = {
next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth, next: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
prev: (currentWidth: number, minWidth: number, maxWidth: number) => currentWidth,
getWidths: (minWidth: number, maxWidth: number): number[] => [], getWidths: (minWidth: number, maxWidth: number): number[] => [],
}; };
try { try {

View File

@@ -18,9 +18,6 @@ namespace ClientState {
public destroy(passFocus: boolean) { public destroy(passFocus: boolean) {
this.signalManager.destroy(); this.signalManager.destroy();
if (this.config.floatingKeepAbove) {
this.client.kwinClient.keepAbove = false;
}
} }
// TODO: move to `Tiled.restoreClientAfterTiling` // TODO: move to `Tiled.restoreClientAfterTiling`

View File

@@ -19,9 +19,6 @@ namespace ClientState {
public destroy(passFocus: boolean) { public destroy(passFocus: boolean) {
this.signalManager.destroy(); this.signalManager.destroy();
if (this.config.floatingKeepAbove) {
this.kwinClient.keepAbove = true;
}
this.pinManager.removeClient(this.kwinClient); this.pinManager.removeClient(this.kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) { for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
desktop.onPinsChanged(); desktop.onPinsChanged();

View File

@@ -204,6 +204,7 @@ namespace ClientState {
if (config.tiledKeepBelow) { if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true; client.kwinClient.keepBelow = true;
} }
client.kwinClient.keepAbove = false;
client.setFullScreen(false); client.setFullScreen(false);
if (client.kwinClient.tile !== null) { if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode client.setMaximize(false, true); // disable quick tile mode

View File

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

191
src/tests/flows/layering.ts Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,12 @@ tests.register("Preset Widths default", 1, () => {
qtMock.fireShortcut("karousel-cycle-preset-widths"); qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth)); Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(maxWidth));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
}); });
tests.register("Preset Widths custom", 1, () => { tests.register("Preset Widths custom", 1, () => {
@@ -61,6 +67,15 @@ tests.register("Preset Widths custom", 1, () => {
qtMock.fireShortcut("karousel-cycle-preset-widths"); qtMock.fireShortcut("karousel-cycle-preset-widths");
Assert.equalRects(kwinClient.frameGeometry, getRect(250)); Assert.equalRects(kwinClient.frameGeometry, getRect(250));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(100));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(500));
qtMock.fireShortcut("karousel-cycle-preset-widths-reverse");
Assert.equalRects(kwinClient.frameGeometry, getRect(halfWidth));
}); });
tests.register("Preset Widths fill screen uniform", 1, () => { tests.register("Preset Widths fill screen uniform", 1, () => {

View File

@@ -126,23 +126,29 @@ namespace Assert {
export function grid( export function grid(
config: Config, config: Config,
screen: QmlRect, screen: QmlRect,
columnWidth: number,
grid: KwinClient[][], grid: KwinClient[][],
centered: boolean,
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},
) { ) {
// assumes uniformly sized windows within columns of width 100 const nColumns = grid.length;
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
const columnsWidth = nColumns * columnWidth + (nColumns-1) * config.gapsInnerHorizontal;
const startX = centered ?
screen.x + (screen.width - columnsWidth) / 2 :
grid[0][0].frameGeometry.x;
// assumes uniformly sized windows within columns of uniform width
function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) { function getRectInGrid(column: number, window: number, nColumns: number, nWindows: number) {
const columnHeight = screen.height - config.gapsOuterTop - config.gapsOuterBottom;
const columnsWidth = nColumns * 100 + (nColumns-1) * config.gapsInnerHorizontal;
const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows; const windowHeight = (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows;
return new MockQmlRect( return new MockQmlRect(
screen.x + column * (100 + config.gapsInnerHorizontal) + (screen.width-columnsWidth) / 2, startX + column * (columnWidth + config.gapsInnerHorizontal),
screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window, screen.y + config.gapsOuterTop + (windowHeight + config.gapsInnerVertical) * window,
100, columnWidth,
(columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows, (columnHeight - config.gapsInnerVertical * (nWindows-1)) / nWindows,
); );
} }
const nColumns = grid.length;
for (let iColumn = 0; iColumn < nColumns; iColumn++) { for (let iColumn = 0; iColumn < nColumns; iColumn++) {
const column = grid[iColumn]; const column = grid[iColumn];
const nWindows = column.length; const nWindows = column.length;
@@ -157,6 +163,22 @@ namespace Assert {
} }
} }
export function centered(
config: Config,
screen: QmlRect,
client:KwinClient,
{ message, skip=0 }: Options = {},
) {
grid(
config,
screen,
client.frameGeometry.width,
[[client]],
true,
{ message: appendMessage("Window not centered", message), skip: skip+1 },
);
}
export function fullyVisible( export function fullyVisible(
rect: QmlRect, rect: QmlRect,
{ message, skip=0 }: Options = {}, { message, skip=0 }: Options = {},

View File

@@ -4,7 +4,7 @@ class MockKwinClient {
private static readonly borderThickness = 10; private static readonly borderThickness = 10;
public readonly shadeable: boolean = false; public readonly shadeable: boolean = false;
public readonly caption = "App"; public caption = "App";
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0); public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public readonly transient: boolean; public readonly transient: boolean;
public readonly move: boolean = false; public readonly move: boolean = false;
@@ -21,30 +21,32 @@ class MockKwinClient {
public readonly popupWindow: boolean = false; public readonly popupWindow: boolean = false;
public readonly pid = 1; public readonly pid = 1;
private _maximizedVertically: boolean = false;
private _maximizedHorizontally: boolean = false;
private _fullScreen: boolean = false; private _fullScreen: boolean = false;
public activities: string[] = []; public activities: string[] = [];
public skipSwitcher: boolean = false; public skipSwitcher: boolean = false;
public keepAbove: boolean = false; public keepAbove: boolean = false;
public keepBelow: boolean = false; public keepBelow: boolean = false;
public shade: boolean = false; public shade: boolean = false;
public _minimized: boolean = false; private _minimized: boolean = false;
public desktops: KwinDesktop[] = []; private _desktops: KwinDesktop[] = [];
public _tile: Tile|null = null; private _tile: Tile|null = null;
public opacity: number = 1.0; public opacity: number = 1.0;
public readonly fullScreenChanged = new MockQSignal(); public readonly fullScreenChanged = new MockQSignal<[]>();
public readonly desktopsChanged = new MockQSignal(); public readonly desktopsChanged = new MockQSignal<[]>();
public readonly activitiesChanged = new MockQSignal(); public readonly activitiesChanged = new MockQSignal<[]>();
public readonly minimizedChanged = new MockQSignal(); public readonly minimizedChanged = new MockQSignal<[]>();
public readonly maximizedAboutToChange = new MockQSignal<[MaximizedMode]>(); public readonly maximizedAboutToChange = new MockQSignal<[MaximizedMode]>();
public readonly captionChanged = new MockQSignal(); public readonly captionChanged = new MockQSignal<[]>();
public readonly tileChanged = new MockQSignal(); public readonly tileChanged = new MockQSignal<[]>();
public readonly interactiveMoveResizeStarted = new MockQSignal(); public readonly interactiveMoveResizeStarted = new MockQSignal<[]>();
public readonly interactiveMoveResizeFinished = new MockQSignal(); public readonly interactiveMoveResizeFinished = new MockQSignal<[]>();
public readonly frameGeometryChanged = new MockQSignal<[oldGeometry: QmlRect]>(); public readonly frameGeometryChanged = new MockQSignal<[oldGeometry: QmlRect]>();
private windowedFrameGeometry: MockQmlRect; private windowedFrameGeometry: MockQmlRect;
private windowed: boolean = false; private windowed: boolean = true;
private hasBorder: boolean = true; private hasBorder: boolean = true;
constructor( constructor(
@@ -53,11 +55,18 @@ class MockKwinClient {
) { ) {
this.windowedFrameGeometry = _frameGeometry.clone(); this.windowedFrameGeometry = _frameGeometry.clone();
this.transient = transientFor !== null; this.transient = transientFor !== null;
this._desktops = [Workspace.currentDesktop];
} }
setMaximize(vertically: boolean, horizontally: boolean) { setMaximize(vertically: boolean, horizontally: boolean) {
this.windowed = !(vertically || horizontally); this.windowed = !(vertically || horizontally);
if (vertically === this._maximizedVertically && horizontally === this._maximizedHorizontally) {
return;
}
this._maximizedVertically = vertically;
this._maximizedHorizontally = horizontally;
this.maximizedAboutToChange.fire( this.maximizedAboutToChange.fire(
vertically ? ( vertically ? (
horizontally ? MaximizedMode.Maximized : MaximizedMode.Vertically horizontally ? MaximizedMode.Maximized : MaximizedMode.Vertically
@@ -167,6 +176,18 @@ class MockKwinClient {
this.minimizedChanged.fire(); this.minimizedChanged.fire();
} }
public get desktops() {
return this._desktops;
}
public set desktops(desktops: KwinDesktop[]) {
this._desktops = desktops;
this.desktopsChanged.fire();
if (Workspace.activeWindow === this && !desktops.includes(Workspace.currentDesktop)) {
Workspace.activeWindow = null;
};
}
public get tile() { public get tile() {
return this._tile; return this._tile;
} }
@@ -189,4 +210,8 @@ class MockKwinClient {
public getFrameGeometryCopy() { public getFrameGeometryCopy() {
return this._frameGeometry.clone(); return this._frameGeometry.clone();
} }
public toString() {
return `MockKwinClient("${this.caption}")`;
}
} }

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ class MockWorkspace {
public currentDesktop = this.desktops[0]; public currentDesktop = this.desktops[0];
public currentActivity = this.activities[0]; public currentActivity = this.activities[0];
public activeScreen: Output = { __brand: "Output" }; public activeScreen: Output = { __brand: "Output" };
public windows = []; public readonly windows: MockKwinClient[] = [];
public cursorPos = new MockQmlPoint(0, 0); public cursorPos = new MockQmlPoint(0, 0);
private _activeWindow: KwinClient|null = null; private _activeWindow: KwinClient|null = null;
@@ -30,6 +30,7 @@ class MockWorkspace {
public createWindows(...kwinClients: MockKwinClient[]) { public createWindows(...kwinClients: MockKwinClient[]) {
for (const kwinClient of kwinClients) { for (const kwinClient of kwinClients) {
this.windows.push(kwinClient);
this.windowAdded.fire(kwinClient); this.windowAdded.fire(kwinClient);
this.activeWindow = kwinClient; this.activeWindow = kwinClient;
} }
@@ -41,6 +42,7 @@ class MockWorkspace {
public createClientsWithFrames(...frames: MockQmlRect[]) { public createClientsWithFrames(...frames: MockQmlRect[]) {
const clients = frames.map(rect => new MockKwinClient(rect)); const clients = frames.map(rect => new MockKwinClient(rect));
clients.forEach((client, index) => client.caption = `Client ${index}`);
this.createWindows(...clients); this.createWindows(...clients);
return clients; return clients;
} }
@@ -49,6 +51,17 @@ class MockWorkspace {
return this.createClientsWithFrames(...widths.map(width => new MockQmlRect(randomInt(100), randomInt(100), width, 100+randomInt(400)))); return this.createClientsWithFrames(...widths.map(width => new MockQmlRect(randomInt(100), randomInt(100), width, 100+randomInt(400))));
} }
public removeWindow(window: MockKwinClient) {
runReorder(
() => this.windows.splice(this.windows.indexOf(window), 1),
() => this.windowRemoved.fire(window),
);
if (window === this.activeWindow) {
const windows = this.windows.filter(w => w.desktops.includes(this.currentDesktop));
Workspace.activeWindow = windows.length > 0 ? randomItem(windows) : null;
};
}
public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) { public resizeWindow(window: MockKwinClient, edgeResize: boolean, leftEdge: boolean, topEdge: boolean, ...deltas: QmlSize[]) {
const frame = window.getFrameGeometryCopy(); const frame = window.getFrameGeometryCopy();
if (edgeResize) { if (edgeResize) {

View File

@@ -26,6 +26,12 @@ function randomInt(n: number) {
return Math.floor(Math.random() * n); return Math.floor(Math.random() * n);
} }
function randomItem(items: any[]) {
Assert.assert(items.length > 0);
const index = randomInt(items.length);
return items[index];
}
function shuffle(items: any[]) { function shuffle(items: any[]) {
for (let n = items.length; n > 1; n--) { for (let n = items.length; n > 1; n--) {
const i = n-1; const i = n-1;