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})