From 2679112a88f34890d469dea931f1342f88011e80 Mon Sep 17 00:00:00 2001 From: Diogo Ferreira Date: Fri, 5 Aug 2022 16:52:14 +0100 Subject: [PATCH] feat: Add VerticalTileLayout A Vertical tiling layout which is tailored for portrait orientated monitors but might have use cases in landscape as well. The intended behavior is for the master area to take a fixed (but configurable via shortcut) percentage of the screen and for the remaining panes to split the remaining space. Multiple masters are supported and will share the master area equally. Two masters, followed by a stack of two panels: --------- | | | M1 | | | | ------- | | | | M2 | | | --------- | 1 | --------- | 2 | --------- --- src/config/bismuth_config.kcfg | 5 + src/core/ts-proxy.cpp | 1 + src/kcm/package/contents/ui/views/Layouts.qml | 5 + src/kwinscript/controller/action.ts | 13 + src/kwinscript/controller/index.ts | 1 + .../engine/layout/vertical_layout.ts | 223 ++++++++++++++++++ src/kwinscript/engine/layout_store.ts | 3 + .../icons/16-status-bismuth-vertical-tile.svg | 5 + .../icons/32-status-bismuth-vertical-tile.svg | 5 + src/kwinscript/icons/CMakeLists.txt | 2 + 10 files changed, 263 insertions(+) create mode 100644 src/kwinscript/engine/layout/vertical_layout.ts create mode 100644 src/kwinscript/icons/16-status-bismuth-vertical-tile.svg create mode 100644 src/kwinscript/icons/32-status-bismuth-vertical-tile.svg diff --git a/src/config/bismuth_config.kcfg b/src/config/bismuth_config.kcfg index f4e57ede..a0102cfb 100644 --- a/src/config/bismuth_config.kcfg +++ b/src/config/bismuth_config.kcfg @@ -48,6 +48,11 @@ false + + + false + + diff --git a/src/core/ts-proxy.cpp b/src/core/ts-proxy.cpp index 0fa25a9f..32314a73 100644 --- a/src/core/ts-proxy.cpp +++ b/src/core/ts-proxy.cpp @@ -54,6 +54,7 @@ QJSValue TSProxy::jsConfig() addLayout("enableQuarterLayout", "QuarterLayout"); addLayout("enableFloatingLayout", "FloatingLayout"); addLayout("enableCascadeLayout", "CascadeLayout"); + addLayout("enableVerticalTileLayout", "VerticalTileLayout"); setProp("monocleMaximize", m_config.monocleMaximize()); setProp("maximizeSoleTile", m_config.maximizeSoleTile()); diff --git a/src/kcm/package/contents/ui/views/Layouts.qml b/src/kcm/package/contents/ui/views/Layouts.qml index cd59dc1d..e62cccb0 100644 --- a/src/kcm/package/contents/ui/views/Layouts.qml +++ b/src/kcm/package/contents/ui/views/Layouts.qml @@ -57,6 +57,11 @@ Kirigami.Page { settingName: "enableFloatingLayout" } + ListElement { + name: "Vertical Tile" + settingName: "enableVerticalTileLayout" + } + } KCM.ScrollView { diff --git a/src/kwinscript/controller/action.ts b/src/kwinscript/controller/action.ts index c83da63f..9599fdb4 100644 --- a/src/kwinscript/controller/action.ts +++ b/src/kwinscript/controller/action.ts @@ -570,6 +570,19 @@ export class ToggleSpiralLayout extends ToggleCurrentLayout { } } +export class ToggleVerticalTileLayout extends ToggleCurrentLayout { + constructor(protected engine: Engine, protected log: Log) { + super( + engine, + "VerticalTileLayout", + "toggle_vertical_tile_layout", + "Toggle Vertical Tile Layout", + "", + log + ); + } +} + export class Rotate extends ActionImpl implements Action { constructor(protected engine: Engine, protected log: Log) { super(engine, "rotate", "Rotate", "Meta+R", log); diff --git a/src/kwinscript/controller/index.ts b/src/kwinscript/controller/index.ts index aae1af0d..a3c88695 100644 --- a/src/kwinscript/controller/index.ts +++ b/src/kwinscript/controller/index.ts @@ -424,6 +424,7 @@ export class ControllerImpl implements Controller { new Action.ToggleFloatingLayout(this.engine, this.log), new Action.ToggleQuarterLayout(this.engine, this.log), new Action.ToggleSpiralLayout(this.engine, this.log), + new Action.ToggleVerticalTileLayout(this.engine, this.log), new Action.Rotate(this.engine, this.log), new Action.RotateReverse(this.engine, this.log), diff --git a/src/kwinscript/engine/layout/vertical_layout.ts b/src/kwinscript/engine/layout/vertical_layout.ts new file mode 100644 index 00000000..c95115eb --- /dev/null +++ b/src/kwinscript/engine/layout/vertical_layout.ts @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2022 Diogo Ferreira +// +// SPDX-License-Identifier: MIT + +import { WindowsLayout } from "."; +import LayoutUtils from "./layout_utils"; + +import { WindowState, EngineWindow } from "../window"; + +import { + Action, + DecreaseLayoutMasterAreaSize, + IncreaseLayoutMasterAreaSize, + IncreaseMasterAreaWindowCount, + DecreaseMasterAreaWindowCount, + Rotate, + RotateReverse, +} from "../../controller/action"; + +import { partitionArrayBySizes, clip, slide } from "../../util/func"; +import { Rect, RectDelta } from "../../util/rect"; +import { Config } from "../../config"; +import { Controller } from "../../controller"; +import { Engine } from ".."; + +/** + * A Vertical tiling layout which is tailored for portrait orientated + * monitors but might have use cases in landscape as well. + * + * The intended behavior is for the master area to take a fixed (but + * configurable via shortcut) percentage of the screen and for the + * remaining panes to split the remaining space. Multiple masters are + * supported and will share the master area equally. + * + * --------- + * | | + * | M1 | + * | | + * | ------- | + * | | + * | M2 | + * | | + * --------- + * | 1 | + * --------- + * | 2 | + * --------- + */ +export default class VerticalTileLayout implements WindowsLayout { + public static readonly MIN_MASTER_RATIO = 0.2; + public static readonly MAX_MASTER_RATIO = 0.75; + public static readonly id = "VerticalTileLayout"; + public readonly classID = VerticalTileLayout.id; + public readonly name = "Vertical Tile Layout"; + public readonly icon = "bismuth-vertical-tile"; + + public get hint(): string { + return String(this.masterCount); + } + + private masterRatio: number; + private masterCount: number; + + private config: Config; + + constructor(config: Config) { + this.config = config; + this.masterRatio = 0.75; + this.masterCount = 1; + } + + public adjust( + area: Rect, + tiles: EngineWindow[], + basis: EngineWindow, + delta: RectDelta + ): void { + const basisIndex = tiles.indexOf(basis); + if (basisIndex < 0) { + return; + } + + if (tiles.length === 0) { + /* no tiles */ + return; + } else if (tiles.length <= this.masterCount) { + /* every window takes a piece of the master area */ + LayoutUtils.adjustAreaWeights( + area, + tiles.map((tile) => tile.weight), + this.config.tileLayoutGap, + tiles.indexOf(basis), + delta + ).forEach((newWeight, i) => (tiles[i].weight = newWeight * tiles.length)); + } else if (tiles.length > this.masterCount) { + /* Two rows */ + let basisGroup; + if (basisIndex < this.masterCount) { + /* master area */ + basisGroup = 1; + } else { + /* bottom stack */ + basisGroup = 0; + } + + /* adjust master-stack ratio */ + const stackRatio = 1 - this.masterRatio; + const newRatios = LayoutUtils.adjustAreaWeights( + area, + [stackRatio, this.masterRatio, stackRatio], + this.config.tileLayoutGap, + basisGroup, + delta, + false /* vertical */ + ); + const newMasterRatio = newRatios[1]; + const newStackRatio = basisGroup === 0 ? newRatios[0] : newRatios[2]; + this.masterRatio = newMasterRatio / (newMasterRatio + newStackRatio); + + /* adjust tile weight */ + const bottomStackNumTiles = tiles.length - this.masterCount; + const [masterTiles, bottomStackTiles] = + partitionArrayBySizes(tiles, [ + this.masterCount, + bottomStackNumTiles, + ]); + const groupTiles = [masterTiles, bottomStackTiles][basisGroup]; + LayoutUtils.adjustAreaWeights( + area /* we only need height */, + groupTiles.map((tile) => tile.weight), + this.config.tileLayoutGap, + groupTiles.indexOf(basis), + delta, + false /* vertical */ + ).forEach( + (newWeight, i) => (groupTiles[i].weight = newWeight * groupTiles.length) + ); + } + } + + public apply( + _controller: Controller, + tileables: EngineWindow[], + area: Rect + ): void { + /* Tile all tileables */ + tileables.forEach((tileable) => (tileable.state = WindowState.Tiled)); + const tiles = tileables; + + if (tiles.length <= this.masterCount) { + /* only master */ + LayoutUtils.splitAreaWeighted( + area, + tiles.map((tile) => tile.weight), + this.config.tileLayoutGap, + false /* vertical */ + ).forEach((tileArea, i) => (tiles[i].geometry = tileArea)); + } else { + /* master & bottom-stack */ + const stackRatio = 1 - this.masterRatio; + + /** Areas allocated to master, and bottom-stack */ + const groupAreas = LayoutUtils.splitAreaWeighted( + area, + [this.masterRatio, stackRatio], + this.config.tileLayoutGap, + false /* vertical */ + ); + + const bottomStackNumTiles = tiles.length - this.masterCount; + const [masterTiles, bottomStackTiles] = + partitionArrayBySizes(tiles, [ + this.masterCount, + bottomStackNumTiles, + ]); + [masterTiles, bottomStackTiles].forEach((groupTiles, group) => { + LayoutUtils.splitAreaWeighted( + groupAreas[group], + groupTiles.map((tile) => tile.weight), + this.config.tileLayoutGap, + false /* vertical */ + ).forEach((tileArea, i) => (groupTiles[i].geometry = tileArea)); + }); + } + } + + public clone(): WindowsLayout { + const other = new VerticalTileLayout(this.config); + other.masterRatio = this.masterRatio; + return other; + } + + public executeAction(engine: Engine, action: Action): void { + if (action instanceof DecreaseLayoutMasterAreaSize) { + this.masterRatio = clip( + slide(this.masterRatio, -0.05), + VerticalTileLayout.MIN_MASTER_RATIO, + VerticalTileLayout.MAX_MASTER_RATIO + ); + } else if (action instanceof IncreaseLayoutMasterAreaSize) { + this.masterRatio = clip( + slide(this.masterRatio, +0.05), + VerticalTileLayout.MIN_MASTER_RATIO, + VerticalTileLayout.MAX_MASTER_RATIO + ); + } else if (action instanceof IncreaseMasterAreaWindowCount) { + this.resizeMaster(engine, 1); + } else if (action instanceof DecreaseMasterAreaWindowCount) { + this.resizeMaster(engine, -1); + } else { + action.executeWithoutLayoutOverride(); + } + } + + public toString(): string { + return `VerticalTileLayout(masterCount=${this.masterCount})`; + } + + private resizeMaster(engine: Engine, step: -1 | 1): void { + this.masterCount = clip(this.masterCount + step, 1, 10); + engine.showLayoutNotification(); + } +} diff --git a/src/kwinscript/engine/layout_store.ts b/src/kwinscript/engine/layout_store.ts index 8d1e8a68..86a773e7 100644 --- a/src/kwinscript/engine/layout_store.ts +++ b/src/kwinscript/engine/layout_store.ts @@ -19,6 +19,7 @@ import SpiralLayout from "./layout/spiral_layout"; import SpreadLayout from "./layout/spread_layout"; import StairLayout from "./layout/stair_layout"; import ThreeColumnLayout from "./layout/three_column_layout"; +import VerticalTileLayout from "./layout/vertical_layout"; export class LayoutStoreEntry { public get currentLayout(): WindowsLayout { @@ -96,6 +97,8 @@ export class LayoutStoreEntry { return new ThreeColumnLayout(this.config); } else if (id == TileLayout.id) { return new TileLayout(this.config); + } else if (id == VerticalTileLayout.id) { + return new VerticalTileLayout(this.config); } else { return new FloatingLayout(); } diff --git a/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg b/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg new file mode 100644 index 00000000..e11b1df6 --- /dev/null +++ b/src/kwinscript/icons/16-status-bismuth-vertical-tile.svg @@ -0,0 +1,5 @@ + + diff --git a/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg b/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg new file mode 100644 index 00000000..bd9e808f --- /dev/null +++ b/src/kwinscript/icons/32-status-bismuth-vertical-tile.svg @@ -0,0 +1,5 @@ + + diff --git a/src/kwinscript/icons/CMakeLists.txt b/src/kwinscript/icons/CMakeLists.txt index 0c73ca01..a0e99571 100644 --- a/src/kwinscript/icons/CMakeLists.txt +++ b/src/kwinscript/icons/CMakeLists.txt @@ -11,6 +11,7 @@ ecm_install_icons( 16-status-bismuth-spread.svg 16-status-bismuth-stair.svg 16-status-bismuth-tile.svg + 16-status-bismuth-vertical-tile.svg 32-status-bismuth-column.svg 32-status-bismuth-floating.svg 32-status-bismuth-monocle.svg @@ -19,5 +20,6 @@ ecm_install_icons( 32-status-bismuth-spread.svg 32-status-bismuth-stair.svg 32-status-bismuth-tile.svg + 32-status-bismuth-vertical-tile.svg DESTINATION ${KDE_INSTALL_ICONDIR})