Actions: add actions columnShrinkLeft and columnShrinkRight

This commit is contained in:
Peter Fajdiga
2024-10-05 20:56:04 +02:00
parent 703ed2eb40
commit acf4c5c6ae
7 changed files with 215 additions and 1 deletions

View File

@@ -184,6 +184,81 @@ class Actions {
this.config.columnResizer.decreaseWidth(column, this.config.manualResizeStep);
}
public readonly columnShrinkLeft = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!focusedColumn.isVisible(visibleRange, true)) {
return;
}
const visibleColumns: Column[] = [];
for (const visibleColumn of grid.getVisibleColumns(visibleRange, true)) {
visibleColumns.push(visibleColumn);
if (visibleColumn === focusedColumn) {
break;
}
}
console.assert(visibleColumns.length > 0, "should at least contain the focused column");
const targetColumn = grid.getLeftColumn(visibleColumns[0]);
if (targetColumn === null) {
return;
}
this.squeezeColumns([targetColumn, ...visibleColumns]);
}
public readonly columnShrinkRight = (cm: ClientManager, dm: DesktopManager, window: Window, focusedColumn: Column, grid: Grid) => {
const visibleRange = grid.desktop.getCurrentVisibleRange();
if (!focusedColumn.isVisible(visibleRange, true)) {
return;
}
const visibleColumns: Column[] = [focusedColumn];
for (const visibleColumn of grid.getVisibleColumns(visibleRange, true)) {
if (visibleColumn.isToTheRightOf(focusedColumn)) {
visibleColumns.push(visibleColumn);
}
}
const targetColumn = grid.getRightColumn(visibleColumns[visibleColumns.length-1]);
if (targetColumn === null) {
return;
}
this.squeezeColumns([...visibleColumns, targetColumn]);
}
private readonly squeezeColumns = (columns: Column[]) => {
const firstColumn = columns[0];
const lastColumn = columns[columns.length-1];
const desktop = firstColumn.grid.desktop;
const neededSpace = lastColumn.getRight() - firstColumn.getLeft();
const availableSpace = desktop.tilingArea.width;
let missingSpace = neededSpace - availableSpace;
if (missingSpace <= 0) {
// just scroll
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(firstColumn, lastColumn));
return;
}
const gainableSpacePerColumn = columns.map(column => column.getWidth() - column.getMinWidth());
const gainableSpaceTotal = sum(...gainableSpacePerColumn);
if (gainableSpaceTotal < missingSpace) {
// there's nothing we can do
return;
}
const shrinkRatio = missingSpace / gainableSpaceTotal;
for (let i = 0; i < columns.length-1; i++) {
const shrinkAmount = Math.round(gainableSpacePerColumn[i] * shrinkRatio);
columns[i].adjustWidth(-shrinkAmount, true);
missingSpace -= shrinkAmount;
}
lastColumn.adjustWidth(-missingSpace, true);
desktop.scrollCenterRange(Desktop.RangeImpl.fromRanges(firstColumn, lastColumn));
}
public readonly cyclePresetWidths = (cm: ClientManager, dm: DesktopManager, window: Window, column: Column, grid: Grid) => {
if (this.config.presetWidths === null) {
return;

View File

@@ -146,6 +146,19 @@ function getKeyBindings(world: World, actions: Actions): KeyBinding[] {
defaultKeySequence: "Meta+Ctrl+-",
action: () => world.doIfTiledFocused(actions.columnWidthDecrease),
},
{
name: "column-shrink-left",
description: "Decrease column width to make room for the left column",
comment: "Clashes with default KDE shortcuts, may require manual remapping",
defaultKeySequence: "Meta+Ctrl+A",
action: () => world.doIfTiledFocused(actions.columnShrinkLeft),
},
{
name: "column-shrink-right",
description: "Decrease column width to make room for the right column",
defaultKeySequence: "Meta+Ctrl+D",
action: () => world.doIfTiledFocused(actions.columnShrinkRight),
},
{
name: "cycle-preset-widths",
description: "Cycle through preset column widths",

View File

@@ -8,6 +8,10 @@ function clamp(value: number, min: number, max: number) {
return value;
}
function sum(...list: number[]) {
return list.reduce((acc, val) => acc + val);
}
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];

View File

@@ -0,0 +1,77 @@
tests.register("column shrink left", 1, () => {
const baseTestCases = [
{ widths: [500, 500], blocked: [false, false], possible: true },
{ widths: [500, 768], blocked: [false, false], possible: true },
{ widths: [500, 500], blocked: [false, true], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, false], possible: true },
{ widths: [500, 200, 200], blocked: [false, false, true], possible: true },
{ widths: [500, 200, 200], blocked: [true, false, true], possible: true },
{ widths: [500, 500, 500], blocked: [false, true, true], possible: false },
];
const testCasesLeft = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
name: "left " + i,
action: "karousel-column-shrink-left",
focus: baseTestCase.widths.length-1,
}));
const testCasesRight = baseTestCases.map((baseTestCase, i) => ({
...baseTestCase,
widths: baseTestCase.widths.slice().reverse(),
blocked: baseTestCase.blocked.slice().reverse(),
name: "right " + i,
action: "karousel-column-shrink-right",
focus: 0,
}));
const testCases = [...testCasesLeft, ...testCasesRight];
for (const testCase of testCases) {
const assertOpt = { message: `Case: ${testCase.name}` };
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const clients = workspaceMock.createClientsWithWidths(...testCase.widths);
workspaceMock.activeWindow = clients[testCase.focus];
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
clients[i].minSize = new MockQmlSize(testCase.widths[i], 100);
}
}
if (testCase.possible) {
qtMock.fireShortcut(testCase.action);
Assert.columnsFillTilingArea(clients, assertOpt);
for (let i = 0; i < clients.length; i++) {
if (testCase.blocked[i]) {
Assert.equal(clients[i].frameGeometry.width, testCase.widths[i], assertOpt);
}
}
}
const frames = clients.map(client => client.frameGeometry);
qtMock.fireShortcut(testCase.action);
const newFrames = clients.map(client => client.frameGeometry);
for (let i = 0; i < clients.length; i++) {
Assert.equalRects(frames[i], newFrames[i], assertOpt);
}
}
});
tests.register("column shrink left (just scroll)", 1, () => {
const config = getDefaultConfig();
const { qtMock, workspaceMock, world } = init(config);
const [ clientLeft, clientMiddle, clientRight ] = workspaceMock.createClientsWithWidths(300, 300, 300);
workspaceMock.activeWindow = clientMiddle;
Assert.notFullyVisible(clientLeft.frameGeometry);
Assert.fullyVisible(clientMiddle.frameGeometry);
Assert.fullyVisible(clientRight.frameGeometry);
qtMock.fireShortcut("karousel-column-shrink-left");
Assert.fullyVisible(clientLeft.frameGeometry);
Assert.fullyVisible(clientMiddle.frameGeometry);
Assert.notFullyVisible(clientRight.frameGeometry);
});

