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>
<widget class="QCheckBox" name="kcfg_monocleMinimizeRest">
<property name="text">
<string>Minimize unfocused windows (WIP)</string>
<string>Minimize unfocused windows</string>
</property>
</widget>
</item>

View File

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

View File

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

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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();

View File

@ -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);

View File

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