refactor arrange

This commit is contained in:
Peter Fajdiga
2023-08-19 23:46:38 +02:00
parent c87ef982ae
commit 9477b7e337
11 changed files with 451 additions and 398 deletions

View File

@@ -2,7 +2,7 @@ module Actions {
export function init(world: World, config: Config) {
return {
focusLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
@@ -12,7 +12,7 @@ module Actions {
},
focusRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
@@ -22,7 +22,7 @@ module Actions {
},
focusUp: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const prevWindow = column.getPrevWindow(window);
if (prevWindow === null) {
return;
@@ -32,7 +32,7 @@ module Actions {
},
focusDown: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const nextWindow = column.getNextWindow(window);
if (nextWindow === null) {
return;
@@ -42,27 +42,29 @@ module Actions {
},
focusStart: () => {
const grid = world.getCurrentGrid();
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
grid.container.arrange();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
firstColumn.focus();
});
},
focusEnd: () => {
const grid = world.getCurrentGrid();
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
grid.container.arrange();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
lastColumn.focus();
});
},
windowMoveLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const prevColumn = grid.getPrevColumn(column);
@@ -70,18 +72,17 @@ module Actions {
return;
}
window.moveToColumn(prevColumn);
grid.container.onGridReordered();
grid.container.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, grid.getPrevColumn(column));
window.moveToColumn(newColumn);
}
grid.container.arrange();
});
},
windowMoveRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
if (column.getWindowCount() === 1) {
// move from own column into existing column
const nextColumn = grid.getNextColumn(column);
@@ -89,97 +90,89 @@ module Actions {
return;
}
window.moveToColumn(nextColumn);
grid.container.onGridReordered();
grid.container.autoAdjustScroll();
} else {
// move from shared column into own column
const newColumn = new Column(grid, column);
window.moveToColumn(newColumn);
}
grid.container.arrange();
});
},
windowMoveUp: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
column.moveWindowUp(window);
grid.container.arrange(); // TODO (optimization): only arrange moved windows
});
},
windowMoveDown: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
// TODO (optimization): only arrange moved windows
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
column.moveWindowDown(window);
grid.container.arrange(); // TODO (optimization): only arrange moved windows
});
},
windowMoveStart: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const newColumn = new Column(grid, null);
window.moveToColumn(newColumn);
grid.container.arrange();
});
},
windowMoveEnd: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const newColumn = new Column(grid, grid.getLastColumn());
window.moveToColumn(newColumn);
grid.container.arrange();
});
},
windowToggleFloating: () => {
const kwinClient = workspace.activeClient;
world.toggleFloatingClient(kwinClient);
world.do((clientManager, svm) => {
clientManager.toggleFloatingClient(kwinClient);
});
},
columnMoveLeft: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
grid.moveColumnLeft(column);
grid.container.arrange();
});
},
columnMoveRight: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
grid.moveColumnRight(column);
grid.container.arrange();
});
},
columnMoveStart: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
column.moveAfter(null);
grid.container.arrange();
});
},
columnMoveEnd: () => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
column.moveAfter(grid.getLastColumn());
grid.container.arrange();
});
},
columnToggleStacked: () => {
world.doIfTiledFocused(false, (window, column, grid) => {
world.doIfTiledFocused(false, (world, svm, window, column, grid) => {
column.toggleStacked();
grid.container.arrange();
});
},
columnWidthIncrease: () => {
world.doIfTiledFocused(false, (window, column, grid) => {
world.doIfTiledFocused(false, (world, svm, window, column, grid) => {
grid.increaseColumnWidth(column);
grid.container.arrange();
});
},
columnWidthDecrease: () => {
world.doIfTiledFocused(false, (window, column, grid) => {
world.doIfTiledFocused(false, (world, svm, window, column, grid) => {
grid.decreaseColumnWidth(column);
grid.container.arrange();
});
},
@@ -192,66 +185,65 @@ module Actions {
},
gridScrollStart: () => {
const grid = world.getCurrentGrid();
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.container.scrollToColumn(firstColumn);
grid.container.arrange();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const firstColumn = grid.getFirstColumn();
if (firstColumn === null) {
return;
}
grid.container.scrollToColumn(firstColumn);
});
},
gridScrollEnd: () => {
const grid = world.getCurrentGrid();
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.container.scrollToColumn(lastColumn);
grid.container.arrange();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const lastColumn = grid.getLastColumn();
if (lastColumn === null) {
return;
}
grid.container.scrollToColumn(lastColumn);
});
},
gridScrollFocused: () => {
const focusedWindow = world.getFocusedWindow(true);
if (focusedWindow === null) {
return;
}
const column = focusedWindow.column;
const grid = column.grid;
grid.container.scrollCenterColumn(column);
grid.container.arrange();
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
grid.container.scrollCenterColumn(column);
})
},
gridScrollLeftColumn: () => {
const grid = world.getCurrentGrid();
const column = grid.getLeftmostVisibleColumn(grid.container.getCurrentScrollPos(), true);
if (column === null) {
return;
}
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const column = grid.getLeftmostVisibleColumn(grid.container.getCurrentScrollPos(), true);
if (column === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
const prevColumn = grid.getPrevColumn(column);
if (prevColumn === null) {
return;
}
grid.container.scrollToColumn(prevColumn);
grid.container.arrange();
grid.container.scrollToColumn(prevColumn);
});
},
gridScrollRightColumn: () => {
const grid = world.getCurrentGrid();
const column = grid.getRightmostVisibleColumn(grid.container.getCurrentScrollPos(), true);
if (column === null) {
return;
}
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const column = grid.getRightmostVisibleColumn(grid.container.getCurrentScrollPos(), true);
if (column === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
const nextColumn = grid.getNextColumn(column);
if (nextColumn === null) {
return;
}
grid.container.scrollToColumn(nextColumn);
grid.container.arrange();
grid.container.scrollToColumn(nextColumn);
});
},
};
}
@@ -259,28 +251,29 @@ module Actions {
export function initNum(world: World) {
return {
focusColumn: (columnIndex: number) => {
const grid = world.getCurrentGrid();
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
targetColumn.focus();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
targetColumn.focus();
});
},
windowMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null) {
return null;
}
window.moveToColumn(targetColumn);
grid.container.onGridReordered();
grid.container.arrange();
grid.container.autoAdjustScroll();
});
},
columnMoveToColumn: (columnIndex: number) => {
world.doIfTiledFocused(true, (window, column, grid) => {
world.doIfTiledFocused(true, (world, svm, window, column, grid) => {
const targetColumn = grid.getColumnAtIndex(columnIndex);
if (targetColumn === null || targetColumn === column) {
return null;
@@ -290,43 +283,38 @@ module Actions {
} else {
column.moveAfter(grid.getPrevColumn(targetColumn));
}
grid.container.arrange();
});
},
columnMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (window, column, oldGrid) => {
world.doIfTiledFocused(true, (clientManager, svm, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = world.getGridInCurrentActivity(desktopNumber);
const newGrid = svm.getInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
column.moveToGrid(newGrid, newGrid.getLastColumn());
oldGrid.container.arrange();
newGrid.container.arrange();
});
},
tailMoveToDesktop: (desktopIndex: number) => {
world.doIfTiledFocused(true, (window, column, oldGrid) => {
world.doIfTiledFocused(true, (clientManager, svm, window, column, oldGrid) => {
const desktopNumber = desktopIndex + 1;
const newGrid = world.getGridInCurrentActivity(desktopNumber);
const newGrid = svm.getInCurrentActivity(desktopNumber).grid;
if (newGrid === null || newGrid === oldGrid) {
return;
}
oldGrid.evacuateTail(newGrid, column);
oldGrid.container.arrange();
newGrid.container.arrange();
});
},
};
}
function gridScroll(world: World, amount: number) {
const scrollAmount = amount;
const grid = world.getCurrentGrid();
grid.container.adjustScroll(scrollAmount, false);
grid.container.arrange();
world.do((clientManager, svm) => {
const grid = svm.getCurrent().grid;
grid.container.adjustScroll(amount, false);
});
}
export type Config = {

View File

@@ -47,10 +47,12 @@ class Column {
public moveWindowUp(window: Window) {
this.windows.moveBack(window);
this.grid.container.onLayoutChanged();
}
public moveWindowDown(window: Window) {
this.windows.moveForward(window);
this.grid.container.onLayoutChanged();
}
public getWindowCount() {
@@ -124,6 +126,8 @@ class Column {
window.height += heightDelta;
otherWindow.height -= heightDelta;
this.grid.container.onLayoutChanged();
}
public resizeWindows() {
@@ -144,9 +148,11 @@ class Column {
remainingWindows--;
}
// TODO: respect min height
this.grid.container.onLayoutChanged();
}
private getFocusTaker() {
public getFocusTaker() {
if (this.focusTaker === null || !this.windows.contains(this.focusTaker)) {
return null;
}
@@ -206,6 +212,7 @@ class Column {
return;
}
this.stacked = !this.stacked;
this.grid.container.onLayoutChanged();
}
public isVisible(scrollPos: ScrollView.Pos, fullyVisible: boolean) {
@@ -230,6 +237,8 @@ class Column {
if (window.isFocused()) {
this.onWindowFocused(window);
}
this.grid.container.onLayoutChanged();
}
public onWindowRemoved(window: Window, passFocus: boolean) {
@@ -251,6 +260,8 @@ class Column {
windowToFocus.focus();
}
}
this.grid.container.onLayoutChanged();
}
public onWindowFocused(window: Window) {

View File

@@ -16,7 +16,8 @@ class Grid {
this.userResize = false;
this.userResizeFinishedDelayer = new Delayer(50, () => {
// this delay prevents windows' contents from freezing after resizing
this.container.onGridWidthChanged();
this.container.onLayoutChanged();
this.container.autoAdjustScroll();
this.container.arrange();
});
}
@@ -24,7 +25,8 @@ class Grid {
public moveColumnLeft(column: Column) {
this.columns.moveBack(column);
this.columnsSetX(column);
this.container.onGridReordered();
this.container.onLayoutChanged();
this.container.autoAdjustScroll();
}
public moveColumnRight(column: Column) {
@@ -66,6 +68,14 @@ class Grid {
return this.lastFocusedColumn;
}
public getLastFocusedWindow() {
const lastFocusedColumn = this.getLastFocusedColumn();
if (lastFocusedColumn === null) {
return null;
}
return lastFocusedColumn.getFocusTaker();
}
private columnsSetX(firstMovedColumn: Column|null) {
const lastUnmovedColumn = firstMovedColumn === null ? this.columns.getLast() : this.columns.getPrev(firstMovedColumn);
let x = lastUnmovedColumn === null ? 0 : lastUnmovedColumn.getRight() + this.config.gapsInnerHorizontal;
@@ -200,6 +210,11 @@ class Grid {
column.arrange(x);
x += column.getWidth() + this.config.gapsInnerHorizontal;
}
const focusedWindow = this.getLastFocusedWindow();
if (focusedWindow !== null) {
focusedWindow.client.ensureTransientsVisible(this.container.clientArea);
}
}
public onColumnAdded(column: Column, prevColumn: Column|null) {
@@ -209,7 +224,8 @@ class Grid {
this.columns.insertAfter(column, prevColumn);
}
this.columnsSetX(column);
this.container.onGridWidthChanged();
this.container.onLayoutChanged();
this.container.autoAdjustScroll();
}
public onColumnRemoved(column: Column, passFocus: boolean) {
@@ -226,8 +242,9 @@ class Grid {
if (passFocus && columnToFocus !== null) {
columnToFocus.focus();
} else {
this.container.onGridWidthChanged();
this.container.autoAdjustScroll();
}
this.container.onLayoutChanged();
}
public onColumnMoved(column: Column, prevColumn: Column|null) {
@@ -235,15 +252,17 @@ class Grid {
const firstMovedColumn = movedLeft ? column : this.getNextColumn(column);
this.columns.move(column, prevColumn);
this.columnsSetX(firstMovedColumn);
this.container.onGridReordered();
this.container.onLayoutChanged();
this.container.autoAdjustScroll();
}
public onColumnWidthChanged(column: Column, oldWidth: number, width: number) {
const nextColumn = this.columns.getNext(column);
this.columnsSetX(nextColumn);
if (!this.userResize) {
this.container.onGridWidthChanged();
this.container.autoAdjustScroll();
}
this.container.onLayoutChanged();
}
public onColumnFocused(column: Column) {

View File

@@ -1,16 +1,16 @@
class ScrollView {
public readonly world: World;
public readonly grid: Grid;
public readonly desktop: number;
private readonly config: ScrollView.Config;
private scrollX: number;
private dirty: boolean;
public clientArea: QRect;
public tilingArea: QRect;
constructor(world: World, desktop: number, config: ScrollView.Config, layoutConfig: LayoutConfig) {
constructor(desktop: number, config: ScrollView.Config, layoutConfig: LayoutConfig) {
this.config = config;
this.world = world;
this.scrollX = 0;
this.dirty = false;
this.desktop = desktop;
this.grid = new Grid(this, layoutConfig);
this.updateArea();
@@ -65,7 +65,7 @@ class ScrollView {
}
public scrollToColumn(column: Column) {
this.scrollX = this.getScrollPosForColumn(column).x;
this.setScroll(this.getScrollPosForColumn(column).x, true);
}
public scrollCenterColumn(column: Column) {
@@ -74,18 +74,18 @@ class ScrollView {
this.adjustScroll(Math.round(windowCenter - screenCenter), false);
}
private autoAdjustScroll() {
const focusedWindow = this.world.getFocusedWindow(true);
if (focusedWindow === null) {
public autoAdjustScroll() {
const focusedColumn = this.grid.getLastFocusedColumn();
if (focusedColumn === null) {
this.removeOverscroll();
return;
}
const column = focusedWindow.column;
if (column.grid !== this.grid) {
if (focusedColumn.grid !== this.grid) {
return;
}
this.scrollToColumn(column);
this.scrollToColumn(focusedColumn);
}
private getScrollPos(scrollX: number) {
@@ -108,11 +108,15 @@ class ScrollView {
}
private setScroll(x: number, force: boolean) {
const oldScrollX = this.scrollX;
this.scrollX = force ? x : this.clampScrollX(x);
if (this.scrollX !== oldScrollX) {
this.onLayoutChanged();
}
}
private applyScrollPos(scrollPos: ScrollView.Pos) {
this.scrollX = scrollPos.x;
this.setScroll(scrollPos.x, true);
}
public adjustScroll(dx: number, force: boolean) {
@@ -126,16 +130,15 @@ class ScrollView {
public arrange() {
// TODO (optimization): only arrange visible windows
this.updateArea();
if (!this.dirty) {
return;
}
this.grid.arrange(this.tilingArea.x - this.scrollX);
this.world.ensureFocusedTransientsVisible(); // TODO: refactor - call from elsewhere
this.dirty = false;
}
public onGridWidthChanged() {
this.autoAdjustScroll();
}
public onGridReordered() {
this.autoAdjustScroll();
public onLayoutChanged() {
this.dirty = true;
}
public destroy() {

View File

@@ -63,6 +63,7 @@ class Window {
}
this.client.setMaximize(false, false);
this.client.setFullScreen(false);
this.column.grid.container.onLayoutChanged();
}
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {

View File

@@ -3,7 +3,7 @@ class WindowRuleEnforcer {
private readonly preferTiling: ClientMatcher;
private readonly followCaption: Set<string>;
constructor(world: World, windowRules: WindowRule[]) {
constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = createWindowRuleMaps(windowRules);
this.preferFloating = new ClientMatcher(mapFloat);
this.preferTiling = new ClientMatcher(mapTile);
@@ -26,11 +26,13 @@ class WindowRuleEnforcer {
const manager = new SignalManager();
manager.connect(kwinClient.captionChanged, () => {
const shouldTile = enforcer.shouldTile(kwinClient);
if (shouldTile) {
world.tileClient(kwinClient);
} else {
world.untileClient(kwinClient);
}
world.do((clientManager, svm) => {
if (shouldTile) {
clientManager.tileClient(kwinClient);
} else {
clientManager.untileClient(kwinClient);
}
});
});
return manager;
}

View File

@@ -2,7 +2,6 @@ function initWorkspaceSignalHandlers(world: World) {
const manager = new SignalManager();
manager.connect(workspace.clientAdded, (kwinClient: AbstractClient) => {
console.assert(!world.hasClient(kwinClient));
if (Clients.canTileEver(kwinClient)) {
// never open new tileable clients on all desktops or activities
if (kwinClient.desktop <= 0) {
@@ -12,26 +11,32 @@ function initWorkspaceSignalHandlers(world: World) {
kwinClient.activities = [workspace.currentActivity];
}
}
world.addClient(kwinClient);
world.do((clientManager, svm) => {
clientManager.addClient(kwinClient)
});
});
manager.connect(workspace.clientRemoved, (kwinClient: AbstractClient) => {
console.assert(world.hasClient(kwinClient));
world.removeClient(kwinClient, true);
world.do((clientManager, svm) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(workspace.clientMinimized, (kwinClient: AbstractClient) => {
world.minimizeClient(kwinClient);
world.do((clientManager, svm) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: AbstractClient) => {
world.unminimizeClient(kwinClient);
world.do((clientManager, svm) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: AbstractClient, horizontally: boolean, vertically: boolean) => {
world.doIfTiled(kwinClient, false, (window, column, grid) => {
world.doIfTiled(kwinClient, false, (world, svm, window, column, grid) => {
window.onMaximizedChanged(horizontally, vertically);
grid.container.arrange();
});
});
@@ -39,17 +44,14 @@ function initWorkspaceSignalHandlers(world: World) {
if (kwinClient === null) {
return;
}
world.onClientFocused(kwinClient);
world.doIfTiled(kwinClient, true, (window, column, grid) => {
window.onFocused();
grid.container.arrange();
world.do((clientManager, svm) => {
clientManager.onClientFocused(kwinClient);
});
});
manager.connect(workspace.clientFullScreenSet, (kwinClient: X11Client, fullScreen: boolean, user: boolean) => {
world.doIfTiled(kwinClient, false, (window, column, grid) => {
world.doIfTiled(kwinClient, false, (clientManager, svm, window, column, grid) => {
window.onFullScreenChanged(fullScreen);
grid.container.arrange();
});
});

164
src/world/ClientManager.ts Normal file
View File

@@ -0,0 +1,164 @@
class ClientManager {
private readonly world: World;
private readonly scrollViewManager: ScrollViewManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>;
private lastFocusedClient: AbstractClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(config: Config, world: World, svm: ScrollViewManager) {
this.world = world;
this.scrollViewManager = svm;
this.clientMap = new Map();
this.lastFocusedClient = null;
let parsedWindowRules: WindowRule[] = [];
try {
parsedWindowRules = JSON.parse(config.windowRules);
} catch (error: any) {
console.log("failed to parse windowRules:", error);
}
this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules);
}
public addClient(kwinClient: AbstractClient) {
console.assert(!this.hasClient(kwinClient));
const client = new ClientWrapper(
kwinClient,
new ClientStateFloating(),
this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient),
);
this.clientMap.set(kwinClient, client);
if (kwinClient.dock) {
client.stateManager.setState(new ClientStateDocked(this.world, kwinClient), false);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
const grid = this.scrollViewManager.getForClient(client.kwinClient).grid;
client.stateManager.setState(new ClientStateTiled(this.world, client, grid), false);
}
}
public removeClient(kwinClient: AbstractClient, passFocus: boolean) {
console.assert(this.hasClient(kwinClient));
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
this.clientMap.delete(kwinClient);
}
private findTransientFor(kwinClient: AbstractClient) {
if (!kwinClient.transient) {
return null;
}
const transientFor = this.clientMap.get(kwinClient.transientFor);
if (transientFor === undefined) {
return null;
}
return transientFor;
}
public minimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateTiledMinimized(), kwinClient === this.lastFocusedClient);
}
}
public unminimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiledMinimized) {
const grid = this.scrollViewManager.getForClient(client.kwinClient).grid;
client.stateManager.setState(new ClientStateTiled(this.world, client, grid), false);
}
}
public tileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
return;
}
const grid = this.scrollViewManager.getForClient(client.kwinClient).grid;
client.stateManager.setState(new ClientStateTiled(this.world, client, grid), false);
}
public untileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
public toggleFloatingClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateFloating && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient);
const grid = this.scrollViewManager.getForClient(client.kwinClient).grid;
client.stateManager.setState(new ClientStateTiled(this.world, client, grid), false);
} else if (clientState instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
public hasClient(kwinClient: AbstractClient) {
return this.clientMap.has(kwinClient);
}
public onClientFocused(kwinClient: AbstractClient) {
this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient, true);
if (window !== null) {
window.onFocused();
}
}
public findTiledWindow(kwinClient: AbstractClient, followTransient: boolean) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return null;
}
return this.findTiledWindowOfClient(client, followTransient);
}
private findTiledWindowOfClient(client: ClientWrapper, followTransient: boolean): Window|null {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateTiled) {
return clientState.window;
} else if (followTransient && client.transientFor !== null) {
return this.findTiledWindowOfClient(client.transientFor, true);
} else {
return null;
}
}
private removeAllClients() {
for (const kwinClient of Array.from(this.clientMap.keys())) {
this.removeClient(kwinClient, false);
}
}
public destroy() {
this.removeAllClients();
}
}

View File

@@ -2,13 +2,11 @@ class ClientStateTiled {
readonly window: Window;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper) {
constructor(world: World, client: ClientWrapper, grid: Grid) {
client.prepareForTiling();
const grid = world.getClientGrid(client.kwinClient);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column);
grid.container.arrange();
this.window = window;
this.signalManager = ClientStateTiled.initSignalManager(world, window);
@@ -21,7 +19,6 @@ class ClientStateTiled {
const grid = window.column.grid;
const clientWrapper = window.client;
window.destroy(passFocus);
grid.container.arrange();
clientWrapper.prepareForFloating(grid.container.clientArea);
}
@@ -32,39 +29,45 @@ class ClientStateTiled {
const manager = new SignalManager();
manager.connect(kwinClient.desktopChanged, () => {
if (kwinClient.desktop === -1) {
// windows on all desktops are not supported
world.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(world, window);
world.do((clientManager, svm) => {
if (kwinClient.desktop === -1) {
// windows on all desktops are not supported
clientManager.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(svm, window);
});
});
manager.connect(kwinClient.activitiesChanged, (kwinClient: AbstractClient) => {
if (kwinClient.activities.length !== 1) {
// windows on multiple activities are not supported
world.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(world, window);
world.do((clientManager, svm) => {
if (kwinClient.activities.length !== 1) {
// windows on multiple activities are not supported
clientManager.untileClient(kwinClient);
return;
}
ClientStateTiled.moveWindowToCorrectGrid(svm, window);
});
})
let lastResize = false;
manager.connect(kwinClient.moveResizedChanged, () => {
if (world.untileOnDrag && kwinClient.move) {
world.untileClient(kwinClient);
return;
}
world.do((clientManager, svm) => {
if (world.untileOnDrag && kwinClient.move) {
clientManager.untileClient(kwinClient);
return;
}
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
const grid = window.column.grid;
const resize = kwinClient.resize;
if (!lastResize && resize) {
grid.onUserResizeStarted();
}
if (lastResize && !resize) {
grid.onUserResizeFinished();
}
lastResize = resize;
});
});
let cursorChangedAfterResizeStart = false;
@@ -76,27 +79,27 @@ class ClientStateTiled {
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
const scrollView = window.column.grid.container;
if (kwinClient.resize) {
window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart);
scrollView.arrange();
} else {
const maximized = rectEqual(kwinClient.frameGeometry, scrollView.clientArea);
if (!client.isManipulatingGeometry() && !kwinClient.fullScreen && !maximized) {
window.onProgrammaticResize(oldGeometry);
scrollView.arrange();
world.do((clientManager, svm) => {
const scrollView = window.column.grid.container;
if (kwinClient.resize) {
window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart);
} else {
const maximized = rectEqual(kwinClient.frameGeometry, scrollView.clientArea);
if (!client.isManipulatingGeometry() && !kwinClient.fullScreen && !maximized) {
window.onProgrammaticResize(oldGeometry);
}
}
}
});
});
return manager;
}
private static moveWindowToCorrectGrid(world: World, window: Window) {
private static moveWindowToCorrectGrid(svm: ScrollViewManager, window: Window) {
const kwinClient = window.client.kwinClient;
const oldGrid = window.column.grid;
const newGrid = world.getClientGrid(kwinClient);
const newGrid = svm.getForClient(kwinClient).grid;
if (oldGrid === newGrid) {
// window already on the correct grid
return;
@@ -104,7 +107,5 @@ class ClientStateTiled {
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
window.moveToColumn(newColumn);
oldGrid.container.arrange();
newGrid.container.arrange();
}
}

View File

@@ -1,21 +1,23 @@
class ScrollViewManager {
private readonly world: World;
private readonly config: ScrollView.Config;
public readonly layoutConfig: LayoutConfig;
private readonly scrollViewsPerActivity: Map<string, ScrollView[]>;
private nDesktops: number;
constructor(world: World, config: ScrollView.Config, layoutConfig: LayoutConfig, currentActivity: string, nDesktops: number) {
constructor(config: ScrollView.Config, layoutConfig: LayoutConfig, currentActivity: string) {
this.config = config;
this.layoutConfig = layoutConfig;
this.world = world;
this.scrollViewsPerActivity = new Map();
this.nDesktops = 0;
this.setNDesktops(nDesktops);
this.update()
this.addActivity(currentActivity);
}
get(activity: string, desktopNumber: number) {
public update() {
this.setNDesktops(workspace.desktops);
}
public get(activity: string, desktopNumber: number) {
const desktopIndex = desktopNumber - 1;
if (desktopIndex >= this.nDesktops || this.nDesktops < 0) {
throw new Error("invalid desktop number: " + String(desktopNumber));
@@ -26,7 +28,20 @@ class ScrollViewManager {
return this.scrollViewsPerActivity.get(activity)![desktopIndex];
}
setNDesktops(nDesktops: number) {
public getCurrent() {
return this.get(workspace.currentActivity, workspace.currentDesktop);
}
public getInCurrentActivity(desktopNumber: number) {
return this.get(workspace.currentActivity, desktopNumber);
}
public getForClient(kwinClient: AbstractClient) {
console.assert(kwinClient.activities.length === 1);
return this.get(kwinClient.activities[0], kwinClient.desktop);
}
private setNDesktops(nDesktops: number) {
if (nDesktops > this.nDesktops) {
this.addDesktopsToActivities(nDesktops - this.nDesktops);
} else if (nDesktops < this.nDesktops) {
@@ -45,7 +60,7 @@ class ScrollViewManager {
const nStart = scrollViews.length;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
scrollViews.push(new ScrollView(this.world, desktopNumber, this.config, this.layoutConfig));
scrollViews.push(new ScrollView(desktopNumber, this.config, this.layoutConfig));
}
}
@@ -60,13 +75,13 @@ class ScrollViewManager {
}
}
addActivity(activity: string) {
private addActivity(activity: string) {
const scrollViews: ScrollView[] = [];
this.addDesktops(scrollViews, this.nDesktops);
this.scrollViewsPerActivity.set(activity, scrollViews);
}
removeActivity(activity: string) {
private removeActivity(activity: string) {
const removedScrollViews = this.scrollViewsPerActivity.get(activity)!;
this.scrollViewsPerActivity.delete(activity);
const targetActivityScrollViews = this.scrollViewsPerActivity.values().next().value;
@@ -75,7 +90,13 @@ class ScrollViewManager {
}
}
*scrollViews() {
public destroy() {
for (const scrollView of this.scrollViews()) {
scrollView.destroy();
}
}
public *scrollViews() {
for (const scrollViews of this.scrollViewsPerActivity.values()) {
for (const scrollView of scrollViews) {
yield scrollView;

View File

@@ -1,36 +1,24 @@
class World {
public readonly untileOnDrag: boolean;
private readonly scrollViewManager: ScrollViewManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>;
private lastFocusedClient: AbstractClient|null;
public readonly clientManager: ClientManager;
private readonly workspaceSignalManager: SignalManager;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
private readonly screenResizedDelayer: Delayer;
constructor(config: Config) {
this.untileOnDrag = config.untileOnDrag;
this.clientMap = new Map();
this.lastFocusedClient = null;
this.workspaceSignalManager = initWorkspaceSignalHandlers(this);
let parsedWindowRules: WindowRule[] = [];
try {
parsedWindowRules = JSON.parse(config.windowRules);
} catch (error: any) {
console.log("failed to parse windowRules:", error);
}
this.windowRuleEnforcer = new WindowRuleEnforcer(this, parsedWindowRules);
this.screenResizedDelayer = new Delayer(1000, () => {
// this delay ensures that docks get taken into account by `workspace.clientArea`
const gridManager = this.scrollViewManager; // workaround for bug in Qt5's JS engine
for (const scrollView of gridManager.scrollViews()) {
scrollView.arrange();
scrollView.onLayoutChanged();
}
this.update();
});
this.scrollViewManager = new ScrollViewManager(
this,
{
marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom,
@@ -40,206 +28,59 @@ class World {
},
config,
workspace.currentActivity,
workspace.desktops,
);
this.clientManager = new ClientManager(config, this, this.scrollViewManager);
this.addExistingClients();
}
public updateDesktops() {
this.scrollViewManager.setNDesktops(workspace.desktops);
this.update();
}
private addExistingClients() {
const kwinClients = workspace.clientList();
for (let i = 0; i < kwinClients.length; i++) {
const kwinClient = kwinClients[i];
this.addClient(kwinClient);
this.clientManager.addClient(kwinClient);
}
}
private getGrid(activity: string, desktopNumber: number) {
console.assert(desktopNumber > 0 && desktopNumber <= workspace.desktops);
return this.scrollViewManager.get(activity, desktopNumber).grid;
public updateDesktops() {
this.scrollViewManager.update();
}
public getGridInCurrentActivity(desktopNumber: number) {
return this.getGrid(workspace.currentActivity, desktopNumber);
public update() {
this.scrollViewManager.getCurrent().arrange();
}
public getCurrentGrid() {
return this.getGrid(workspace.currentActivity, workspace.currentDesktop);
public do(f: (clientManager: ClientManager, svm: ScrollViewManager) => void) {
f(this.clientManager, this.scrollViewManager);
this.update();
}
public getClientGrid(kwinClient: AbstractClient) {
console.assert(kwinClient.activities.length === 1);
return this.getGrid(kwinClient.activities[0], kwinClient.desktop);
}
public addClient(kwinClient: AbstractClient) {
const client = new ClientWrapper(
kwinClient,
new ClientStateFloating(),
this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this, kwinClient),
);
this.clientMap.set(kwinClient, client);
if (kwinClient.dock) {
client.stateManager.setState(new ClientStateDocked(this, kwinClient), false);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
}
public removeClient(kwinClient: AbstractClient, passFocus: boolean) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
client.destroy(passFocus && kwinClient === this.lastFocusedClient);
this.clientMap.delete(kwinClient);
}
private findTransientFor(kwinClient: AbstractClient) {
if (!kwinClient.transient) {
return null;
}
const transientFor = this.clientMap.get(kwinClient.transientFor);
if (transientFor === undefined) {
return null;
}
return transientFor;
}
public ensureFocusedTransientsVisible() {
this.doIfTiledFocused(true, (window, column, grid) => {
window.client.ensureTransientsVisible(grid.container.clientArea);
});
}
public minimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateTiledMinimized(), kwinClient === this.lastFocusedClient);
}
}
public unminimizeClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiledMinimized) {
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
}
public tileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
return;
}
client.stateManager.setState(new ClientStateTiled(this, client), false);
}
public untileClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
public toggleFloatingClient(kwinClient: AbstractClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateFloating && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient);
client.stateManager.setState(new ClientStateTiled(this, client), false);
} else if (clientState instanceof ClientStateTiled) {
client.stateManager.setState(new ClientStateFloating(), false);
}
}
public hasClient(kwinClient: AbstractClient) {
return this.clientMap.has(kwinClient);
}
public onClientFocused(kwinClient: AbstractClient) {
this.lastFocusedClient = kwinClient;
}
private findTiledWindow(client: ClientWrapper, followTransient: boolean): Window|null {
const clientState = client.stateManager.getState();
if (clientState instanceof ClientStateTiled) {
return clientState.window;
} else if (followTransient && client.transientFor !== null) {
return this.findTiledWindow(client.transientFor, true);
} else {
return null;
}
}
public doIfTiled(kwinClient: AbstractClient, followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const window = this.findTiledWindow(client, followTransient);
public doIfTiled(
kwinClient: AbstractClient,
followTransient: boolean,
f: (clientManager: ClientManager, svm: ScrollViewManager, window: Window, column: Column, grid: Grid) => void,
) {
const window = this.clientManager.findTiledWindow(kwinClient, followTransient);
if (window === null) {
return;
}
const column = window.column;
const grid = column.grid;
f(window, column, grid);
f(this.clientManager, this.scrollViewManager, window, column, grid);
this.update();
}
public doIfTiledFocused(followTransient: boolean, f: (window: Window, column: Column, grid: Grid) => void) {
public doIfTiledFocused(
followTransient: boolean,
f: (clientManager: ClientManager, svm: ScrollViewManager, window: Window, column: Column, grid: Grid) => void,
) {
this.doIfTiled(workspace.activeClient, followTransient, f);
}
public getFocusedWindow(followTransient: boolean) {
const activeClient = workspace.activeClient;
if (activeClient === null) {
return null;
}
const client = this.clientMap.get(activeClient);
if (client === undefined) {
return null;
}
return this.findTiledWindow(client, followTransient);
}
private removeAllClients() {
for (const kwinClient of Array.from(this.clientMap.keys())) {
this.removeClient(kwinClient, false);
}
}
public destroy() {
this.workspaceSignalManager.destroy();
this.removeAllClients();
for (const scrollView of this.scrollViewManager.scrollViews()) {
scrollView.destroy();
}
this.clientManager.destroy();
this.scrollViewManager.destroy();
}
public onScreenResized() {