Actions: add actions columnShrinkLeft and columnShrinkRight
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
77
src/tests/flows/columnShrinkSide.ts
Normal file
77
src/tests/flows/columnShrinkSide.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user