fix: 🐛 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
This commit is contained in:
Derek Stevens 2021-09-30 11:47:14 +00:00 committed by GitHub
parent d196853941
commit 5e262141a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 159 additions and 49 deletions

0
.husky/pre-commit Normal file → Executable file
View File

View File

@ -115,7 +115,7 @@
<item> <item>
<widget class="QCheckBox" name="kcfg_monocleMinimizeRest"> <widget class="QCheckBox" name="kcfg_monocleMinimizeRest">
<property name="text"> <property name="text">
<string>Minimize unfocused windows (WIP)</string> <string>Minimize unfocused windows</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -69,7 +69,7 @@ describe("action", () => {
action.execute(); action.execute();
expect(fakeEngine.focusOrder).toBeCalledWith(1); expect(fakeEngine.focusOrder).toBeCalledWith(1, false);
}); });
}); });
@ -79,7 +79,7 @@ describe("action", () => {
action.execute(); action.execute();
expect(fakeEngine.focusOrder).toBeCalledWith(-1); expect(fakeEngine.focusOrder).toBeCalledWith(-1, false);
}); });
}); });
}); });

View File

@ -87,7 +87,7 @@ export class FocusNextWindow extends ActionImpl implements Action {
} }
public executeWithoutLayoutOverride(): void { 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 { public executeWithoutLayoutOverride(): void {
this.engine.focusOrder(-1); this.engine.focusOrder(-1, false);
} }
} }

View File