View File

@@ -140,4 +140,45 @@ namespace Assert {
}
}
}
export function fullyVisible(
rect: QmlRect,
{ message, skip=0 }: Options = {},
) {
assert(
rect.left >= tilingArea.left && rect.right <= tilingArea.right,
{
message: appendMessage(`Rect ${rect} not fully visible`, message),
skip: skip + 1,
},
);
}
export function notFullyVisible(
rect: QmlRect,
{ message, skip=0 }: Options = {},
) {
assert(
rect.left < tilingArea.left || rect.right > tilingArea.right,
{
message: appendMessage(`Rect ${rect} is fully visible, but shouldn't be`, message),
skip: skip + 1,
},
);
}
export function columnsFillTilingArea(
columns: KwinClient[],
{ message, skip=0 }: Options = {},
) {
const options = { message: message, skip: skip+1 };
let x = tilingArea.left;
for (const column of columns) {
const width = column.frameGeometry.width;
fullyVisible(column.frameGeometry, options);
rect(column.frameGeometry, x, tilingArea.top, width, tilingArea.height, options);
x += width + gapH;
}
equal(columns[columns.length-1].frameGeometry.right, tilingArea.right, options);
}
}

View File

@@ -7,6 +7,8 @@ let notificationInvalidPresetWidths: Notification;
let screen: QmlRect;
let tilingArea: QmlRect;
let gapH: number;
let gapV: number;
let runLog: string[];
function init(config: Config) {
@@ -17,6 +19,8 @@ function init(config: Config) {
screen.width - config.gapsOuterLeft - config.gapsOuterRight,
screen.height - config.gapsOuterTop - config.gapsOuterBottom,
);
gapH = config.gapsInnerHorizontal;
gapV = config.gapsInnerVertical;
runLog = [];
const qtMock = new MockQt();

View File

@@ -5,7 +5,7 @@ class MockKwinClient {
public readonly shadeable: boolean = false;
public readonly caption = "App";
public readonly minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public minSize: Readonly<QmlSize> = new MockQmlSize(0, 0);
public readonly transient: boolean;
public readonly move: boolean = false;
public readonly resize: boolean = false;