From 5e262141a114e3d7163355e03d163c372bd050aa Mon Sep 17 00:00:00 2001 From: Derek Stevens Date: Thu, 30 Sep 2021 11:47:14 +0000 Subject: [PATCH] fix: :bug: Monocle Layout "minimize unfocused windows" fixes and improvements ## Summary Fixes Monocle Layout with `Minimize Unfocused Windows` (internally `config.monocleMinimizeRest`) option enabled. Restores previous functionality (before Wayland patches) and adds * minimize inactive windows only on same monitor with multimonitor setup * can switch windows with focus-changing `Actions` * switches to the next window when closing the active window (still a little buggy in multimonitor, but a big improvment) * handles moving window onto a `Surface` with this layout properly ## UI Changes Removed "(WIP)" from "Minimize unfocused windows" option in the config UI. ## Test Plan 1. Reload script 2. Verify all behavior in the Summary ## Related Issues Closes #43, #55 --- .husky/pre-commit | 0 res/config.ui | 2 +- src/controller/action.test.ts | 4 +- src/controller/action.ts | 4 +- src/controller/index.ts | 38 +++++++++++- src/driver/window.ts | 20 +++++- src/engine/index.ts | 96 +++++++++++++++++++++-------- src/engine/layout/monocle_layout.ts | 20 +----- src/engine/window.ts | 12 ++++ src/engine/window_store.ts | 12 ++++ 10 files changed, 159 insertions(+), 49 deletions(-) mode change 100644 => 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit old mode 100644 new mode 100755 diff --git a/res/config.ui b/res/config.ui index e3e84a49..5c58d597 100644 --- a/res/config.ui +++ b/res/config.ui @@ -115,7 +115,7 @@ - Minimize unfocused windows (WIP) + Minimize unfocused windows diff --git a/src/controller/action.test.ts b/src/controller/action.test.ts index a60ea828..810a4221 100644 --- a/src/controller/action.test.ts +++ b/src/controller/action.test.ts @@ -69,7 +69,7 @@ describe("action", () => { action.execute(); - expect(fakeEngine.focusOrder).toBeCalledWith(1); + expect(fakeEngine.focusOrder).toBeCalledWith(1, false); }); }); @@ -79,7 +79,7 @@ describe("action", () => { action.execute(); - expect(fakeEngine.focusOrder).toBeCalledWith(-1); + expect(fakeEngine.focusOrder).toBeCalledWith(-1, false); }); }); }); diff --git a/src/controller/action.ts b/src/controller/action.ts index b31f47ee..d82356fe 100644 --- a/src/controller/action.ts +++ b/src/controller/action.ts @@ -87,7 +87,7 @@ export class FocusNextWindow extends ActionImpl implements Action { } public executeWithoutLayoutOverride(): void { - this.engine.focusOrder(+1); + this.engine.focusOrder(+1, false); } } @@ -97,7 +97,7 @@ export class FocusPreviousWindow extends ActionImpl implements Action { } public executeWithoutLayoutOverride(): void { - this.engine.focusOrder(-1); + this.engine.focusOrder(-1, false); } } diff --git a/src/controller/index.ts b/src/controller/index.ts index c34fac99..5ba13220 100644 --- a/src/controller/index.ts +++ b/src/controller/index.ts @@ -9,6 +9,7 @@ import { WindowState } from "../engine/window"; import { DriverContext, KWinDriver } from "../driver"; import { DriverSurface } from "../driver/surface"; +import MonocleLayout from "../engine/layout/monocle_layout"; import Config from "../config"; import Debug from "../util/debug"; @@ -28,7 +29,6 @@ export interface Controller { * A bunch of surfaces, that represent the user's screens. */ readonly screens: DriverSurface[]; - /** * Current active window. In other words the window, that has focus. */ @@ -144,7 +144,6 @@ export interface Controller { export class TilingController implements Controller { private engine: Engine; private driver: DriverContext; - public constructor( qmlObjects: Bismuth.Qml.Main, kwinApi: KWin.Api, @@ -206,6 +205,12 @@ export class TilingController implements Controller { { srf: this.currentSurface }, ]); this.engine.arrange(); + /* HACK: minimize others and change geometry with Monocle Layout and + * config.monocleMinimizeRest + */ + if (this.currentWindow) { + this.onWindowFocused(this.currentWindow); + } } public onWindowAdded(window: Window): void { @@ -237,6 +242,16 @@ export class TilingController implements Controller { this.engine.unmanage(window); this.engine.arrange(); + + // Switch to next window if monocle with config.monocleMinimizeRest + if (!this.currentWindow && this.engine.isLayoutMonocleAndMinimizeRest()) { + this.engine.focusOrder(1, true); + /* HACK: force window to maximize if it isn't already + * This is ultimately to trigger onWindowFocused() at the right time + */ + this.engine.focusOrder(1, true); + this.engine.focusOrder(-1, true); + } } public onWindowMoveStart(_window: Window): void { @@ -334,6 +349,25 @@ export class TilingController implements Controller { public onWindowFocused(window: Window): void { window.timestamp = new Date().getTime(); + this.currentWindow = window; + // Minimize other windows if Moncole and config.monocleMinimizeRest + if ( + this.engine.isLayoutMonocleAndMinimizeRest() && + this.engine.windows.getVisibleTiles(window.surface).includes(window) + ) { + /* If a window hasn't been foucsed in this layout yet, ensure its geometry + * gets maximized. + */ + this.engine + .currentLayoutOnCurrentSurface() + .apply( + this, + this.engine.windows.getAllTileables(window.surface), + window.surface.workingArea + ); + + this.engine.minimizeOthers(window); + } } public manageWindow(win: Window): void { diff --git a/src/driver/window.ts b/src/driver/window.ts index 930d002d..900b6ff2 100644 --- a/src/driver/window.ts +++ b/src/driver/window.ts @@ -18,8 +18,10 @@ export interface DriverWindow { readonly maximized: boolean; readonly shouldIgnore: boolean; readonly shouldFloat: boolean; - + readonly screen: number; + readonly active: boolean; surface: DriverSurface; + minimized: boolean; commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean): void; visible(srf: DriverSurface): boolean; @@ -41,6 +43,10 @@ export class KWinWindow implements DriverWindow { return toRect(this.client.geometry); } + public get active(): boolean { + return this.client.active; + } + public get shouldIgnore(): boolean { const resourceClass = String(this.client.resourceClass); const resourceName = String(this.client.resourceName); @@ -69,6 +75,18 @@ export class KWinWindow implements DriverWindow { ); } + public get screen(): number { + return this.client.screen; + } + + public get minimized(): boolean { + return this.client.minimized; + } + + public set minimized(min: boolean) { + this.client.minimized = min; + } + public maximized: boolean; public get surface(): DriverSurface { diff --git a/src/engine/index.ts b/src/engine/index.ts index 3b26a511..42e26ad2 100644 --- a/src/engine/index.ts +++ b/src/engine/index.ts @@ -49,7 +49,13 @@ export interface Engine { enforceSize(window: Window): void; currentLayoutOnCurrentSurface(): WindowsLayout; currentWindow(): Window | null; - focusOrder(step: -1 | 1): void; + + /** + * Focus next or previous window + * @param step Direction to step in (1=forward, -1=backward) + * @param includeHidden Whether to step through (true) or skip over (false) minimized windows + */ + focusOrder(step: -1 | 1, includeHidden: boolean): void; focusDir(dir: Direction): void; swapOrder(window: Window, step: -1 | 1): void; swapDirOrMoveFloat(dir: Direction): void; @@ -58,7 +64,8 @@ export interface Engine { floatAll(srf: DriverSurface): void; cycleLayout(step: 1 | -1): void; setLayout(layoutClassID: string): void; - + minimizeOthers(window: Window): void; + isLayoutMonocleAndMinimizeRest(): boolean; showNotification(text: string): void; } @@ -343,42 +350,42 @@ export class TilingEngine implements Engine { this.windows.remove(window); } - /** - * Focus the next or previous window. + /** Focus next or previous window + * @param step direction to step in (1 for forward, -1 for back) + * @param includeHidden whether to switch to or skip minimized windows */ - public focusOrder(step: -1 | 1): void { + public focusOrder(step: -1 | 1, includeHidden = false): void { const window = this.controller.currentWindow; + let windows; - /* if no current window, select the first tile. */ - if (window === null) { - const tiles = this.windows.getVisibleTiles( - this.controller.currentSurface - ); - if (tiles.length > 1) { - this.controller.currentWindow = tiles[0]; - } - return; + if (includeHidden) { + windows = this.windows.getAllWindows(this.controller.currentSurface); + } else { + windows = this.windows.getVisibleWindows(this.controller.currentSurface); } - const visibles = this.windows.getVisibleWindows( - this.controller.currentSurface - ); - if (visibles.length === 0) { + if (windows.length === 0) { // Nothing to focus return; } - const idx = visibles.indexOf(window); - if (!window || idx < 0) { - /* unmanaged window -> focus master */ - this.controller.currentWindow = visibles[0]; + /* If no current window, select the first one. */ + if (window === null) { + this.controller.currentWindow = windows[0]; return; } - const num = visibles.length; + const idx = windows.indexOf(window); + if (!window || idx < 0) { + /* This probably shouldn't happen, but just in case... */ + this.controller.currentWindow = windows[0]; + return; + } + + const num = windows.length; const newIndex = (idx + (step % num) + num) % num; - this.controller.currentWindow = visibles[newIndex]; + this.controller.currentWindow = windows[newIndex]; } /** @@ -548,6 +555,14 @@ export class TilingEngine implements Engine { ); if (layout) { this.controller.showNotification(layout.description); + + // Minimize inactive windows if Monocle and config.monocleMinimizeRest + if ( + this.isLayoutMonocleAndMinimizeRest() && + this.controller.currentWindow + ) { + this.minimizeOthers(this.controller.currentWindow); + } } } @@ -561,9 +576,42 @@ export class TilingEngine implements Engine { ); if (layout) { this.controller.showNotification(layout.description); + + // Minimize inactive windows if Monocle and config.monocleMinimizeRest + if ( + this.isLayoutMonocleAndMinimizeRest() && + this.controller.currentWindow + ) { + this.minimizeOthers(this.controller.currentWindow); + } } } + /** + * Minimize all windows on the surface except the given window. + * Used mainly in Monocle mode with config.monocleMinimizeRest + */ + public minimizeOthers(window: Window): void { + for (const tile of this.windows.getVisibleTiles(window.surface)) { + if ( + tile.screen == window.screen && + tile.id !== window.id && + this.windows.getVisibleTiles(window.surface).includes(window) + ) { + tile.minimized = true; + } else { + tile.minimized = false; + } + } + } + + public isLayoutMonocleAndMinimizeRest(): boolean { + return ( + this.currentLayoutOnCurrentSurface() instanceof MonocleLayout && + this.config.monocleMinimizeRest + ); + } + private getNeighborByDirection(basis: Window, dir: Direction): Window | null { let vertical: boolean; let sign: -1 | 1; diff --git a/src/engine/layout/monocle_layout.ts b/src/engine/layout/monocle_layout.ts index 04aed513..ecfad0a7 100644 --- a/src/engine/layout/monocle_layout.ts +++ b/src/engine/layout/monocle_layout.ts @@ -8,8 +8,6 @@ import { WindowsLayout } from "."; import Window from "../window"; import { WindowState } from "../window"; -import { KWinWindow } from "../../driver/window"; - import { Action, FocusBottomWindow, @@ -43,21 +41,9 @@ export default class MonocleLayout implements WindowsLayout { tile.state = this.config.monocleMaximize ? WindowState.Maximized : WindowState.Tiled; + tile.geometry = area; }); - - /* KWin-specific `monocleMinimizeRest` option */ - if (this.config.monocleMinimizeRest) { - const tiles = [...tileables]; - const current = controller.currentWindow; - if (current && current.tiled) { - tiles.forEach((window) => { - if (window !== current) { - (window.window as KWinWindow).client.minimized = true; - } - }); - } - } } public clone(): this { @@ -71,13 +57,13 @@ export default class MonocleLayout implements WindowsLayout { action instanceof FocusLeftWindow || action instanceof FocusPreviousWindow ) { - engine.focusOrder(-1); + engine.focusOrder(-1, this.config.monocleMinimizeRest); } else if ( action instanceof FocusBottomWindow || action instanceof FocusRightWindow || action instanceof FocusNextWindow ) { - engine.focusOrder(1); + engine.focusOrder(1, this.config.monocleMinimizeRest); } else { console.log("Executing from Monocle regular action!"); action.executeWithoutLayoutOverride(); diff --git a/src/engine/window.ts b/src/engine/window.ts index 09d7e029..5da969d5 100644 --- a/src/engine/window.ts +++ b/src/engine/window.ts @@ -57,6 +57,18 @@ export default class Window { return this.window.shouldIgnore; } + public get screen(): number { + return this.window.screen; + } + + public get minimized(): boolean { + return this.window.minimized; + } + + public set minimized(min: boolean) { + this.window.minimized = min; + } + /** If this window ***can be*** tiled by layout. */ public get tileable(): boolean { return Window.isTileableState(this.state); diff --git a/src/engine/window_store.ts b/src/engine/window_store.ts index e3d6ced3..187caaad 100644 --- a/src/engine/window_store.ts +++ b/src/engine/window_store.ts @@ -95,5 +95,17 @@ export default class WindowStore { return this.list.filter((win) => win.tileable && win.visible(srf)); } + /** + * Return all "tileable" windows on the given surface, including hidden + */ + public getAllTileables(srf: DriverSurface): Window[] { + return this.list.filter((win) => win.tileable && win.surface.id === srf.id); + } + + /** Return all windows on this surface, including minimized windows */ + public getAllWindows(srf: DriverSurface): Window[] { + return this.list.filter((win) => win.surface.id === srf.id); + } + //#endregion }