@ -9,6 +9,7 @@ import { WindowState } from "../engine/window";
import { DriverContext, KWinDriver } from "../driver"; import { DriverContext, KWinDriver } from "../driver";
import { DriverSurface } from "../driver/surface"; import { DriverSurface } from "../driver/surface";
import MonocleLayout from "../engine/layout/monocle_layout";
import Config from "../config"; import Config from "../config";
import Debug from "../util/debug"; import Debug from "../util/debug";
@ -28,7 +29,6 @@ export interface Controller {
* A bunch of surfaces, that represent the user's screens. * A bunch of surfaces, that represent the user's screens.
*/ */
readonly screens: DriverSurface[]; readonly screens: DriverSurface[];
/** /**
* Current active window. In other words the window, that has focus. * Current active window. In other words the window, that has focus.
*/ */
@ -144,7 +144,6 @@ export interface Controller {
export class TilingController implements Controller { export class TilingController implements Controller {
private engine: Engine; private engine: Engine;
private driver: DriverContext; private driver: DriverContext;
public constructor( public constructor(
qmlObjects: Bismuth.Qml.Main, qmlObjects: Bismuth.Qml.Main,
kwinApi: KWin.Api, kwinApi: KWin.Api,
@ -206,6 +205,12 @@ export class TilingController implements Controller {
{ srf: this.currentSurface }, { srf: this.currentSurface },
]); ]);
this.engine.arrange(); 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 { public onWindowAdded(window: Window): void {
@ -237,6 +242,16 @@ export class TilingController implements Controller {
this.engine.unmanage(window); this.engine.unmanage(window);
this.engine.arrange(); 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 { public onWindowMoveStart(_window: Window): void {
@ -334,6 +349,25 @@ export class TilingController implements Controller {
public onWindowFocused(window: Window): void { public onWindowFocused(window: Window): void {
window.timestamp = new Date().getTime(); 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 { public manageWindow(win: Window): void {

View File

@ -18,8 +18,10 @@ export interface DriverWindow {
readonly maximized: boolean; readonly maximized: boolean;
readonly shouldIgnore: boolean; readonly shouldIgnore: boolean;
readonly shouldFloat: boolean; readonly shouldFloat: boolean;
readonly screen: number;
readonly active: boolean;
surface: DriverSurface; surface: DriverSurface;
minimized: boolean;
commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean): void; commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean): void;
visible(srf: DriverSurface): boolean; visible(srf: DriverSurface): boolean;
@ -41,6 +43,10 @@ export class KWinWindow implements DriverWindow {
return toRect(this.client.geometry); return toRect(this.client.geometry);
} }
public get active(): boolean {
return this.client.active;
}
public get shouldIgnore(): boolean { public get shouldIgnore(): boolean {
const resourceClass = String(this.client.resourceClass); const resourceClass = String(this.client.resourceClass);
const resourceName = String(this.client.resourceName); 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 maximized: boolean;
public get surface(): DriverSurface { public get surface(): DriverSurface {

View File

@ -49,7 +49,13 @@ export interface Engine {
enforceSize(window: Window): void; enforceSize(window: Window): void;
currentLayoutOnCurrentSurface(): WindowsLayout; currentLayoutOnCurrentSurface(): WindowsLayout;
currentWindow(): Window | null; 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; focusDir(dir: Direction): void;
swapOrder(window: Window, step: -1 | 1): void; swapOrder(window: Window, step: -1 | 1): void;
swapDirOrMoveFloat(dir: Direction): void; swapDirOrMoveFloat(dir: Direction): void;
@ -58,7 +64,8 @@ export interface Engine {
floatAll(srf: DriverSurface): void; floatAll(srf: DriverSurface): void;
cycleLayout(step: 1 | -1): void; cycleLayout(step: 1 | -1): void;
setLayout(layoutClassID: string): void; setLayout(layoutClassID: string): void;
minimizeOthers(window: Window): void;
isLayoutMonocleAndMinimizeRest(): boolean;
showNotification(text: string): void; showNotification(text: string): void;
} }
@ -343,42 +350,42 @@ export class TilingEngine implements Engine {
this.windows.remove(window); this.windows.remove(window);
} }
/** /** Focus next or previous window
* Focus the 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; const window = this.controller.currentWindow;
let windows;
/* if no current window, select the first tile. */ if (includeHidden) {
if (window === null) { windows = this.windows.getAllWindows(this.controller.currentSurface);
const tiles = this.windows.getVisibleTiles( } else {
this.controller.currentSurface windows = this.windows.getVisibleWindows(this.controller.currentSurface);
);
if (tiles.length > 1) {
this.controller.currentWindow = tiles[0];
}
return;
} }
const visibles = this.windows.getVisibleWindows( if (windows.length === 0) {
this.controller.currentSurface
);
if (visibles.length === 0) {
// Nothing to focus // Nothing to focus
return; return;
} }
const idx = visibles.indexOf(window); /* If no current window, select the first one. */
if (!window || idx < 0) { if (window === null) {
/* unmanaged window -> focus master */ this.controller.currentWindow = windows[0];
this.controller.currentWindow = visibles[0];
return; 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; 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) { if (layout) {
this.controller.showNotification(layout.description); 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) { if (layout) {
this.controller.showNotification(layout.description); 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 { private getNeighborByDirection(basis: Window, dir: Direction): Window | null {
let vertical: boolean; let vertical: boolean;
let sign: -1 | 1; let sign: -1 | 1;

View File

@ -8,8 +8,6 @@ import { WindowsLayout } from ".";
import Window from "../window"; import Window from "../window";
import { WindowState } from "../window"; import { WindowState } from "../window";
import { KWinWindow } from "../../driver/window";
import { import {
Action, Action,
FocusBottomWindow, FocusBottomWindow,
@ -43,21 +41,9 @@ export default class MonocleLayout implements WindowsLayout {
tile.state = this.config.monocleMaximize tile.state = this.config.monocleMaximize
? WindowState.Maximized ? WindowState.Maximized
: WindowState.Tiled; : WindowState.Tiled;
tile.geometry = area; 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 { public clone(): this {
@ -71,13 +57,13 @@ export default class MonocleLayout implements WindowsLayout {
action instanceof FocusLeftWindow || action instanceof FocusLeftWindow ||
action instanceof FocusPreviousWindow action instanceof FocusPreviousWindow
) { ) {
engine.focusOrder(-1); engine.focusOrder(-1, this.config.monocleMinimizeRest);
} else if ( } else if (
action instanceof FocusBottomWindow || action instanceof FocusBottomWindow ||
action instanceof FocusRightWindow || action instanceof FocusRightWindow ||
action instanceof FocusNextWindow action instanceof FocusNextWindow
) { ) {
engine.focusOrder(1); engine.focusOrder(1, this.config.monocleMinimizeRest);
} else { } else {
console.log("Executing from Monocle regular action!"); console.log("Executing from Monocle regular action!");
action.executeWithoutLayoutOverride(); action.executeWithoutLayoutOverride();

View File

@ -57,6 +57,18 @@ export default class Window {
return this.window.shouldIgnore; 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. */ /** If this window ***can be*** tiled by layout. */
public get tileable(): boolean { public get tileable(): boolean {
return Window.isTileableState(this.state); return Window.isTileableState(this.state);

View File

@ -95,5 +95,17 @@ export default class WindowStore {
return this.list.filter((win) => win.tileable && win.visible(srf)); 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 //#endregion
} }