refactor: use prettier

This commit is contained in:
Mikhail Zolotukhin 2021-08-27 23:06:44 +03:00
parent 96cb46ef00
commit 50bd6aea61
41 changed files with 3659 additions and 3212 deletions

4
.prettierignore Normal file
View File

@ -0,0 +1,4 @@
build
node_modules
*.md

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
{}

View File

@ -13,6 +13,7 @@
},
"devDependencies": {
"mocha": "^6.0.0",
"prettier": "2.3.2",
"typedoc": "^0.21.6",
"typescript": "^4.3.5"
},

View File

@ -19,132 +19,132 @@
// DEALINGS IN THE SOFTWARE.
enum Shortcut {
Left,
Right,
Up,
Down,
Left,
Right,
Up,
Down,
/* Alternate HJKL bindings */
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
/* Alternate HJKL bindings */
FocusUp,
FocusDown,
FocusLeft,
FocusRight,
ShiftLeft,
ShiftRight,
ShiftUp,
ShiftDown,
ShiftLeft,
ShiftRight,
ShiftUp,
ShiftDown,
SwapUp,
SwapDown,
SwapLeft,
SwapRight,
SwapUp,
SwapDown,
SwapLeft,
SwapRight,
GrowWidth,
GrowHeight,
ShrinkWidth,
ShrinkHeight,
GrowWidth,
GrowHeight,
ShrinkWidth,
ShrinkHeight,
Increase,
Decrease,
ShiftIncrease,
ShiftDecrease,
Increase,
Decrease,
ShiftIncrease,
ShiftDecrease,
ToggleFloat,
ToggleFloatAll,
SetMaster,
NextLayout,
PreviousLayout,
SetLayout,
ToggleFloat,
ToggleFloatAll,
SetMaster,
NextLayout,
PreviousLayout,
SetLayout,
Rotate,
RotatePart,
Rotate,
RotatePart,
}
//#region Driver
interface IConfig {
//#region Layout
layoutOrder: string[];
layoutFactories: {[key: string]: () => ILayout};
monocleMaximize: boolean;
maximizeSoleTile: boolean;
//#endregion
//#region Layout
layoutOrder: string[];
layoutFactories: { [key: string]: () => ILayout };
monocleMaximize: boolean;
maximizeSoleTile: boolean;
//#endregion
//#region Features
adjustLayout: boolean;
adjustLayoutLive: boolean;
keepFloatAbove: boolean;
noTileBorder: boolean;
limitTileWidthRatio: number;
//#endregion
//#region Features
adjustLayout: boolean;
adjustLayoutLive: boolean;
keepFloatAbove: boolean;
noTileBorder: boolean;
limitTileWidthRatio: number;
//#endregion
//#region Gap
screenGapBottom: number;
screenGapLeft: number;
screenGapRight: number;
screenGapTop: number;
tileLayoutGap: number;
//#endregion
//#region Gap
screenGapBottom: number;
screenGapLeft: number;
screenGapRight: number;
screenGapTop: number;
tileLayoutGap: number;
//#endregion
//#region Behavior
directionalKeyMode: "dwm" | "focus";
newWindowAsMaster: boolean;
//#endregion
//#region Behavior
directionalKeyMode: "dwm" | "focus";
newWindowAsMaster: boolean;
//#endregion
}
interface IDriverWindow {
readonly fullScreen: boolean;
readonly geometry: Readonly<Rect>;
readonly id: string;
readonly maximized: boolean;
readonly shouldIgnore: boolean;
readonly shouldFloat: boolean;
readonly fullScreen: boolean;
readonly geometry: Readonly<Rect>;
readonly id: string;
readonly maximized: boolean;
readonly shouldIgnore: boolean;
readonly shouldFloat: boolean;
surface: ISurface;
surface: ISurface;
commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean): void;
visible(srf: ISurface): boolean;
commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean): void;
visible(srf: ISurface): boolean;
}
interface ISurface {
readonly id: string;
readonly ignore: boolean;
readonly workingArea: Readonly<Rect>;
readonly id: string;
readonly ignore: boolean;
readonly workingArea: Readonly<Rect>;
next(): ISurface | null;
next(): ISurface | null;
}
interface IDriverContext {
readonly backend: string;
readonly screens: ISurface[];
readonly cursorPosition: [number, number] | null;
readonly backend: string;
readonly screens: ISurface[];
readonly cursorPosition: [number, number] | null;
currentSurface: ISurface;
currentWindow: Window | null;
currentSurface: ISurface;
currentWindow: Window | null;
setTimeout(func: () => void, timeout: number): void;
showNotification(text: string): void;
setTimeout(func: () => void, timeout: number): void;
showNotification(text: string): void;
}
//#endregion
interface ILayoutClass {
readonly id: string;
new(): ILayout;
readonly id: string;
new (): ILayout;
}
interface ILayout {
/* read-only */
readonly capacity?: number;
readonly description: string;
/* read-only */
readonly capacity?: number;
readonly description: string;
/* methods */
adjust?(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): void;
apply(ctx: EngineContext, tileables: Window[], area: Rect): void;
handleShortcut?(ctx: EngineContext, input: Shortcut, data?: any): boolean;
/* methods */
adjust?(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): void;
apply(ctx: EngineContext, tileables: Window[], area: Rect): void;
handleShortcut?(ctx: EngineContext, input: Shortcut, data?: any): boolean;
toString(): string;
toString(): string;
}
let CONFIG: IConfig;

View File

@ -19,136 +19,139 @@
// DEALINGS IN THE SOFTWARE.
class KWinConfig implements IConfig {
//#region Layout
public layoutOrder: string[];
public layoutFactories: {[key: string]: () => ILayout};
public maximizeSoleTile: boolean;
public monocleMaximize: boolean;
public monocleMinimizeRest: boolean; // KWin-specific
//#endregion
//#region Layout
public layoutOrder: string[];
public layoutFactories: { [key: string]: () => ILayout };
public maximizeSoleTile: boolean;
public monocleMaximize: boolean;
public monocleMinimizeRest: boolean; // KWin-specific
//#endregion
//#region Features
public adjustLayout: boolean;
public adjustLayoutLive: boolean;
public keepFloatAbove: boolean;
public noTileBorder: boolean;
public limitTileWidthRatio: number;
//#endregion
//#region Features
public adjustLayout: boolean;
public adjustLayoutLive: boolean;
public keepFloatAbove: boolean;
public noTileBorder: boolean;
public limitTileWidthRatio: number;
//#endregion
//#region Gap
public screenGapBottom: number;
public screenGapLeft: number;
public screenGapRight: number;
public screenGapTop: number;
public tileLayoutGap: number;
//#endregion
//#region Gap
public screenGapBottom: number;
public screenGapLeft: number;
public screenGapRight: number;
public screenGapTop: number;
public tileLayoutGap: number;
//#endregion
//#region Behavior
public directionalKeyMode: "dwm" | "focus";
public newWindowAsMaster: boolean;
//#endregion
//#region Behavior
public directionalKeyMode: "dwm" | "focus";
public newWindowAsMaster: boolean;
//#endregion
//#region KWin-specific
public layoutPerActivity: boolean;
public layoutPerDesktop: boolean;
public preventMinimize: boolean;
public preventProtrusion: boolean;
public pollMouseXdotool: boolean;
//#endregion
//#region KWin-specific
public layoutPerActivity: boolean;
public layoutPerDesktop: boolean;
public preventMinimize: boolean;
public preventProtrusion: boolean;
public pollMouseXdotool: boolean;
//#endregion
//#region KWin-specific Rules
public floatUtility: boolean;
//#region KWin-specific Rules
public floatUtility: boolean;
public floatingClass: string[];
public floatingTitle: string[];
public ignoreClass: string[];
public ignoreTitle: string[];
public ignoreRole: string[];
public floatingClass: string[];
public floatingTitle: string[];
public ignoreClass: string[];
public ignoreTitle: string[];
public ignoreRole: string[];
public ignoreActivity: string[];
public ignoreScreen: number[];
//#endregion
public ignoreActivity: string[];
public ignoreScreen: number[];
//#endregion
constructor() {
function commaSeparate(str: string): string[] {
if (!str || typeof str !== "string")
return [];
return str.split(",").map((part) => part.trim());
}
DEBUG.enabled = DEBUG.enabled || KWin.readConfig("debug", false);
this.layoutOrder = [];
this.layoutFactories = {};
([
["enableTileLayout" , true , TileLayout ],
["enableMonocleLayout" , true , MonocleLayout ],
["enableThreeColumnLayout", true , ThreeColumnLayout],
["enableSpreadLayout" , true , SpreadLayout ],
["enableStairLayout" , true , StairLayout ],
["enableSpiralLayout" , true , SpiralLayout ],
["enableQuarterLayout" , false, QuarterLayout ],
["enableFloatingLayout" , false, FloatingLayout ],
["enableCascadeLayout" , false, CascadeLayout ], // TODO: add config
] as Array<[string, boolean, ILayoutClass]>)
.forEach(([configKey, defaultValue, layoutClass]) => {
if (KWin.readConfig(configKey, defaultValue))
this.layoutOrder.push(layoutClass.id);
this.layoutFactories[layoutClass.id] = (() => new layoutClass());
});
this.maximizeSoleTile = KWin.readConfig("maximizeSoleTile" , false);
this.monocleMaximize = KWin.readConfig("monocleMaximize" , true);
this.monocleMinimizeRest = KWin.readConfig("monocleMinimizeRest" , false);
this.adjustLayout = KWin.readConfig("adjustLayout" , true);
this.adjustLayoutLive = KWin.readConfig("adjustLayoutLive" , true);
this.keepFloatAbove = KWin.readConfig("keepFloatAbove" , true);
this.noTileBorder = KWin.readConfig("noTileBorder" , false);
this.limitTileWidthRatio = 0;
if (KWin.readConfig("limitTileWidth" , false))
this.limitTileWidthRatio = KWin.readConfig("limitTileWidthRatio", 1.6);
this.screenGapBottom = KWin.readConfig("screenGapBottom" , 0);
this.screenGapLeft = KWin.readConfig("screenGapLeft" , 0);
this.screenGapRight = KWin.readConfig("screenGapRight" , 0);
this.screenGapTop = KWin.readConfig("screenGapTop" , 0);
this.tileLayoutGap = KWin.readConfig("tileLayoutGap" , 0);
const directionalKeyDwm = KWin.readConfig("directionalKeyDwm" , true);
const directionalKeyFocus = KWin.readConfig("directionalKeyFocus" , false);
this.directionalKeyMode = (directionalKeyDwm) ? "dwm" : "focus";
this.newWindowAsMaster = KWin.readConfig("newWindowAsMaster" , false);
this.layoutPerActivity = KWin.readConfig("layoutPerActivity" , true);
this.layoutPerDesktop = KWin.readConfig("layoutPerDesktop" , true);
this.floatUtility = KWin.readConfig("floatUtility" , true);
this.preventMinimize = KWin.readConfig("preventMinimize" , false);
this.preventProtrusion = KWin.readConfig("preventProtrusion" , true);
this.pollMouseXdotool = KWin.readConfig("pollMouseXdotool" , false);
this.floatingClass = commaSeparate(KWin.readConfig("floatingClass" , ""));
this.floatingTitle = commaSeparate(KWin.readConfig("floatingTitle" , ""));
this.ignoreActivity = commaSeparate(KWin.readConfig("ignoreActivity", ""));
this.ignoreClass = commaSeparate(KWin.readConfig("ignoreClass" ,
"krunner,yakuake,spectacle,kded5"));
this.ignoreRole = commaSeparate(KWin.readConfig("ignoreRole" ,
"quake"));
this.ignoreScreen = commaSeparate(KWin.readConfig("ignoreScreen", ""))
.map((str) => parseInt(str, 10));
this.ignoreTitle = commaSeparate(KWin.readConfig("ignoreTitle" , ""));
if (this.preventMinimize && this.monocleMinimizeRest) {
debug(() => "preventMinimize is disabled because of monocleMinimizeRest.");
this.preventMinimize = false;
}
constructor() {
function commaSeparate(str: string): string[] {
if (!str || typeof str !== "string") return [];
return str.split(",").map((part) => part.trim());
}
public toString(): string {
return "Config(" + JSON.stringify(this, undefined, 2) + ")";
DEBUG.enabled = DEBUG.enabled || KWin.readConfig("debug", false);
this.layoutOrder = [];
this.layoutFactories = {};
(
[
["enableTileLayout", true, TileLayout],
["enableMonocleLayout", true, MonocleLayout],
["enableThreeColumnLayout", true, ThreeColumnLayout],
["enableSpreadLayout", true, SpreadLayout],
["enableStairLayout", true, StairLayout],
["enableSpiralLayout", true, SpiralLayout],
["enableQuarterLayout", false, QuarterLayout],
["enableFloatingLayout", false, FloatingLayout],
["enableCascadeLayout", false, CascadeLayout], // TODO: add config
] as Array<[string, boolean, ILayoutClass]>
).forEach(([configKey, defaultValue, layoutClass]) => {
if (KWin.readConfig(configKey, defaultValue))
this.layoutOrder.push(layoutClass.id);
this.layoutFactories[layoutClass.id] = () => new layoutClass();
});
this.maximizeSoleTile = KWin.readConfig("maximizeSoleTile", false);
this.monocleMaximize = KWin.readConfig("monocleMaximize", true);
this.monocleMinimizeRest = KWin.readConfig("monocleMinimizeRest", false);
this.adjustLayout = KWin.readConfig("adjustLayout", true);
this.adjustLayoutLive = KWin.readConfig("adjustLayoutLive", true);
this.keepFloatAbove = KWin.readConfig("keepFloatAbove", true);
this.noTileBorder = KWin.readConfig("noTileBorder", false);
this.limitTileWidthRatio = 0;
if (KWin.readConfig("limitTileWidth", false))
this.limitTileWidthRatio = KWin.readConfig("limitTileWidthRatio", 1.6);
this.screenGapBottom = KWin.readConfig("screenGapBottom", 0);
this.screenGapLeft = KWin.readConfig("screenGapLeft", 0);
this.screenGapRight = KWin.readConfig("screenGapRight", 0);
this.screenGapTop = KWin.readConfig("screenGapTop", 0);
this.tileLayoutGap = KWin.readConfig("tileLayoutGap", 0);
const directionalKeyDwm = KWin.readConfig("directionalKeyDwm", true);
const directionalKeyFocus = KWin.readConfig("directionalKeyFocus", false);
this.directionalKeyMode = directionalKeyDwm ? "dwm" : "focus";
this.newWindowAsMaster = KWin.readConfig("newWindowAsMaster", false);
this.layoutPerActivity = KWin.readConfig("layoutPerActivity", true);
this.layoutPerDesktop = KWin.readConfig("layoutPerDesktop", true);
this.floatUtility = KWin.readConfig("floatUtility", true);
this.preventMinimize = KWin.readConfig("preventMinimize", false);
this.preventProtrusion = KWin.readConfig("preventProtrusion", true);
this.pollMouseXdotool = KWin.readConfig("pollMouseXdotool", false);
this.floatingClass = commaSeparate(KWin.readConfig("floatingClass", ""));
this.floatingTitle = commaSeparate(KWin.readConfig("floatingTitle", ""));
this.ignoreActivity = commaSeparate(KWin.readConfig("ignoreActivity", ""));
this.ignoreClass = commaSeparate(
KWin.readConfig("ignoreClass", "krunner,yakuake,spectacle,kded5")
);
this.ignoreRole = commaSeparate(KWin.readConfig("ignoreRole", "quake"));
this.ignoreScreen = commaSeparate(KWin.readConfig("ignoreScreen", "")).map(
(str) => parseInt(str, 10)
);
this.ignoreTitle = commaSeparate(KWin.readConfig("ignoreTitle", ""));
if (this.preventMinimize && this.monocleMinimizeRest) {
debug(
() => "preventMinimize is disabled because of monocleMinimizeRest."
);
this.preventMinimize = false;
}
}
public toString(): string {
return "Config(" + JSON.stringify(this, undefined, 2) + ")";
}
}
/* HACK: save casting */

View File

@ -26,342 +26,378 @@
* functions.
*/
class KWinDriver implements IDriverContext {
public static backendName: string = "kwin";
public static backendName: string = "kwin";
// TODO: split context implementation
//#region implement properties of IDriverContext (except `setTimeout`)
public get backend(): string {
return KWinDriver.backendName;
// TODO: split context implementation
//#region implement properties of IDriverContext (except `setTimeout`)
public get backend(): string {
return KWinDriver.backendName;
}
public get currentSurface(): ISurface {
return new KWinSurface(
workspace.activeClient ? workspace.activeClient.screen : 0,
workspace.currentActivity,
workspace.currentDesktop
);
}
public set currentSurface(value: ISurface) {
const ksrf = value as KWinSurface;
/* NOTE: only supports switching desktops */
// TODO: fousing window on other screen?
// TODO: find a way to change activity
if (workspace.currentDesktop !== ksrf.desktop)
workspace.currentDesktop = ksrf.desktop;
}
public get currentWindow(): Window | null {
const client = workspace.activeClient;
return client ? this.windowMap.get(client) : null;
}
public set currentWindow(window: Window | null) {
if (window !== null)
workspace.activeClient = (window.window as KWinWindow).client;
}
public get screens(): ISurface[] {
const screens = [];
for (let screen = 0; screen < workspace.numScreens; screen++)
screens.push(
new KWinSurface(
screen,
workspace.currentActivity,
workspace.currentDesktop
)
);
return screens;
}
public get cursorPosition(): [number, number] | null {
return this.mousePoller.mousePosition;
}
//#endregion
private engine: TilingEngine;
private control: TilingController;
private windowMap: WrapperMap<KWin.Client, Window>;
private entered: boolean;
private mousePoller: KWinMousePoller;
constructor() {
this.engine = new TilingEngine();
this.control = new TilingController(this.engine);
this.windowMap = new WrapperMap(
(client: KWin.Client) => KWinWindow.generateID(client),
(client: KWin.Client) => new Window(new KWinWindow(client))
);
this.entered = false;
this.mousePoller = new KWinMousePoller();
}
/*
* Main
*/
public main() {
CONFIG = KWINCONFIG = new KWinConfig();
debug(() => "Config: " + KWINCONFIG);
this.bindEvents();
this.bindShortcut();
const clients = workspace.clientList();
for (let i = 0; i < clients.length; i++) {
const window = this.windowMap.add(clients[i]);
this.engine.manage(window);
if (window.state !== WindowState.Unmanaged)
this.bindWindowEvents(window, clients[i]);
else this.windowMap.remove(clients[i]);
}
this.engine.arrange(this);
}
//#region implement methods of IDriverContext`
public setTimeout(func: () => void, timeout: number) {
KWinSetTimeout(() => this.enter(func), timeout);
}
public showNotification(text: string) {
popupDialog.show(text);
}
//#endregion
private bindShortcut() {
if (!KWin.registerShortcut) {
debug(
() => "KWin.registerShortcut doesn't exist. Omitting shortcut binding."
);
return;
}
public get currentSurface(): ISurface {
return new KWinSurface(
(workspace.activeClient) ? workspace.activeClient.screen : 0,
workspace.currentActivity,
workspace.currentDesktop,
const bind = (seq: string, title: string, input: Shortcut) => {
title = "Krohnkite: " + title;
seq = "Meta+" + seq;
KWin.registerShortcut(title, "", seq, () => {
this.enter(() => this.control.onShortcut(this, input));
});
};
bind("J", "Down/Next", Shortcut.Down);
bind("K", "Up/Prev", Shortcut.Up);
bind("H", "Left", Shortcut.Left);
bind("L", "Right", Shortcut.Right);
bind("Shift+J", "Move Down/Next", Shortcut.ShiftDown);
bind("Shift+K", "Move Up/Prev", Shortcut.ShiftUp);
bind("Shift+H", "Move Left", Shortcut.ShiftLeft);
bind("Shift+L", "Move Right", Shortcut.ShiftRight);
bind("Ctrl+J", "Grow Height", Shortcut.GrowHeight);
bind("Ctrl+K", "Shrink Height", Shortcut.ShrinkHeight);
bind("Ctrl+H", "Shrink Width", Shortcut.ShrinkWidth);
bind("Ctrl+L", "Grow Width", Shortcut.GrowWidth);
bind("I", "Increase", Shortcut.Increase);
bind("D", "Decrease", Shortcut.Decrease);
bind("F", "Float", Shortcut.ToggleFloat);
bind("Shift+F", "Float All", Shortcut.ToggleFloatAll);
bind("", "Cycle Layout", Shortcut.NextLayout); // TODO: remove this shortcut
bind("\\", "Next Layout", Shortcut.NextLayout);
bind("|", "Previous Layout", Shortcut.PreviousLayout);
bind("R", "Rotate", Shortcut.Rotate);
bind("Shift+R", "Rotate Part", Shortcut.RotatePart);
bind("Return", "Set master", Shortcut.SetMaster);
const bindLayout = (
seq: string,
title: string,
layoutClass: ILayoutClass
) => {
title = "Krohnkite: " + title + " Layout";
seq = seq !== "" ? "Meta+" + seq : "";
KWin.registerShortcut(title, "", seq, () => {
this.enter(() =>
this.control.onShortcut(this, Shortcut.SetLayout, layoutClass.id)
);
});
};
bindLayout("T", "Tile", TileLayout);
bindLayout("M", "Monocle", MonocleLayout);
bindLayout("", "Three Column", ThreeColumnLayout);
bindLayout("", "Spread", SpreadLayout);
bindLayout("", "Stair", StairLayout);
bindLayout("", "Floating", FloatingLayout);
bindLayout("", "Quarter", QuarterLayout);
}
//#region Helper functions
/**
* Binds callback to the signal w/ extra fail-safe measures, like re-entry
* prevention and auto-disconnect on termination.
*/
private connect(signal: QSignal, handler: (..._: any[]) => void): () => void {
const wrapper = (...args: any[]) => {
/* HACK: `workspace` become undefined when the script is disabled. */
if (typeof workspace === "undefined") signal.disconnect(wrapper);
else this.enter(() => handler.apply(this, args));
};
signal.connect(wrapper);
return wrapper;
}
/**
* Run the given function in a protected(?) context to prevent nested event
* handling.
*
* KWin emits signals as soons as window states are changed, even when
* those states are modified by the script. This causes multiple re-entry
* during event handling, resulting in performance degradation and harder
* debugging.
*/
private enter(callback: () => void) {
if (this.entered) return;
this.entered = true;
try {
callback();
} catch (e) {
debug(() => "Error raised from line " + e.lineNumber);
debug(() => e);
} finally {
this.entered = false;
}
}
//#endregion
public set currentSurface(value: ISurface) {
const ksrf = value as KWinSurface;
private bindEvents() {
this.connect(workspace.numberScreensChanged, (count: number) =>
this.control.onSurfaceUpdate(this, "screens=" + count)
);
/* NOTE: only supports switching desktops */
// TODO: fousing window on other screen?
// TODO: find a way to change activity
this.connect(workspace.screenResized, (screen: number) => {
const srf = new KWinSurface(
screen,
workspace.currentActivity,
workspace.currentDesktop
);
this.control.onSurfaceUpdate(this, "resized " + srf.toString());
});
if (workspace.currentDesktop !== ksrf.desktop)
workspace.currentDesktop = ksrf.desktop;
}
this.connect(workspace.currentActivityChanged, (activity: string) =>
this.control.onCurrentSurfaceChanged(this)
);
public get currentWindow(): Window | null {
const client = workspace.activeClient;
return (client) ? this.windowMap.get(client) : null;
}
this.connect(
workspace.currentDesktopChanged,
(desktop: number, client: KWin.Client) =>
this.control.onCurrentSurfaceChanged(this)
);
public set currentWindow(window: Window | null) {
if (window !== null)
workspace.activeClient = (window.window as KWinWindow).client;
}
this.connect(workspace.clientAdded, (client: KWin.Client) => {
/* NOTE: windowShown can be fired in various situations.
* We need only the first one - when window is created. */
let handled = false;
const handler = () => {
if (handled) return;
handled = true;
public get screens(): ISurface[] {
const screens = [];
for (let screen = 0; screen < workspace.numScreens; screen++)
screens.push(new KWinSurface(
screen, workspace.currentActivity, workspace.currentDesktop));
return screens;
}
const window = this.windowMap.add(client);
this.control.onWindowAdded(this, window);
if (window.state !== WindowState.Unmanaged)
this.bindWindowEvents(window, client);
else this.windowMap.remove(client);
public get cursorPosition(): [number, number] | null {
return this.mousePoller.mousePosition;
}
client.windowShown.disconnect(wrapper);
};
//#endregion
const wrapper = this.connect(client.windowShown, handler);
this.setTimeout(handler, 50);
});
private engine: TilingEngine;
private control: TilingController;
private windowMap: WrapperMap<KWin.Client, Window>;
private entered: boolean;
private mousePoller: KWinMousePoller;
this.connect(workspace.clientRemoved, (client: KWin.Client) => {
const window = this.windowMap.get(client);
if (window) {
this.control.onWindowRemoved(this, window);
this.windowMap.remove(client);
}
});
constructor() {
this.engine = new TilingEngine();
this.control = new TilingController(this.engine);
this.windowMap = new WrapperMap(
(client: KWin.Client) => KWinWindow.generateID(client),
(client: KWin.Client) => new Window(new KWinWindow(client)),
this.connect(
workspace.clientMaximizeSet,
(client: KWin.Client, h: boolean, v: boolean) => {
const maximized = h === true && v === true;
const window = this.windowMap.get(client);
if (window) {
(window.window as KWinWindow).maximized = maximized;
this.control.onWindowMaximizeChanged(this, window, maximized);
}
}
);
this.connect(
workspace.clientFullScreenSet,
(client: KWin.Client, fullScreen: boolean, user: boolean) =>
this.control.onWindowChanged(
this,
this.windowMap.get(client),
"fullscreen=" + fullScreen + " user=" + user
)
);
this.connect(workspace.clientMinimized, (client: KWin.Client) => {
if (KWINCONFIG.preventMinimize) {
client.minimized = false;
workspace.activeClient = client;
} else
this.control.onWindowChanged(
this,
this.windowMap.get(client),
"minimized"
);
this.entered = false;
this.mousePoller = new KWinMousePoller();
}
});
/*
* Main
*/
this.connect(workspace.clientUnminimized, (client: KWin.Client) =>
this.control.onWindowChanged(
this,
this.windowMap.get(client),
"unminimized"
)
);
public main() {
CONFIG = KWINCONFIG = new KWinConfig();
debug(() => "Config: " + KWINCONFIG);
// TODO: options.configChanged.connect(this.onConfigChanged);
/* NOTE: How disappointing. This doesn't work at all. Even an official kwin script tries this.
* https://github.com/KDE/kwin/blob/master/scripts/minimizeall/contents/code/main.js */
}
this.bindEvents();
this.bindShortcut();
private bindWindowEvents(window: Window, client: KWin.Client) {
let moving = false;
let resizing = false;
const clients = workspace.clientList();
for (let i = 0; i < clients.length; i++) {
const window = this.windowMap.add(clients[i]);
this.engine.manage(window);
if (window.state !== WindowState.Unmanaged)
this.bindWindowEvents(window, clients[i]);
else
this.windowMap.remove(clients[i]);
this.connect(client.moveResizedChanged, () => {
debugObj(() => [
"moveResizedChanged",
{ window, move: client.move, resize: client.resize },
]);
if (moving !== client.move) {
moving = client.move;
if (moving) {
this.mousePoller.start();
this.control.onWindowMoveStart(window);
} else {
this.control.onWindowMoveOver(this, window);
this.mousePoller.stop();
}
this.engine.arrange(this);
}
}
if (resizing !== client.resize) {
resizing = client.resize;
if (resizing) this.control.onWindowResizeStart(window);
else this.control.onWindowResizeOver(this, window);
}
});
//#region implement methods of IDriverContext`
public setTimeout(func: () => void, timeout: number) {
KWinSetTimeout(() => this.enter(func), timeout);
}
this.connect(client.geometryChanged, () => {
if (moving) this.control.onWindowMove(window);
else if (resizing) this.control.onWindowResize(this, window);
else {
if (!window.actualGeometry.equals(window.geometry))
this.control.onWindowGeometryChanged(this, window);
}
});
public showNotification(text: string) {
popupDialog.show(text);
}
//#endregion
this.connect(client.activeChanged, () => {
if (client.active) this.control.onWindowFocused(this, window);
});
private bindShortcut() {
if (!KWin.registerShortcut) {
debug(() => "KWin.registerShortcut doesn't exist. Omitting shortcut binding.");
return;
}
this.connect(client.screenChanged, () =>
this.control.onWindowChanged(this, window, "screen=" + client.screen)
);
const bind = (seq: string, title: string, input: Shortcut) => {
title = "Krohnkite: " + title;
seq = "Meta+" + seq;
KWin.registerShortcut(title, "", seq, () => {
this.enter(() =>
this.control.onShortcut(this, input));
});
};
this.connect(client.activitiesChanged, () =>
this.control.onWindowChanged(
this,
window,
"activity=" + client.activities.join(",")
)
);
bind("J", "Down/Next", Shortcut.Down);
bind("K", "Up/Prev" , Shortcut.Up);
bind("H", "Left" , Shortcut.Left);
bind("L", "Right" , Shortcut.Right);
this.connect(client.desktopChanged, () =>
this.control.onWindowChanged(this, window, "desktop=" + client.desktop)
);
}
bind("Shift+J", "Move Down/Next", Shortcut.ShiftDown);
bind("Shift+K", "Move Up/Prev" , Shortcut.ShiftUp);
bind("Shift+H", "Move Left" , Shortcut.ShiftLeft);
bind("Shift+L", "Move Right" , Shortcut.ShiftRight);
bind("Ctrl+J", "Grow Height" , Shortcut.GrowHeight);
bind("Ctrl+K", "Shrink Height" , Shortcut.ShrinkHeight);
bind("Ctrl+H", "Shrink Width" , Shortcut.ShrinkWidth);
bind("Ctrl+L", "Grow Width" , Shortcut.GrowWidth);
bind("I", "Increase", Shortcut.Increase);
bind("D", "Decrease", Shortcut.Decrease);
bind("F", "Float", Shortcut.ToggleFloat);
bind("Shift+F", "Float All", Shortcut.ToggleFloatAll);
bind("", "Cycle Layout", Shortcut.NextLayout); // TODO: remove this shortcut
bind("\\", "Next Layout", Shortcut.NextLayout);
bind("|", "Previous Layout", Shortcut.PreviousLayout);
bind("R", "Rotate", Shortcut.Rotate);
bind("Shift+R", "Rotate Part", Shortcut.RotatePart);
bind("Return", "Set master", Shortcut.SetMaster);
const bindLayout = (seq: string, title: string, layoutClass: ILayoutClass) => {
title = "Krohnkite: " + title + " Layout";
seq = (seq !== "") ? "Meta+" + seq : "";
KWin.registerShortcut(title, "", seq, () => {
this.enter(() =>
this.control.onShortcut(this, Shortcut.SetLayout, layoutClass.id));
});
};
bindLayout("T", "Tile", TileLayout);
bindLayout("M", "Monocle", MonocleLayout);
bindLayout("", "Three Column", ThreeColumnLayout);
bindLayout("", "Spread", SpreadLayout);
bindLayout("", "Stair", StairLayout);
bindLayout("", "Floating", FloatingLayout);
bindLayout("", "Quarter", QuarterLayout);
}
//#region Helper functions
/**
* Binds callback to the signal w/ extra fail-safe measures, like re-entry
* prevention and auto-disconnect on termination.
*/
private connect(signal: QSignal, handler: (..._: any[]) => void): (() => void) {
const wrapper = (...args: any[]) => {
/* HACK: `workspace` become undefined when the script is disabled. */
if (typeof workspace === "undefined")
signal.disconnect(wrapper);
else
this.enter(() => handler.apply(this, args));
};
signal.connect(wrapper);
return wrapper;
}
/**
* Run the given function in a protected(?) context to prevent nested event
* handling.
*
* KWin emits signals as soons as window states are changed, even when
* those states are modified by the script. This causes multiple re-entry
* during event handling, resulting in performance degradation and harder
* debugging.
*/
private enter(callback: () => void) {
if (this.entered)
return;
this.entered = true;
try {
callback();
} catch (e) {
debug(() => "Error raised from line " + e.lineNumber);
debug(() => e);
} finally {
this.entered = false;
}
}
//#endregion
private bindEvents() {
this.connect(workspace.numberScreensChanged, (count: number) =>
this.control.onSurfaceUpdate(this, "screens=" + count));
this.connect(workspace.screenResized, (screen: number) => {
const srf = new KWinSurface(
screen, workspace.currentActivity, workspace.currentDesktop);
this.control.onSurfaceUpdate(this, "resized " + srf.toString());
});
this.connect(workspace.currentActivityChanged, (activity: string) =>
this.control.onCurrentSurfaceChanged(this));
this.connect(workspace.currentDesktopChanged, (desktop: number, client: KWin.Client) =>
this.control.onCurrentSurfaceChanged(this));
this.connect(workspace.clientAdded, (client: KWin.Client) => {
/* NOTE: windowShown can be fired in various situations.
* We need only the first one - when window is created. */
let handled = false;
const handler = () => {
if (handled)
return;
handled = true;
const window = this.windowMap.add(client);
this.control.onWindowAdded(this, window);
if (window.state !== WindowState.Unmanaged)
this.bindWindowEvents(window, client);
else
this.windowMap.remove(client);
client.windowShown.disconnect(wrapper);
};
const wrapper = this.connect(client.windowShown, handler);
this.setTimeout(handler, 50);
});
this.connect(workspace.clientRemoved, (client: KWin.Client) => {
const window = this.windowMap.get(client);
if (window) {
this.control.onWindowRemoved(this, window);
this.windowMap.remove(client);
}
});
this.connect(workspace.clientMaximizeSet, (client: KWin.Client, h: boolean, v: boolean) => {
const maximized = (h === true && v === true);
const window = this.windowMap.get(client);
if (window) {
(window.window as KWinWindow).maximized = maximized;
this.control.onWindowMaximizeChanged(this, window, maximized);
}
});
this.connect(workspace.clientFullScreenSet, (client: KWin.Client, fullScreen: boolean, user: boolean) =>
this.control.onWindowChanged(this, this.windowMap.get(client),
"fullscreen=" + fullScreen + " user=" + user));
this.connect(workspace.clientMinimized, (client: KWin.Client) => {
if (KWINCONFIG.preventMinimize) {
client.minimized = false;
workspace.activeClient = client;
} else
this.control.onWindowChanged(this, this.windowMap.get(client), "minimized");
});
this.connect(workspace.clientUnminimized, (client: KWin.Client) =>
this.control.onWindowChanged(this, this.windowMap.get(client), "unminimized"));
// TODO: options.configChanged.connect(this.onConfigChanged);
/* NOTE: How disappointing. This doesn't work at all. Even an official kwin script tries this.
* https://github.com/KDE/kwin/blob/master/scripts/minimizeall/contents/code/main.js */
}
private bindWindowEvents(window: Window, client: KWin.Client) {
let moving = false;
let resizing = false;
this.connect(client.moveResizedChanged, () => {
debugObj(() => ["moveResizedChanged", {window, move: client.move, resize: client.resize}]);
if (moving !== client.move) {
moving = client.move;
if (moving) {
this.mousePoller.start();
this.control.onWindowMoveStart(window);
} else {
this.control.onWindowMoveOver(this, window);
this.mousePoller.stop();
}
}
if (resizing !== client.resize) {
resizing = client.resize;
if (resizing)
this.control.onWindowResizeStart(window);
else
this.control.onWindowResizeOver(this, window);
}
});
this.connect(client.geometryChanged, () => {
if (moving)
this.control.onWindowMove(window);
else if (resizing)
this.control.onWindowResize(this, window);
else {
if (!window.actualGeometry.equals(window.geometry))
this.control.onWindowGeometryChanged(this, window);
}
});
this.connect(client.activeChanged, () => {
if (client.active)
this.control.onWindowFocused(this, window);
});
this.connect(client.screenChanged, () =>
this.control.onWindowChanged(this, window, "screen=" + client.screen));
this.connect(client.activitiesChanged, () =>
this.control.onWindowChanged(this, window, "activity=" + client.activities.join(",")));
this.connect(client.desktopChanged, () =>
this.control.onWindowChanged(this, window, "desktop=" + client.desktop));
}
// TODO: private onConfigChanged = () => {
// this.loadConfig();
// this.engine.arrange();
// }
/* NOTE: check `bindEvents` for details */
// TODO: private onConfigChanged = () => {
// this.loadConfig();
// this.engine.arrange();
// }
/* NOTE: check `bindEvents` for details */
}

View File

@ -19,69 +19,66 @@
// DEALINGS IN THE SOFTWARE.
class KWinMousePoller {
public static readonly COMMAND = "xdotool getmouselocation";
public static readonly INTERVAL = 50; /* ms */
public static readonly COMMAND = "xdotool getmouselocation";
public static readonly INTERVAL = 50; /* ms */
public get started(): boolean {
return this.startCount > 0;
}
public get started(): boolean {
return this.startCount > 0;
}
public get mousePosition(): [number, number] | null {
return this.parseResult();
}
public get mousePosition(): [number, number] | null {
return this.parseResult();
}
/** poller activates only when count > 0 */
private startCount: number;
private cmdResult: string | null;
/** poller activates only when count > 0 */
private startCount: number;
private cmdResult: string | null;
constructor() {
this.startCount = 0;
this.cmdResult = null;
constructor() {
this.startCount = 0;
this.cmdResult = null;
/* we will poll manually, because this interval value will be
* aligned to intervalAlignment, which probably is 1000. */
mousePoller.interval = 0;
/* we will poll manually, because this interval value will be
* aligned to intervalAlignment, which probably is 1000. */
mousePoller.interval = 0;
mousePoller.onNewData.connect((sourceName: string, data: any) => {
// tslint:disable-next-line:no-string-literal
this.cmdResult = (data["exit code"] === 0) ? data["stdout"] : null;
mousePoller.disconnectSource(KWinMousePoller.COMMAND);
mousePoller.onNewData.connect((sourceName: string, data: any) => {
// tslint:disable-next-line:no-string-literal
this.cmdResult = data["exit code"] === 0 ? data["stdout"] : null;
mousePoller.disconnectSource(KWinMousePoller.COMMAND);
KWinSetTimeout(() => {
if (this.started)
mousePoller.connectSource(KWinMousePoller.COMMAND);
}, KWinMousePoller.INTERVAL);
});
}
KWinSetTimeout(() => {
if (this.started) mousePoller.connectSource(KWinMousePoller.COMMAND);
}, KWinMousePoller.INTERVAL);
});
}
public start() {
this.startCount += 1;
if (KWINCONFIG.pollMouseXdotool)
mousePoller.connectSource(KWinMousePoller.COMMAND);
}
public start() {
this.startCount += 1;
if (KWINCONFIG.pollMouseXdotool)
mousePoller.connectSource(KWinMousePoller.COMMAND);
}
public stop() {
this.startCount = Math.max(this.startCount - 1, 0);
}
public stop() {
this.startCount = Math.max(this.startCount - 1, 0);
}
private parseResult(): [number, number] | null {
// output example: x:1031 y:515 screen:0 window:90177537
if (!this.cmdResult)
return null;
private parseResult(): [number, number] | null {
// output example: x:1031 y:515 screen:0 window:90177537
if (!this.cmdResult) return null;
let x: number | null = null;
let y: number | null = null;
this.cmdResult
.split(" ")
.slice(0, 2)
.forEach((part) => {
const [key, value, _] = part.split(":");
if (key === "x") x = parseInt(value, 10);
if (key === "y") y = parseInt(value, 10);
});
let x: number | null = null;
let y: number | null = null;
this.cmdResult
.split(" ")
.slice(0, 2)
.forEach((part) => {
const [key, value, _] = part.split(":");
if (key === "x") x = parseInt(value, 10);
if (key === "y") y = parseInt(value, 10);
});
if (x === null || y === null)
return null;
return [x, y];
}
if (x === null || y === null) return null;
return [x, y];
}
}

View File

@ -19,38 +19,47 @@
// DEALINGS IN THE SOFTWARE.
class KWinTimerPool {
public static readonly instance = new KWinTimerPool();
public static readonly instance = new KWinTimerPool();
public timers: QQmlTimer[];
public numTimers: number;
public timers: QQmlTimer[];
public numTimers: number;
constructor() {
this.timers = [];
this.numTimers = 0;
constructor() {
this.timers = [];
this.numTimers = 0;
}
public setTimeout(func: () => void, timeout: number) {
if (this.timers.length === 0) {
this.numTimers++;
debugObj(() => ["setTimeout/newTimer", { numTimers: this.numTimers }]);
}
public setTimeout(func: () => void, timeout: number) {
if (this.timers.length === 0) {
this.numTimers ++;
debugObj(() => ["setTimeout/newTimer", { numTimers: this.numTimers}]);
}
const timer: QQmlTimer =
this.timers.pop() ||
Qt.createQmlObject("import QtQuick 2.0; Timer {}", scriptRoot);
const timer: QQmlTimer = this.timers.pop() ||
Qt.createQmlObject("import QtQuick 2.0; Timer {}", scriptRoot);
const callback = () => {
try {
timer.triggered.disconnect(callback);
} catch (e) {
/* ignore */
}
try {
func();
} catch (e) {
/* ignore */
}
this.timers.push(timer);
};
const callback = () => {
try { timer.triggered.disconnect(callback); } catch (e) { /* ignore */ }
try { func(); } catch (e) { /* ignore */ }
this.timers.push(timer);
};
timer.interval = timeout;
timer.repeat = false;
timer.triggered.connect(callback);
timer.start();
}
timer.interval = timeout;
timer.repeat = false;
timer.triggered.connect(callback);
timer.start();
}
}
function KWinSetTimeout(func: () => void, timeout: number) {
KWinTimerPool.instance.setTimeout(func, timeout);
KWinTimerPool.instance.setTimeout(func, timeout);
}

View File

@ -19,49 +19,55 @@
// DEALINGS IN THE SOFTWARE.
class KWinSurface implements ISurface {
public static generateId(screen: number, activity: string, desktop: number) {
let path = String(screen);
if (KWINCONFIG.layoutPerActivity)
path += "@" + activity;
if (KWINCONFIG.layoutPerDesktop)
path += "#" + desktop;
return path;
}
public static generateId(screen: number, activity: string, desktop: number) {
let path = String(screen);
if (KWINCONFIG.layoutPerActivity) path += "@" + activity;
if (KWINCONFIG.layoutPerDesktop) path += "#" + desktop;
return path;
}
public readonly id: string;
public readonly ignore: boolean;
public readonly workingArea: Rect;
public readonly id: string;
public readonly ignore: boolean;
public readonly workingArea: Rect;
public readonly screen: number;
public readonly activity: string;
public readonly desktop: number;
public readonly screen: number;
public readonly activity: string;
public readonly desktop: number;
constructor(screen: number, activity: string, desktop: number) {
const activityName = activityInfo.activityName(activity);
constructor(screen: number, activity: string, desktop: number) {
const activityName = activityInfo.activityName(activity);
this.id = KWinSurface.generateId(screen, activity, desktop);
this.ignore = (
(KWINCONFIG.ignoreActivity.indexOf(activityName) >= 0)
|| (KWINCONFIG.ignoreScreen.indexOf(screen) >= 0)
);
this.workingArea = toRect(
workspace.clientArea(KWin.PlacementArea, screen, desktop));
this.id = KWinSurface.generateId(screen, activity, desktop);
this.ignore =
KWINCONFIG.ignoreActivity.indexOf(activityName) >= 0 ||
KWINCONFIG.ignoreScreen.indexOf(screen) >= 0;
this.workingArea = toRect(
workspace.clientArea(KWin.PlacementArea, screen, desktop)
);
this.screen = screen;
this.activity = activity;
this.desktop = desktop;
}
this.screen = screen;
this.activity = activity;
this.desktop = desktop;
}
public next(): ISurface | null {
if (this.desktop === workspace.desktops)
/* this is the last virtual desktop */
/* TODO: option to create additional desktop */
return null;
public next(): ISurface | null {
if (this.desktop === workspace.desktops)
/* this is the last virtual desktop */
/* TODO: option to create additional desktop */
return null;
return new KWinSurface(this.screen, this.activity, this.desktop + 1);
}
return new KWinSurface(this.screen, this.activity, this.desktop + 1);
}
public toString(): string {
return "KWinSurface(" + [this.screen, activityInfo.activityName(this.activity), this.desktop].join(", ") + ")";
}
public toString(): string {
return (
"KWinSurface(" +
[
this.screen,
activityInfo.activityName(this.activity),
this.desktop,
].join(", ") +
")"
);
}
}

View File

@ -19,196 +19,218 @@
// DEALINGS IN THE SOFTWARE.
class KWinWindow implements IDriverWindow {
public static generateID(client: KWin.Client) {
return String(client) + "/" + client.windowId;
public static generateID(client: KWin.Client) {
return String(client) + "/" + client.windowId;
}
public readonly client: KWin.Client;
public readonly id: string;
public get fullScreen(): boolean {
return this.client.fullScreen;
}
public get geometry(): Rect {
return toRect(this.client.geometry);
}
public get shouldIgnore(): boolean {
const resourceClass = String(this.client.resourceClass);
const resourceName = String(this.client.resourceName);
const windowRole = String(this.client.windowRole);
return (
this.client.specialWindow ||
resourceClass === "plasmashell" ||
KWINCONFIG.ignoreClass.indexOf(resourceClass) >= 0 ||
KWINCONFIG.ignoreClass.indexOf(resourceName) >= 0 ||
matchWords(this.client.caption, KWINCONFIG.ignoreTitle) >= 0 ||
KWINCONFIG.ignoreRole.indexOf(windowRole) >= 0
);
}
public get shouldFloat(): boolean {
const resourceClass = String(this.client.resourceClass);
const resourceName = String(this.client.resourceName);
return (
this.client.modal ||
!this.client.resizeable ||
(KWINCONFIG.floatUtility &&
(this.client.dialog || this.client.splash || this.client.utility)) ||
KWINCONFIG.floatingClass.indexOf(resourceClass) >= 0 ||
KWINCONFIG.floatingClass.indexOf(resourceName) >= 0 ||
matchWords(this.client.caption, KWINCONFIG.floatingTitle) >= 0
);
}
public maximized: boolean;
public get surface(): ISurface {
let activity;
if (this.client.activities.length === 0)
activity = workspace.currentActivity;
else if (this.client.activities.indexOf(workspace.currentActivity) >= 0)
activity = workspace.currentActivity;
else activity = this.client.activities[0];
const desktop =
this.client.desktop >= 0 ? this.client.desktop : workspace.currentDesktop;
return new KWinSurface(this.client.screen, activity, desktop);
}
public set surface(srf: ISurface) {
const ksrf = srf as KWinSurface;
// TODO: setting activity?
// TODO: setting screen = move to the screen
if (this.client.desktop !== ksrf.desktop)
this.client.desktop = ksrf.desktop;
}
private noBorderManaged: boolean;
private noBorderOriginal: boolean;
constructor(client: KWin.Client) {
this.client = client;
this.id = KWinWindow.generateID(client);
this.maximized = false;
this.noBorderManaged = false;
this.noBorderOriginal = client.noBorder;
}
public commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean) {
debugObj(() => ["KWinWindow#commit", { geometry, noBorder, keepAbove }]);
if (this.client.move || this.client.resize) return;
if (noBorder !== undefined) {
if (!this.noBorderManaged && noBorder)
/* Backup border state when transitioning from unmanaged to managed */
this.noBorderOriginal = this.client.noBorder;
else if (this.noBorderManaged && !this.client.noBorder)
/* If border is enabled while in managed mode, remember it.
* Note that there's no way to know if border is re-disabled in managed mode. */
this.noBorderOriginal = false;
if (noBorder)
/* (Re)entering managed mode: remove border. */
this.client.noBorder = true;
else if (this.noBorderManaged)
/* Exiting managed mode: restore original value. */
this.client.noBorder = this.noBorderOriginal;
/* update mode */
this.noBorderManaged = noBorder;
}
public readonly client: KWin.Client;
public readonly id: string;
if (keepAbove !== undefined) this.client.keepAbove = keepAbove;
public get fullScreen(): boolean {
return this.client.fullScreen;
}
public get geometry(): Rect {
return toRect(this.client.geometry);
}
public get shouldIgnore(): boolean {
const resourceClass = String(this.client.resourceClass);
const resourceName = String(this.client.resourceName);
const windowRole = String(this.client.windowRole);
return (
this.client.specialWindow
|| resourceClass === "plasmashell"
|| (KWINCONFIG.ignoreClass.indexOf(resourceClass) >= 0)
|| (KWINCONFIG.ignoreClass.indexOf(resourceName) >= 0)
|| (matchWords(this.client.caption, KWINCONFIG.ignoreTitle) >= 0)
|| (KWINCONFIG.ignoreRole.indexOf(windowRole) >= 0)
if (geometry !== undefined) {
geometry = this.adjustGeometry(geometry);
if (KWINCONFIG.preventProtrusion) {
const area = toRect(
workspace.clientArea(
KWin.PlacementArea,
this.client.screen,
workspace.currentDesktop
)
);
}
public get shouldFloat(): boolean {
const resourceClass = String(this.client.resourceClass);
const resourceName = String(this.client.resourceName);
return (
this.client.modal
|| (!this.client.resizeable)
|| (KWINCONFIG.floatUtility
&& (this.client.dialog || this.client.splash || this.client.utility))
|| (KWINCONFIG.floatingClass.indexOf(resourceClass) >= 0)
|| (KWINCONFIG.floatingClass.indexOf(resourceName) >= 0)
|| (matchWords(this.client.caption, KWINCONFIG.floatingTitle) >= 0)
);
}
public maximized: boolean;
public get surface(): ISurface {
let activity;
if (this.client.activities.length === 0)
activity = workspace.currentActivity;
else if (this.client.activities.indexOf(workspace.currentActivity) >= 0)
activity = workspace.currentActivity;
else
activity = this.client.activities[0];
const desktop = (this.client.desktop >= 0)
? this.client.desktop
: workspace.currentDesktop;
return new KWinSurface(this.client.screen, activity, desktop);
}
public set surface(srf: ISurface) {
const ksrf = srf as KWinSurface;
// TODO: setting activity?
// TODO: setting screen = move to the screen
if (this.client.desktop !== ksrf.desktop)
this.client.desktop = ksrf.desktop;
}
private noBorderManaged: boolean;
private noBorderOriginal: boolean;
constructor(client: KWin.Client) {
this.client = client;
this.id = KWinWindow.generateID(client);
this.maximized = false;
this.noBorderManaged = false;
this.noBorderOriginal = client.noBorder;
}
public commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean) {
debugObj(() => ["KWinWindow#commit", { geometry, noBorder, keepAbove }]);
if (this.client.move || this.client.resize)
return;
if (noBorder !== undefined) {
if (!this.noBorderManaged && noBorder)
/* Backup border state when transitioning from unmanaged to managed */
this.noBorderOriginal = this.client.noBorder;
else if (this.noBorderManaged && !this.client.noBorder)
/* If border is enabled while in managed mode, remember it.
* Note that there's no way to know if border is re-disabled in managed mode. */
this.noBorderOriginal = false;
if (noBorder)
/* (Re)entering managed mode: remove border. */
this.client.noBorder = true;
else if (this.noBorderManaged)
/* Exiting managed mode: restore original value. */
this.client.noBorder = this.noBorderOriginal;
/* update mode */
this.noBorderManaged = noBorder;
if (!area.includes(geometry)) {
/* assume windows will extrude only through right and bottom edges */
const x = geometry.x + Math.min(area.maxX - geometry.maxX, 0);
const y = geometry.y + Math.min(area.maxY - geometry.maxY, 0);
geometry = new Rect(x, y, geometry.width, geometry.height);
geometry = this.adjustGeometry(geometry);
}
}
this.client.geometry = toQRect(geometry);
}
}
if (keepAbove !== undefined)
this.client.keepAbove = keepAbove;
public toString(): string {
/* using a shorthand name to keep debug message tidy */
return (
"KWin(" +
this.client.windowId.toString(16) +
"." +
this.client.resourceClass +
")"
);
}
if (geometry !== undefined) {
geometry = this.adjustGeometry(geometry);
if (KWINCONFIG.preventProtrusion) {
const area = toRect(
workspace.clientArea(KWin.PlacementArea, this.client.screen, workspace.currentDesktop));
if (!area.includes(geometry)) {
/* assume windows will extrude only through right and bottom edges */
const x = geometry.x + Math.min(area.maxX - geometry.maxX, 0);
const y = geometry.y + Math.min(area.maxY - geometry.maxY, 0);
geometry = new Rect(x, y, geometry.width, geometry.height);
geometry = this.adjustGeometry(geometry);
}
}
this.client.geometry = toQRect(geometry);
}
public visible(srf: ISurface): boolean {
const ksrf = srf as KWinSurface;
return (
!this.client.minimized &&
(this.client.desktop === ksrf.desktop ||
this.client.desktop === -1) /* on all desktop */ &&
(this.client.activities.length === 0 /* on all activities */ ||
this.client.activities.indexOf(ksrf.activity) !== -1) &&
this.client.screen === ksrf.screen
);
}
//#region Private Methods
/** apply various resize hints to the given geometry */
private adjustGeometry(geometry: Rect): Rect {
let width = geometry.width;
let height = geometry.height;
/* do not resize fixed-size windows */
if (!this.client.resizeable) {
width = this.client.geometry.width;
height = this.client.geometry.height;
} else {
/* respect resize increment */
if (
!(
this.client.basicUnit.width === 1 &&
this.client.basicUnit.height === 1
)
)
/* NOT free-size */
[width, height] = this.applyResizeIncrement(geometry);
/* respect min/max size limit */
width = clip(width, this.client.minSize.width, this.client.maxSize.width);
height = clip(
height,
this.client.minSize.height,
this.client.maxSize.height
);
}
public toString(): string {
/* using a shorthand name to keep debug message tidy */
return "KWin(" + this.client.windowId.toString(16) + "." + this.client.resourceClass + ")";
}
return new Rect(geometry.x, geometry.y, width, height);
}
public visible(srf: ISurface): boolean {
const ksrf = srf as KWinSurface;
return (
(!this.client.minimized)
&& (this.client.desktop === ksrf.desktop
|| this.client.desktop === -1 /* on all desktop */)
&& (this.client.activities.length === 0 /* on all activities */
|| this.client.activities.indexOf(ksrf.activity) !== -1)
&& (this.client.screen === ksrf.screen)
);
}
private applyResizeIncrement(geom: Rect): [number, number] {
const unit = this.client.basicUnit;
const base = this.client.minSize;
//#region Private Methods
const padWidth = this.client.geometry.width - this.client.clientSize.width;
const padHeight =
this.client.geometry.height - this.client.clientSize.height;
/** apply various resize hints to the given geometry */
private adjustGeometry(geometry: Rect): Rect {
let width = geometry.width;
let height = geometry.height;
const quotWidth = Math.floor(
(geom.width - base.width - padWidth) / unit.width
);
const quotHeight = Math.floor(
(geom.height - base.height - padHeight) / unit.height
);
/* do not resize fixed-size windows */
if (!this.client.resizeable) {
width = this.client.geometry.width;
height = this.client.geometry.height;
} else {
/* respect resize increment */
if (!(this.client.basicUnit.width === 1 && this.client.basicUnit.height === 1)) /* NOT free-size */
[width, height] = this.applyResizeIncrement(geometry);
const newWidth = base.width + unit.width * quotWidth + padWidth;
const newHeight = base.height + unit.height * quotHeight + padHeight;
/* respect min/max size limit */
width = clip(width , this.client.minSize.width , this.client.maxSize.width );
height = clip(height, this.client.minSize.height, this.client.maxSize.height);
}
// debugObj(() => ["applyResizeIncrement", {
// // tslint:disable-next-line:object-literal-sort-keys
// unit, base, geom,
// pad: [padWidth, padHeight].join("x"),
// size: [newWidth, newHeight].join("x"),
// }]);
return new Rect(geometry.x, geometry.y, width, height);
}
return [newWidth, newHeight];
}
private applyResizeIncrement(geom: Rect): [number, number] {
const unit = this.client.basicUnit;
const base = this.client.minSize;
const padWidth = this.client.geometry.width - this.client.clientSize.width;
const padHeight = this.client.geometry.height - this.client.clientSize.height;
const quotWidth = Math.floor((geom.width - base.width - padWidth ) / unit.width);
const quotHeight = Math.floor((geom.height - base.height - padHeight) / unit.height);
const newWidth = base.width + unit.width * quotWidth + padWidth ;
const newHeight = base.height + unit.height * quotHeight + padHeight;
// debugObj(() => ["applyResizeIncrement", {
// // tslint:disable-next-line:object-literal-sort-keys
// unit, base, geom,
// pad: [padWidth, padHeight].join("x"),
// size: [newWidth, newHeight].join("x"),
// }]);
return [newWidth, newHeight];
}
//#endregion
//#endregion
}

View File

@ -19,128 +19,125 @@
// DEALINGS IN THE SOFTWARE.
class TestDriver {
public currentScreen: number;
public currentWindow: number;
public numScreen: number;
public screenSize: Rect;
public windows: Window[];
public currentScreen: number;
public currentWindow: number;
public numScreen: number;
public screenSize: Rect;
public windows: Window[];
constructor() {
this.currentScreen = 0;
this.currentWindow = 0;
this.numScreen = 1;
this.screenSize = new Rect(0, 0, 10000, 10000);
this.windows = [];
}
constructor() {
this.currentScreen = 0;
this.currentWindow = 0;
this.numScreen = 1;
this.screenSize = new Rect(0, 0, 10000, 10000);
this.windows = [];
}
public forEachScreen(func: (srf: ISurface) => void) {
for (let screen = 0; screen < this.numScreen; screen ++)
func(new TestSurface(this, screen));
}
public forEachScreen(func: (srf: ISurface) => void) {
for (let screen = 0; screen < this.numScreen; screen++)
func(new TestSurface(this, screen));
}
public getCurrentContext(): ISurface {
const window = this.getCurrentWindow();
if (window)
return window.surface;
return new TestSurface(this, 0);
}
public getCurrentContext(): ISurface {
const window = this.getCurrentWindow();
if (window) return window.surface;
return new TestSurface(this, 0);
}
public getCurrentWindow(): Window | null {
return (this.windows.length !== 0)
? this.windows[this.currentWindow]
: null;
}
public getCurrentWindow(): Window | null {
return this.windows.length !== 0 ? this.windows[this.currentWindow] : null;
}
public getWorkingArea(srf: ISurface): Rect {
return this.screenSize;
}
public getWorkingArea(srf: ISurface): Rect {
return this.screenSize;
}
public setCurrentWindow(window: Window) {
const idx = this.windows.indexOf(window);
if (idx !== -1)
this.currentWindow = idx;
}
public setCurrentWindow(window: Window) {
const idx = this.windows.indexOf(window);
if (idx !== -1) this.currentWindow = idx;
}
public setTimeout(func: () => void, timeout: number) {
setTimeout(func, timeout);
}
public setTimeout(func: () => void, timeout: number) {
setTimeout(func, timeout);
}
}
class TestSurface implements ISurface {
public readonly screen: number;
public readonly screen: number;
public get id(): string {
return String(this.screen);
}
public get id(): string {
return String(this.screen);
}
public get ignore(): boolean {
// TODO: optionally ignore some surface to test LayoutStore
return false;
}
public get ignore(): boolean {
// TODO: optionally ignore some surface to test LayoutStore
return false;
}
public get workingArea(): Rect {
return this.driver.screenSize;
}
public get workingArea(): Rect {
return this.driver.screenSize;
}
constructor(private driver: TestDriver, screen: number) {
this.screen = screen;
}
constructor(private driver: TestDriver, screen: number) {
this.screen = screen;
}
public next(): ISurface {
return new TestSurface(this.driver, this.screen + 1);
}
public next(): ISurface {
return new TestSurface(this.driver, this.screen + 1);
}
}
class TestWindow implements IDriverWindow {
private static windowCount: number = 0;
private static windowCount: number = 0;
public readonly id: string;
public readonly shouldFloat: boolean;
public readonly shouldIgnore: boolean;
public readonly id: string;
public readonly shouldFloat: boolean;
public readonly shouldIgnore: boolean;
public surface: TestSurface;
public fullScreen: boolean;
public geometry: Rect;
public keepAbove: boolean;
public maximized: boolean;
public noBorder: boolean;
public surface: TestSurface;
public fullScreen: boolean;
public geometry: Rect;
public keepAbove: boolean;
public maximized: boolean;
public noBorder: boolean;
constructor(srf: TestSurface, geometry?: Rect, ignore?: boolean, float?: boolean) {
this.id = String(TestWindow.windowCount);
TestWindow.windowCount += 1;
constructor(
srf: TestSurface,
geometry?: Rect,
ignore?: boolean,
float?: boolean
) {
this.id = String(TestWindow.windowCount);
TestWindow.windowCount += 1;
this.shouldFloat = (float !== undefined) ? float : false;
this.shouldIgnore = (ignore !== undefined) ? ignore : false;
this.shouldFloat = float !== undefined ? float : false;
this.shouldIgnore = ignore !== undefined ? ignore : false;
this.surface = srf;
this.fullScreen = false;
this.geometry = geometry || new Rect(0, 0, 100, 100);
this.keepAbove = false;
this.maximized = false;
this.noBorder = false;
}
this.surface = srf;
this.fullScreen = false;
this.geometry = geometry || new Rect(0, 0, 100, 100);
this.keepAbove = false;
this.maximized = false;
this.noBorder = false;
}
public commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean) {
if (geometry)
this.geometry = geometry;
if (noBorder !== undefined)
this.noBorder = noBorder;
if (keepAbove !== undefined)
this.keepAbove = keepAbove;
}
public commit(geometry?: Rect, noBorder?: boolean, keepAbove?: boolean) {
if (geometry) this.geometry = geometry;
if (noBorder !== undefined) this.noBorder = noBorder;
if (keepAbove !== undefined) this.keepAbove = keepAbove;
}
public focus() {
// TODO: track focus
}
public focus() {
// TODO: track focus
}
public visible(srf: ISurface): boolean {
const tctx = srf as TestSurface;
return this.surface.screen === tctx.screen;
}
public visible(srf: ISurface): boolean {
const tctx = srf as TestSurface;
return this.surface.screen === tctx.screen;
}
}
function setTestConfig(name: string, value: any) {
if (!CONFIG)
CONFIG = {} as any;
(CONFIG as any)[name] = value;
if (!CONFIG) CONFIG = {} as any;
(CONFIG as any)[name] = value;
}

View File

@ -25,194 +25,262 @@
* In short, this class is just a bunch of event handling methods.
*/
class TilingController {
private engine: TilingEngine;
private engine: TilingEngine;
public constructor(engine: TilingEngine) {
this.engine = engine;
public constructor(engine: TilingEngine) {
this.engine = engine;
}
public onSurfaceUpdate(ctx: IDriverContext, comment: string): void {
debugObj(() => ["onSurfaceUpdate", { comment }]);
this.engine.arrange(ctx);
}
public onCurrentSurfaceChanged(ctx: IDriverContext): void {
debugObj(() => ["onCurrentSurfaceChanged", { srf: ctx.currentSurface }]);
this.engine.arrange(ctx);
}
public onWindowAdded(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowAdded", { window }]);
this.engine.manage(window);
/* move window to next surface if the current surface is "full" */
if (window.tileable) {
const srf = ctx.currentSurface;
const tiles = this.engine.windows.getVisibleTiles(srf);
const layoutCapcity = this.engine.layouts.getCurrentLayout(srf).capacity;
if (layoutCapcity !== undefined && tiles.length > layoutCapcity) {
const nsrf = ctx.currentSurface.next();
if (nsrf) {
// (window.window as KWinWindow).client.desktop = (nsrf as KWinSurface).desktop;
window.surface = nsrf;
ctx.currentSurface = nsrf;
}
}
}
public onSurfaceUpdate(ctx: IDriverContext, comment: string): void {
debugObj(() => ["onSurfaceUpdate", {comment}]);
this.engine.arrange(ctx);
}
public onWindowRemoved(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowRemoved", { window }]);
this.engine.unmanage(window);
this.engine.arrange(ctx);
}
public onWindowMoveStart(window: Window): void {
/* do nothing */
}
public onWindowMove(window: Window): void {
/* do nothing */
}
public onWindowMoveOver(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowMoveOver", { window }]);
/* swap window by dragging */
if (window.state === WindowState.Tiled) {
const tiles = this.engine.windows.getVisibleTiles(ctx.currentSurface);
const cursorPos = ctx.cursorPosition || window.actualGeometry.center;
const targets = tiles.filter(
(tile) =>
tile !== window && tile.actualGeometry.includesPoint(cursorPos)
);
if (targets.length === 1) {
this.engine.windows.swap(window, targets[0]);
this.engine.arrange(ctx);
return;
}
}
public onCurrentSurfaceChanged(ctx: IDriverContext): void {
debugObj(() => ["onCurrentSurfaceChanged", {srf: ctx.currentSurface}]);
/* ... or float window by dragging */
if (window.state === WindowState.Tiled) {
const diff = window.actualGeometry.subtract(window.geometry);
const distance = Math.sqrt(diff.x ** 2 + diff.y ** 2);
// TODO: arbitrary constant
if (distance > 30) {
window.floatGeometry = window.actualGeometry;
window.state = WindowState.Floating;
this.engine.arrange(ctx);
return;
}
}
public onWindowAdded(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowAdded", {window}]);
this.engine.manage(window);
/* ... or return to the previous position */
window.commit();
}
/* move window to next surface if the current surface is "full" */
if (window.tileable) {
const srf = ctx.currentSurface;
const tiles = this.engine.windows.getVisibleTiles(srf);
const layoutCapcity = this.engine.layouts.getCurrentLayout(srf).capacity;
if (layoutCapcity !== undefined && tiles.length > layoutCapcity) {
const nsrf = ctx.currentSurface.next();
if (nsrf) {
// (window.window as KWinWindow).client.desktop = (nsrf as KWinSurface).desktop;
window.surface = nsrf;
ctx.currentSurface = nsrf;
}
}
}
public onWindowResizeStart(window: Window): void {
/* do nothing */
}
public onWindowResize(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowResize", { window }]);
if (CONFIG.adjustLayout && CONFIG.adjustLayoutLive) {
if (window.state === WindowState.Tiled) {
this.engine.adjustLayout(window);
this.engine.arrange(ctx);
}
}
}
public onWindowResizeOver(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowResizeOver", { window }]);
if (CONFIG.adjustLayout && window.tiled) {
this.engine.adjustLayout(window);
this.engine.arrange(ctx);
} else if (!CONFIG.adjustLayout) this.engine.enforceSize(ctx, window);
}
public onWindowMaximizeChanged(
ctx: IDriverContext,
window: Window,
maximized: boolean
): void {
this.engine.arrange(ctx);
}
public onWindowGeometryChanged(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowGeometryChanged", { window }]);
this.engine.enforceSize(ctx, window);
}
// NOTE: accepts `null` to simplify caller. This event is a catch-all hack
// by itself anyway.
public onWindowChanged(
ctx: IDriverContext,
window: Window | null,
comment?: string
): void {
if (window) {
debugObj(() => ["onWindowChanged", { window, comment }]);
if (comment === "unminimized") ctx.currentWindow = window;
this.engine.arrange(ctx);
}
}
public onWindowFocused(ctx: IDriverContext, window: Window) {
window.timestamp = new Date().getTime();
}
public onShortcut(ctx: IDriverContext, input: Shortcut, data?: any) {
if (CONFIG.directionalKeyMode === "focus") {
switch (input) {
case Shortcut.Up:
input = Shortcut.FocusUp;
break;
case Shortcut.Down:
input = Shortcut.FocusDown;
break;
case Shortcut.Left:
input = Shortcut.FocusLeft;
break;
case Shortcut.Right:
input = Shortcut.FocusRight;
break;
case Shortcut.ShiftUp:
input = Shortcut.SwapUp;
break;
case Shortcut.ShiftDown:
input = Shortcut.SwapDown;
break;
case Shortcut.ShiftLeft:
input = Shortcut.SwapLeft;
break;
case Shortcut.ShiftRight:
input = Shortcut.SwapRight;
break;
}
}
public onWindowRemoved(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowRemoved", {window}]);
this.engine.unmanage(window);
this.engine.arrange(ctx);
if (this.engine.handleLayoutShortcut(ctx, input, data)) {
this.engine.arrange(ctx);
return;
}
public onWindowMoveStart(window: Window): void {
/* do nothing */
const window = ctx.currentWindow;
switch (input) {
case Shortcut.Up:
this.engine.focusOrder(ctx, -1);
break;
case Shortcut.Down:
this.engine.focusOrder(ctx, +1);
break;
case Shortcut.FocusUp:
this.engine.focusDir(ctx, "up");
break;
case Shortcut.FocusDown:
this.engine.focusDir(ctx, "down");
break;
case Shortcut.FocusLeft:
this.engine.focusDir(ctx, "left");
break;
case Shortcut.FocusRight:
this.engine.focusDir(ctx, "right");
break;
case Shortcut.GrowWidth:
if (window) this.engine.resizeWindow(window, "east", 1);
break;
case Shortcut.ShrinkWidth:
if (window) this.engine.resizeWindow(window, "east", -1);
break;
case Shortcut.GrowHeight:
if (window) this.engine.resizeWindow(window, "south", 1);
break;
case Shortcut.ShrinkHeight:
if (window) this.engine.resizeWindow(window, "south", -1);
break;
case Shortcut.ShiftUp:
if (window) this.engine.swapOrder(window, -1);
break;
case Shortcut.ShiftDown:
if (window) this.engine.swapOrder(window, +1);
break;
case Shortcut.SwapUp:
this.engine.swapDirOrMoveFloat(ctx, "up");
break;
case Shortcut.SwapDown:
this.engine.swapDirOrMoveFloat(ctx, "down");
break;
case Shortcut.SwapLeft:
this.engine.swapDirOrMoveFloat(ctx, "left");
break;
case Shortcut.SwapRight:
this.engine.swapDirOrMoveFloat(ctx, "right");
break;
case Shortcut.SetMaster:
if (window) this.engine.setMaster(window);
break;
case Shortcut.ToggleFloat:
if (window) this.engine.toggleFloat(window);
break;
case Shortcut.ToggleFloatAll:
this.engine.floatAll(ctx, ctx.currentSurface);
break;
case Shortcut.NextLayout:
this.engine.cycleLayout(ctx, 1);
break;
case Shortcut.PreviousLayout:
this.engine.cycleLayout(ctx, -1);
break;
case Shortcut.SetLayout:
if (typeof data === "string") this.engine.setLayout(ctx, data);
break;
}
public onWindowMove(window: Window): void {
/* do nothing */
}
public onWindowMoveOver(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowMoveOver", {window}]);
/* swap window by dragging */
if (window.state === WindowState.Tiled) {
const tiles = this.engine.windows.getVisibleTiles(ctx.currentSurface);
const cursorPos = ctx.cursorPosition || window.actualGeometry.center;
const targets = tiles.filter((tile) =>
tile !== window && tile.actualGeometry.includesPoint(cursorPos));
if (targets.length === 1) {
this.engine.windows.swap(window, targets[0]);
this.engine.arrange(ctx);
return;
}
}
/* ... or float window by dragging */
if (window.state === WindowState.Tiled) {
const diff = window.actualGeometry.subtract(window.geometry);
const distance = Math.sqrt(diff.x ** 2 + diff.y ** 2);
// TODO: arbitrary constant
if (distance > 30) {
window.floatGeometry = window.actualGeometry;
window.state = WindowState.Floating;
this.engine.arrange(ctx);
return;
}
}
/* ... or return to the previous position */
window.commit();
}
public onWindowResizeStart(window: Window): void {
/* do nothing */
}
public onWindowResize(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowResize", {window}]);
if (CONFIG.adjustLayout && CONFIG.adjustLayoutLive) {
if (window.state === WindowState.Tiled) {
this.engine.adjustLayout(window);
this.engine.arrange(ctx);
}
}
}
public onWindowResizeOver(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowResizeOver", {window}]);
if (CONFIG.adjustLayout && window.tiled) {
this.engine.adjustLayout(window);
this.engine.arrange(ctx);
} else if (!CONFIG.adjustLayout)
this.engine.enforceSize(ctx, window);
}
public onWindowMaximizeChanged(ctx: IDriverContext, window: Window, maximized: boolean): void {
this.engine.arrange(ctx);
}
public onWindowGeometryChanged(ctx: IDriverContext, window: Window): void {
debugObj(() => ["onWindowGeometryChanged", {window}]);
this.engine.enforceSize(ctx, window);
}
// NOTE: accepts `null` to simplify caller. This event is a catch-all hack
// by itself anyway.
public onWindowChanged(ctx: IDriverContext, window: Window | null, comment?: string): void {
if (window) {
debugObj(() => ["onWindowChanged", {window, comment}]);
if (comment === "unminimized")
ctx.currentWindow = window;
this.engine.arrange(ctx);
}
}
public onWindowFocused(ctx: IDriverContext, window: Window) {
window.timestamp = new Date().getTime();
}
public onShortcut(ctx: IDriverContext, input: Shortcut, data?: any) {
if (CONFIG.directionalKeyMode === "focus") {
switch (input) {
case Shortcut.Up : input = Shortcut.FocusUp; break;
case Shortcut.Down : input = Shortcut.FocusDown; break;
case Shortcut.Left : input = Shortcut.FocusLeft; break;
case Shortcut.Right: input = Shortcut.FocusRight; break;
case Shortcut.ShiftUp : input = Shortcut.SwapUp; break;
case Shortcut.ShiftDown : input = Shortcut.SwapDown; break;
case Shortcut.ShiftLeft : input = Shortcut.SwapLeft; break;
case Shortcut.ShiftRight: input = Shortcut.SwapRight; break;
}
}
if (this.engine.handleLayoutShortcut(ctx, input, data)) {
this.engine.arrange(ctx);
return;
}
const window = ctx.currentWindow;
switch (input) {
case Shortcut.Up : this.engine.focusOrder(ctx, -1); break;
case Shortcut.Down: this.engine.focusOrder(ctx, +1); break;
case Shortcut.FocusUp : this.engine.focusDir(ctx, "up" ); break;
case Shortcut.FocusDown : this.engine.focusDir(ctx, "down" ); break;
case Shortcut.FocusLeft : this.engine.focusDir(ctx, "left" ); break;
case Shortcut.FocusRight: this.engine.focusDir(ctx, "right"); break;
case Shortcut.GrowWidth : if (window) this.engine.resizeWindow(window, "east" , 1); break;
case Shortcut.ShrinkWidth : if (window) this.engine.resizeWindow(window, "east" , -1); break;
case Shortcut.GrowHeight : if (window) this.engine.resizeWindow(window, "south", 1); break;
case Shortcut.ShrinkHeight: if (window) this.engine.resizeWindow(window, "south", -1); break;
case Shortcut.ShiftUp : if (window) this.engine.swapOrder(window, -1); break;
case Shortcut.ShiftDown: if (window) this.engine.swapOrder(window, +1); break;
case Shortcut.SwapUp : this.engine.swapDirOrMoveFloat(ctx, "up"); break;
case Shortcut.SwapDown : this.engine.swapDirOrMoveFloat(ctx, "down"); break;
case Shortcut.SwapLeft : this.engine.swapDirOrMoveFloat(ctx, "left"); break;
case Shortcut.SwapRight: this.engine.swapDirOrMoveFloat(ctx, "right"); break;
case Shortcut.SetMaster : if (window) this.engine.setMaster(window); break;
case Shortcut.ToggleFloat: if (window) this.engine.toggleFloat(window); break;
case Shortcut.ToggleFloatAll: this.engine.floatAll(ctx, ctx.currentSurface); break;
case Shortcut.NextLayout: this.engine.cycleLayout(ctx, 1); break;
case Shortcut.PreviousLayout: this.engine.cycleLayout(ctx, -1); break;
case Shortcut.SetLayout: if (typeof data === "string") this.engine.setLayout(ctx, data); break;
}
this.engine.arrange(ctx);
}
this.engine.arrange(ctx);
}
}

View File

@ -24,448 +24,534 @@ type Direction = "up" | "down" | "left" | "right";
* Maintains tiling context and performs various tiling actions.
*/
class TilingEngine {
public layouts: LayoutStore;
public windows: WindowStore;
public layouts: LayoutStore;
public windows: WindowStore;
constructor() {
this.layouts = new LayoutStore();
this.windows = new WindowStore();
constructor() {
this.layouts = new LayoutStore();
this.windows = new WindowStore();
}
/**
* Adjust layout based on the change in size of a tile.
*
* This operation is completely layout-dependent, and no general implementation is
* provided.
*
* Used when tile is resized using mouse.
*/
public adjustLayout(basis: Window) {
const srf = basis.surface;
const layout = this.layouts.getCurrentLayout(srf);
if (layout.adjust) {
const area = srf.workingArea.gap(
CONFIG.screenGapLeft,
CONFIG.screenGapRight,
CONFIG.screenGapTop,
CONFIG.screenGapBottom
);
const tiles = this.windows.getVisibleTiles(srf);
layout.adjust(area, tiles, basis, basis.geometryDelta);
}
}
/**
* Resize the current floating window.
*
* @param window a floating window
*/
public resizeFloat(
window: Window,
dir: "east" | "west" | "south" | "north",
step: -1 | 1
) {
const srf = window.surface;
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.05;
const vStepSize = srf.workingArea.height * 0.05;
let hStep, vStep;
switch (dir) {
case "east":
(hStep = step), (vStep = 0);
break;
case "west":
(hStep = -step), (vStep = 0);
break;
case "south":
(hStep = 0), (vStep = step);
break;
case "north":
(hStep = 0), (vStep = -step);
break;
}
/**
* Adjust layout based on the change in size of a tile.
*
* This operation is completely layout-dependent, and no general implementation is
* provided.
*
* Used when tile is resized using mouse.
*/
public adjustLayout(basis: Window) {
const srf = basis.surface;
const layout = this.layouts.getCurrentLayout(srf);
if (layout.adjust) {
const area = srf.workingArea.gap(CONFIG.screenGapLeft, CONFIG.screenGapRight,
CONFIG.screenGapTop, CONFIG.screenGapBottom);
const tiles = this.windows.getVisibleTiles(srf);
layout.adjust(area, tiles, basis, basis.geometryDelta);
}
const geometry = window.actualGeometry;
const width = geometry.width + hStepSize * hStep;
const height = geometry.height + vStepSize * vStep;
window.forceSetGeometry(new Rect(geometry.x, geometry.y, width, height));
}
/**
* Resize the current tile by adjusting the layout.
*
* Used by grow/shrink shortcuts.
*/
public resizeTile(
basis: Window,
dir: "east" | "west" | "south" | "north",
step: -1 | 1
) {
const srf = basis.surface;
if (dir === "east") {
const maxX = basis.geometry.maxX;
const easternNeighbor = this.windows
.getVisibleTiles(srf)
.filter((tile) => tile.geometry.x >= maxX);
if (easternNeighbor.length === 0) {
dir = "west";
step *= -1;
}
} else if (dir === "south") {
const maxY = basis.geometry.maxY;
const southernNeighbor = this.windows
.getVisibleTiles(srf)
.filter((tile) => tile.geometry.y >= maxY);
if (southernNeighbor.length === 0) {
dir = "north";
step *= -1;
}
}
/**
* Resize the current floating window.
*
* @param window a floating window
*/
public resizeFloat(window: Window, dir: "east" | "west" | "south" | "north", step: -1 | 1) {
const srf = window.surface;
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.05;
const vStepSize = srf.workingArea.height * 0.05;
let hStep, vStep;
switch (dir) {
case "east" : hStep = step, vStep = 0; break;
case "west" : hStep = -step, vStep = 0; break;
case "south": hStep = 0, vStep = step; break;
case "north": hStep = 0, vStep = -step; break;
}
const geometry = window.actualGeometry;
const width = geometry.width + hStepSize * hStep;
const height = geometry.height + vStepSize * vStep;
window.forceSetGeometry(new Rect(geometry.x, geometry.y, width, height));
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.03;
const vStepSize = srf.workingArea.height * 0.03;
let delta: RectDelta;
switch (dir) {
case "east":
delta = new RectDelta(hStepSize * step, 0, 0, 0);
break;
case "west":
delta = new RectDelta(0, hStepSize * step, 0, 0);
break;
case "south":
delta = new RectDelta(0, 0, vStepSize * step, 0);
break;
case "north": /* passthru */
default:
delta = new RectDelta(0, 0, 0, vStepSize * step);
break;
}
/**
* Resize the current tile by adjusting the layout.
*
* Used by grow/shrink shortcuts.
*/
public resizeTile(basis: Window, dir: "east" | "west" | "south" | "north", step: -1 | 1) {
const srf = basis.surface;
if (dir === "east") {
const maxX = basis.geometry.maxX;
const easternNeighbor = this.windows.getVisibleTiles(srf)
.filter((tile) => tile.geometry.x >= maxX);
if (easternNeighbor.length === 0) {
dir = "west";
step *= -1;
}
} else if (dir === "south") {
const maxY = basis.geometry.maxY;
const southernNeighbor = this.windows.getVisibleTiles(srf)
.filter((tile) => tile.geometry.y >= maxY);
if (southernNeighbor.length === 0) {
dir = "north";
step *= -1;
}
}
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.03;
const vStepSize = srf.workingArea.height * 0.03;
let delta: RectDelta;
switch (dir) {
case "east" : delta = new RectDelta(hStepSize * step, 0, 0, 0); break;
case "west" : delta = new RectDelta(0, hStepSize * step, 0, 0); break;
case "south": delta = new RectDelta(0, 0, vStepSize * step, 0); break;
case "north": /* passthru */
default : delta = new RectDelta(0, 0, 0, vStepSize * step); break;
}
const layout = this.layouts.getCurrentLayout(srf);
if (layout.adjust) {
const area = srf.workingArea.gap(CONFIG.screenGapLeft, CONFIG.screenGapRight,
CONFIG.screenGapTop, CONFIG.screenGapBottom);
layout.adjust(area, this.windows.getVisibleTileables(srf), basis, delta);
}
const layout = this.layouts.getCurrentLayout(srf);
if (layout.adjust) {
const area = srf.workingArea.gap(
CONFIG.screenGapLeft,
CONFIG.screenGapRight,
CONFIG.screenGapTop,
CONFIG.screenGapBottom
);
layout.adjust(area, this.windows.getVisibleTileables(srf), basis, delta);
}
}
/**
* Resize the given window, by moving border inward or outward.
*
* The actual behavior depends on the state of the given window.
*
* @param dir which border
* @param step which direction. 1 means outward, -1 means inward.
*/
public resizeWindow(window: Window, dir: "east" | "west" | "south" | "north", step: -1 | 1) {
const state = window.state;
if (Window.isFloatingState(state))
this.resizeFloat(window, dir, step);
else if (Window.isTiledState(state))
this.resizeTile(window, dir, step);
}
/**
* Resize the given window, by moving border inward or outward.
*
* The actual behavior depends on the state of the given window.
*
* @param dir which border
* @param step which direction. 1 means outward, -1 means inward.
*/
public resizeWindow(
window: Window,
dir: "east" | "west" | "south" | "north",
step: -1 | 1
) {
const state = window.state;
if (Window.isFloatingState(state)) this.resizeFloat(window, dir, step);
else if (Window.isTiledState(state)) this.resizeTile(window, dir, step);
}
/**
* Arrange tiles on all screens.
*/
public arrange(ctx: IDriverContext) {
debug(() => "arrange");
ctx.screens.forEach((srf: ISurface) => {
this.arrangeScreen(ctx, srf);
/**
* Arrange tiles on all screens.
*/
public arrange(ctx: IDriverContext) {
debug(() => "arrange");
ctx.screens.forEach((srf: ISurface) => {
this.arrangeScreen(ctx, srf);
});
}
/**
* Arrange tiles on a screen.
*/
public arrangeScreen(ctx: IDriverContext, srf: ISurface) {
const layout = this.layouts.getCurrentLayout(srf);
const workingArea = srf.workingArea;
let tilingArea: Rect;
if (CONFIG.monocleMaximize && layout instanceof MonocleLayout)
tilingArea = workingArea;
else
tilingArea = workingArea.gap(
CONFIG.screenGapLeft,
CONFIG.screenGapRight,
CONFIG.screenGapTop,
CONFIG.screenGapBottom
);
const visibles = this.windows.getVisibleWindows(srf);
debugObj(() => [
"arrangeScreen",
{
layout,
srf,
visibles: visibles.length,
},
]);
visibles.forEach((window) => {
if (window.state === WindowState.Undecided)
window.state = window.shouldFloat
? WindowState.Floating
: WindowState.Tiled;
});
const tileables = this.windows.getVisibleTileables(srf);
if (CONFIG.maximizeSoleTile && tileables.length === 1) {
tileables[0].state = WindowState.Maximized;
tileables[0].geometry = workingArea;
} else if (tileables.length > 0)
layout.apply(new EngineContext(ctx, this), tileables, tilingArea);
if (CONFIG.limitTileWidthRatio > 0 && !(layout instanceof MonocleLayout)) {
const maxWidth = Math.floor(
workingArea.height * CONFIG.limitTileWidthRatio
);
tileables
.filter((tile) => tile.tiled && tile.geometry.width > maxWidth)
.forEach((tile) => {
const g = tile.geometry;
tile.geometry = new Rect(
g.x + Math.floor((g.width - maxWidth) / 2),
g.y,
maxWidth,
g.height
);
});
}
/**
* Arrange tiles on a screen.
*/
public arrangeScreen(ctx: IDriverContext, srf: ISurface) {
const layout = this.layouts.getCurrentLayout(srf);
visibles.forEach((window) => window.commit());
debugObj(() => ["arrangeScreen/finished", { srf }]);
}
const workingArea = srf.workingArea;
/**
* Re-apply window geometry, computed by layout algorithm.
*
* Sometimes applications move or resize windows without user intervention,
* which is straigh against the purpose of tiling WM. This operation
* move/resize such windows back to where/how they should be.
*/
public enforceSize(ctx: IDriverContext, window: Window) {
if (window.tiled && !window.actualGeometry.equals(window.geometry))
ctx.setTimeout(() => {
if (window.tiled) window.commit();
}, 10);
}
let tilingArea: Rect;
if (CONFIG.monocleMaximize && layout instanceof MonocleLayout)
tilingArea = workingArea;
else
tilingArea = workingArea.gap(CONFIG.screenGapLeft, CONFIG.screenGapRight,
CONFIG.screenGapTop, CONFIG.screenGapBottom);
/**
* Register the given window to WM.
*/
public manage(window: Window) {
if (!window.shouldIgnore) {
/* engine#arrange will update the state when required. */
window.state = WindowState.Undecided;
if (CONFIG.newWindowAsMaster) this.windows.unshift(window);
else this.windows.push(window);
}
}
const visibles = this.windows.getVisibleWindows(srf);
debugObj(() => ["arrangeScreen", {
layout, srf,
visibles: visibles.length,
}]);
/**
* Unregister the given window from WM.
*/
public unmanage(window: Window) {
this.windows.remove(window);
}
visibles.forEach((window) => {
if (window.state === WindowState.Undecided)
window.state = (window.shouldFloat) ? WindowState.Floating : WindowState.Tiled;
});
/**
* Focus the next or previous window.
*/
public focusOrder(ctx: IDriverContext, step: -1 | 1) {
const window = ctx.currentWindow;
const tileables = this.windows.getVisibleTileables(srf);
if (CONFIG.maximizeSoleTile && tileables.length === 1) {
tileables[0].state = WindowState.Maximized;
tileables[0].geometry = workingArea;
} else if (tileables.length > 0)
layout.apply(new EngineContext(ctx, this), tileables, tilingArea);
if (CONFIG.limitTileWidthRatio > 0 && !(layout instanceof MonocleLayout)) {
const maxWidth = Math.floor(workingArea.height * CONFIG.limitTileWidthRatio);
tileables.filter((tile) => tile.tiled && tile.geometry.width > maxWidth)
.forEach((tile) => {
const g = tile.geometry;
tile.geometry = new Rect(
g.x + Math.floor((g.width - maxWidth) / 2),
g.y, maxWidth, g.height,
);
});
}
visibles.forEach((window) => window.commit());
debugObj(() => ["arrangeScreen/finished", { srf }]);
/* if no current window, select the first tile. */
if (window === null) {
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1) ctx.currentWindow = tiles[0];
return;
}
/**
* Re-apply window geometry, computed by layout algorithm.
*
* Sometimes applications move or resize windows without user intervention,
* which is straigh against the purpose of tiling WM. This operation
* move/resize such windows back to where/how they should be.
*/
public enforceSize(ctx: IDriverContext, window: Window) {
if (window.tiled && !window.actualGeometry.equals(window.geometry))
ctx.setTimeout(() => {
if (window.tiled)
window.commit();
}, 10);
const visibles = this.windows.getVisibleWindows(ctx.currentSurface);
if (visibles.length === 0) /* nothing to focus */ return;
const idx = visibles.indexOf(window);
if (!window || idx < 0) {
/* unmanaged window -> focus master */
ctx.currentWindow = visibles[0];
return;
}
/**
* Register the given window to WM.
*/
public manage(window: Window) {
if (!window.shouldIgnore) {
/* engine#arrange will update the state when required. */
window.state = WindowState.Undecided;
if (CONFIG.newWindowAsMaster)
this.windows.unshift(window);
else
this.windows.push(window);
}
const num = visibles.length;
const newIndex = (idx + (step % num) + num) % num;
ctx.currentWindow = visibles[newIndex];
}
/**
* Focus a neighbor at the given direction.
*/
public focusDir(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
/* if no current window, select the first tile. */
if (window === null) {
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1) ctx.currentWindow = tiles[0];
return;
}
/**
* Unregister the given window from WM.
*/
public unmanage(window: Window) {
this.windows.remove(window);
const neighbor = this.getNeighborByDirection(ctx, window, dir);
if (neighbor) ctx.currentWindow = neighbor;
}
/**
* Swap the position of the current window with the next or previous window.
*/
public swapOrder(window: Window, step: -1 | 1) {
const srf = window.surface;
const visibles = this.windows.getVisibleWindows(srf);
if (visibles.length < 2) return;
const vsrc = visibles.indexOf(window);
const vdst = wrapIndex(vsrc + step, visibles.length);
const dstWin = visibles[vdst];
this.windows.move(window, dstWin);
}
/**
* Swap the position of the current window with a neighbor at the given direction.
*/
public swapDirection(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
if (window === null) {
/* if no current window, select the first tile. */
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1) ctx.currentWindow = tiles[0];
return;
}
/**
* Focus the next or previous window.
*/
public focusOrder(ctx: IDriverContext, step: -1 | 1) {
const window = ctx.currentWindow;
const neighbor = this.getNeighborByDirection(ctx, window, dir);
if (neighbor) this.windows.swap(window, neighbor);
}
/* if no current window, select the first tile. */
if (window === null) {
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1)
ctx.currentWindow = tiles[0];
return;
}
/**
* Move the given window towards the given direction by one step.
* @param window a floating window
* @param dir which direction
*/
public moveFloat(window: Window, dir: Direction) {
const srf = window.surface;
const visibles = this.windows.getVisibleWindows(ctx.currentSurface);
if (visibles.length === 0) /* nothing to focus */
return;
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.05;
const vStepSize = srf.workingArea.height * 0.05;
const idx = visibles.indexOf(window);
if (!window || idx < 0) { /* unmanaged window -> focus master */
ctx.currentWindow = visibles[0];
return;
}
const num = visibles.length;
const newIndex = (idx + (step % num) + num) % num;
ctx.currentWindow = visibles[newIndex];
let hStep, vStep;
switch (dir) {
case "up":
(hStep = 0), (vStep = -1);
break;
case "down":
(hStep = 0), (vStep = 1);
break;
case "left":
(hStep = -1), (vStep = 0);
break;
case "right":
(hStep = 1), (vStep = 0);
break;
}
/**
* Focus a neighbor at the given direction.
*/
public focusDir(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
const geometry = window.actualGeometry;
const x = geometry.x + hStepSize * hStep;
const y = geometry.y + vStepSize * vStep;
/* if no current window, select the first tile. */
if (window === null) {
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1)
ctx.currentWindow = tiles[0];
return;
}
window.forceSetGeometry(new Rect(x, y, geometry.width, geometry.height));
}
const neighbor = this.getNeighborByDirection(ctx, window, dir);
if (neighbor)
ctx.currentWindow = neighbor;
public swapDirOrMoveFloat(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
if (!window) return;
const state = window.state;
if (Window.isFloatingState(state)) this.moveFloat(window, dir);
else if (Window.isTiledState(state)) this.swapDirection(ctx, dir);
}
/**
* Toggle float mode of window.
*/
public toggleFloat(window: Window) {
window.state = !window.tileable ? WindowState.Tiled : WindowState.Floating;
}
/**
* Toggle float on all windows on the given surface.
*
* The behaviours of this operation depends on the number of floating
* windows: windows will be tiled if more than half are floating, and will
* be floated otherwise.
*/
public floatAll(ctx: IDriverContext, srf: ISurface) {
const windows = this.windows.getVisibleWindows(srf);
const numFloats = windows.reduce<number>((count, window) => {
return window.state === WindowState.Floating ? count + 1 : count;
}, 0);
if (numFloats < windows.length / 2) {
windows.forEach((window) => {
/* TODO: do not use arbitrary constants */
window.floatGeometry = window.actualGeometry.gap(4, 4, 4, 4);
window.state = WindowState.Floating;
});
ctx.showNotification("Float All");
} else {
windows.forEach((window) => {
window.state = WindowState.Tiled;
});
ctx.showNotification("Tile All");
}
}
/**
* Set the current window as the "master".
*
* The "master" window is simply the first window in the window list.
* Some layouts depend on this assumption, and will make such windows more
* visible than others.
*/
public setMaster(window: Window) {
this.windows.setMaster(window);
}
/**
* Change the layout of the current surface to the next.
*/
public cycleLayout(ctx: IDriverContext, step: 1 | -1) {
const layout = this.layouts.cycleLayout(ctx.currentSurface, step);
if (layout) ctx.showNotification(layout.description);
}
/**
* Set the layout of the current surface to the specified layout.
*/
public setLayout(ctx: IDriverContext, layoutClassID: string) {
const layout = this.layouts.setLayout(ctx.currentSurface, layoutClassID);
if (layout) ctx.showNotification(layout.description);
}
/**
* Let the current layout override shortcut.
*
* @returns True if the layout overrides the shortcut. False, otherwise.
*/
public handleLayoutShortcut(
ctx: IDriverContext,
input: Shortcut,
data?: any
): boolean {
const layout = this.layouts.getCurrentLayout(ctx.currentSurface);
if (layout.handleShortcut)
return layout.handleShortcut(new EngineContext(ctx, this), input, data);
return false;
}
private getNeighborByDirection(
ctx: IDriverContext,
basis: Window,
dir: Direction
): Window | null {
let vertical: boolean;
let sign: -1 | 1;
switch (dir) {
case "up":
vertical = true;
sign = -1;
break;
case "down":
vertical = true;
sign = 1;
break;
case "left":
vertical = false;
sign = -1;
break;
case "right":
vertical = false;
sign = 1;
break;
default:
return null;
}
/**
* Swap the position of the current window with the next or previous window.
*/
public swapOrder(window: Window, step: -1 | 1) {
const srf = window.surface;
const visibles = this.windows.getVisibleWindows(srf);
if (visibles.length < 2)
return;
const candidates = this.windows
.getVisibleTiles(ctx.currentSurface)
.filter(
vertical
? (tile) => tile.geometry.y * sign > basis.geometry.y * sign
: (tile) => tile.geometry.x * sign > basis.geometry.x * sign
)
.filter(
vertical
? (tile) =>
overlap(
basis.geometry.x,
basis.geometry.maxX,
tile.geometry.x,
tile.geometry.maxX
)
: (tile) =>
overlap(
basis.geometry.y,
basis.geometry.maxY,
tile.geometry.y,
tile.geometry.maxY
)
);
if (candidates.length === 0) return null;
const vsrc = visibles.indexOf(window);
const vdst = wrapIndex(vsrc + step, visibles.length);
const dstWin = visibles[vdst];
const min =
sign *
candidates.reduce(
vertical
? (prevMin, tile): number => Math.min(tile.geometry.y * sign, prevMin)
: (prevMin, tile): number =>
Math.min(tile.geometry.x * sign, prevMin),
Infinity
);
this.windows.move(window, dstWin);
}
const closest = candidates.filter(
vertical
? (tile) => tile.geometry.y === min
: (tile) => tile.geometry.x === min
);
/**
* Swap the position of the current window with a neighbor at the given direction.
*/
public swapDirection(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
if (window === null) {
/* if no current window, select the first tile. */
const tiles = this.windows.getVisibleTiles(ctx.currentSurface);
if (tiles.length > 1)
ctx.currentWindow = tiles[0];
return;
}
const neighbor = this.getNeighborByDirection(ctx, window, dir);
if (neighbor)
this.windows.swap(window, neighbor);
}
/**
* Move the given window towards the given direction by one step.
* @param window a floating window
* @param dir which direction
*/
public moveFloat(window: Window, dir: Direction) {
const srf = window.surface;
// TODO: configurable step size?
const hStepSize = srf.workingArea.width * 0.05;
const vStepSize = srf.workingArea.height * 0.05;
let hStep, vStep;
switch (dir) {
case "up" : hStep = 0, vStep = -1; break;
case "down" : hStep = 0, vStep = 1; break;
case "left" : hStep = -1, vStep = 0; break;
case "right": hStep = 1, vStep = 0; break;
}
const geometry = window.actualGeometry;
const x = geometry.x + hStepSize * hStep;
const y = geometry.y + vStepSize * vStep;
window.forceSetGeometry(new Rect(x, y, geometry.width, geometry.height));
}
public swapDirOrMoveFloat(ctx: IDriverContext, dir: Direction) {
const window = ctx.currentWindow;
if (!window) return;
const state = window.state;
if (Window.isFloatingState(state))
this.moveFloat(window, dir);
else if (Window.isTiledState(state))
this.swapDirection(ctx, dir);
}
/**
* Toggle float mode of window.
*/
public toggleFloat(window: Window) {
window.state = (!window.tileable)
? WindowState.Tiled
: WindowState.Floating;
}
/**
* Toggle float on all windows on the given surface.
*
* The behaviours of this operation depends on the number of floating
* windows: windows will be tiled if more than half are floating, and will
* be floated otherwise.
*/
public floatAll(ctx: IDriverContext, srf: ISurface) {
const windows = this.windows.getVisibleWindows(srf);
const numFloats = windows.reduce<number>((count, window) => {
return (window.state === WindowState.Floating) ? count + 1 : count;
}, 0);
if (numFloats < windows.length / 2) {
windows.forEach((window) => {
/* TODO: do not use arbitrary constants */
window.floatGeometry = window.actualGeometry.gap(4, 4, 4, 4);
window.state = WindowState.Floating;
});
ctx.showNotification("Float All");
} else {
windows.forEach((window) => {
window.state = WindowState.Tiled;
});
ctx.showNotification("Tile All");
}
}
/**
* Set the current window as the "master".
*
* The "master" window is simply the first window in the window list.
* Some layouts depend on this assumption, and will make such windows more
* visible than others.
*/
public setMaster(window: Window) {
this.windows.setMaster(window);
}
/**
* Change the layout of the current surface to the next.
*/
public cycleLayout(ctx: IDriverContext, step: 1 | -1) {
const layout = this.layouts.cycleLayout(ctx.currentSurface, step);
if (layout)
ctx.showNotification(layout.description);
}
/**
* Set the layout of the current surface to the specified layout.
*/
public setLayout(ctx: IDriverContext, layoutClassID: string) {
const layout = this.layouts.setLayout(ctx.currentSurface, layoutClassID);
if (layout)
ctx.showNotification(layout.description);
}
/**
* Let the current layout override shortcut.
*
* @returns True if the layout overrides the shortcut. False, otherwise.
*/
public handleLayoutShortcut(ctx: IDriverContext, input: Shortcut, data?: any): boolean {
const layout = this.layouts.getCurrentLayout(ctx.currentSurface);
if (layout.handleShortcut)
return layout.handleShortcut(new EngineContext(ctx, this), input, data);
return false;
}
private getNeighborByDirection(ctx: IDriverContext, basis: Window, dir: Direction): Window | null{
let vertical: boolean;
let sign: -1 | 1;
switch (dir) {
case "up" : vertical = true ; sign = -1; break;
case "down" : vertical = true ; sign = 1; break;
case "left" : vertical = false; sign = -1; break;
case "right": vertical = false; sign = 1; break;
default: return null;
}
const candidates = this.windows.getVisibleTiles(ctx.currentSurface)
.filter((vertical)
? ((tile) => tile.geometry.y * sign > basis.geometry.y * sign)
: ((tile) => tile.geometry.x * sign > basis.geometry.x * sign))
.filter((vertical)
? ((tile) => overlap(basis.geometry.x, basis.geometry.maxX, tile.geometry.x, tile.geometry.maxX))
: ((tile) => overlap(basis.geometry.y, basis.geometry.maxY, tile.geometry.y, tile.geometry.maxY)));
if (candidates.length === 0)
return null;
const min = sign * candidates.reduce(
(vertical)
? ((prevMin, tile): number => Math.min(tile.geometry.y * sign, prevMin))
: ((prevMin, tile): number => Math.min(tile.geometry.x * sign, prevMin)),
Infinity);
const closest = candidates.filter(
(vertical)
? (tile) => tile.geometry.y === min
: (tile) => tile.geometry.x === min);
return closest.sort((a, b) => b.timestamp - a.timestamp)[0];
}
return closest.sort((a, b) => b.timestamp - a.timestamp)[0];
}
}

View File

@ -25,34 +25,33 @@
* not really a find-grained control mechanism, but is simple and concise.
*/
class EngineContext {
public get backend(): string {
return this.drvctx.backend;
}
public get backend(): string {
return this.drvctx.backend;
}
public get currentWindow(): Window | null {
return this.drvctx.currentWindow;
}
public get currentWindow(): Window | null {
return this.drvctx.currentWindow;
}
public set currentWindow(window: Window | null) {
this.drvctx.currentWindow = window;
}
public set currentWindow(window: Window | null) {
this.drvctx.currentWindow = window;
}
constructor(private drvctx: IDriverContext, private engine: TilingEngine) {
}
constructor(private drvctx: IDriverContext, private engine: TilingEngine) {}
public setTimeout(func: () => void, timeout: number): void {
this.drvctx.setTimeout(func, timeout);
}
public setTimeout(func: () => void, timeout: number): void {
this.drvctx.setTimeout(func, timeout);
}
public cycleFocus(step: -1 | 1) {
this.engine.focusOrder(this.drvctx, step);
}
public cycleFocus(step: -1 | 1) {
this.engine.focusOrder(this.drvctx, step);
}
public moveWindow(window: Window, target: Window, after?: boolean) {
this.engine.windows.move(window, target, after);
}
public moveWindow(window: Window, target: Window, after?: boolean) {
this.engine.windows.move(window, target, after);
}
public showNotification(text: string) {
this.drvctx.showNotification(text);
}
public showNotification(text: string) {
this.drvctx.showNotification(text);
}
}

View File

@ -19,91 +19,89 @@
// DEALINGS IN THE SOFTWARE.
class LayoutStoreEntry {
public get currentLayout(): ILayout {
return this.loadLayout(this.currentID);
public get currentLayout(): ILayout {
return this.loadLayout(this.currentID);
}
private currentIndex: number | null;
private currentID: string;
private layouts: { [key: string]: ILayout };
private previousID: string;
constructor() {
this.currentIndex = 0;
this.currentID = CONFIG.layoutOrder[0];
this.layouts = {};
this.previousID = this.currentID;
this.loadLayout(this.currentID);
}
public cycleLayout(step: -1 | 1): ILayout {
this.previousID = this.currentID;
this.currentIndex =
this.currentIndex !== null
? wrapIndex(this.currentIndex + step, CONFIG.layoutOrder.length)
: 0;
this.currentID = CONFIG.layoutOrder[this.currentIndex];
return this.loadLayout(this.currentID);
}
public setLayout(targetID: string): ILayout {
const targetLayout = this.loadLayout(targetID);
if (
targetLayout instanceof MonocleLayout &&
this.currentLayout instanceof MonocleLayout
) {
/* toggle Monocle "OFF" */
this.currentID = this.previousID;
this.previousID = targetID;
} else if (this.currentID !== targetID) {
this.previousID = this.currentID;
this.currentID = targetID;
}
private currentIndex: number | null;
private currentID: string;
private layouts: {[key: string]: ILayout};
private previousID: string;
constructor() {
this.currentIndex = 0;
this.currentID = CONFIG.layoutOrder[0];
this.layouts = {};
this.previousID = this.currentID;
this.updateCurrentIndex();
return targetLayout;
}
this.loadLayout(this.currentID);
}
private updateCurrentIndex(): void {
const idx = CONFIG.layoutOrder.indexOf(this.currentID);
this.currentIndex = idx === -1 ? null : idx;
}
public cycleLayout(step: -1 | 1): ILayout {
this.previousID = this.currentID;
this.currentIndex = (this.currentIndex !== null)
? wrapIndex(this.currentIndex + step, CONFIG.layoutOrder.length)
: 0
;
this.currentID = CONFIG.layoutOrder[this.currentIndex];
return this.loadLayout(this.currentID);
}
public setLayout(targetID: string): ILayout {
const targetLayout = this.loadLayout(targetID);
if (targetLayout instanceof MonocleLayout
&& this.currentLayout instanceof MonocleLayout) {
/* toggle Monocle "OFF" */
this.currentID = this.previousID;
this.previousID = targetID;
} else if (this.currentID !== targetID) {
this.previousID = this.currentID;
this.currentID = targetID;
}
this.updateCurrentIndex();
return targetLayout;
}
private updateCurrentIndex(): void {
const idx = CONFIG.layoutOrder.indexOf(this.currentID);
this.currentIndex = (idx === -1) ? null : idx;
}
private loadLayout(ID: string): ILayout {
let layout = this.layouts[ID];
if (!layout)
layout = this.layouts[ID] = CONFIG.layoutFactories[ID]();
return layout
}
private loadLayout(ID: string): ILayout {
let layout = this.layouts[ID];
if (!layout) layout = this.layouts[ID] = CONFIG.layoutFactories[ID]();
return layout;
}
}
class LayoutStore {
private store: { [key: string]: LayoutStoreEntry };
private store: { [key: string]: LayoutStoreEntry };
constructor() {
this.store = {};
}
constructor() {
this.store = {};
}
public getCurrentLayout(srf: ISurface): ILayout {
return (srf.ignore)
? FloatingLayout.instance
: this.getEntry(srf.id).currentLayout;
}
public getCurrentLayout(srf: ISurface): ILayout {
return srf.ignore
? FloatingLayout.instance
: this.getEntry(srf.id).currentLayout;
}
public cycleLayout(srf: ISurface, step: 1 | -1): ILayout | null {
if (srf.ignore)
return null;
return this.getEntry(srf.id).cycleLayout(step);
}
public cycleLayout(srf: ISurface, step: 1 | -1): ILayout | null {
if (srf.ignore) return null;
return this.getEntry(srf.id).cycleLayout(step);
}
public setLayout(srf: ISurface, layoutClassID: string): ILayout | null {
if (srf.ignore)
return null;
return this.getEntry(srf.id).setLayout(layoutClassID);
}
public setLayout(srf: ISurface, layoutClassID: string): ILayout | null {
if (srf.ignore) return null;
return this.getEntry(srf.id).setLayout(layoutClassID);
}
private getEntry(key: string): LayoutStoreEntry {
if (!this.store[key])
this.store[key] = new LayoutStoreEntry();
return this.store[key];
}
private getEntry(key: string): LayoutStoreEntry {
if (!this.store[key]) this.store[key] = new LayoutStoreEntry();
return this.store[key];
}
}

View File

@ -19,189 +19,195 @@
// DEALINGS IN THE SOFTWARE.
enum WindowState {
/* initial value */
Unmanaged,
/* initial value */
Unmanaged,
/* script-external state - overrides internal state */
NativeFullscreen,
NativeMaximized,
/* script-external state - overrides internal state */
NativeFullscreen,
NativeMaximized,
/* script-internal state */
Floating,
Maximized,
Tiled,
TiledAfloat,
Undecided,
/* script-internal state */
Floating,
Maximized,
Tiled,
TiledAfloat,
Undecided,
}
class Window {
public static isTileableState(state: WindowState): boolean {
return (
(state === WindowState.Tiled)
|| (state === WindowState.Maximized)
|| (state === WindowState.TiledAfloat)
);
public static isTileableState(state: WindowState): boolean {
return (
state === WindowState.Tiled ||
state === WindowState.Maximized ||
state === WindowState.TiledAfloat
);
}
public static isTiledState(state: WindowState): boolean {
return state === WindowState.Tiled || state === WindowState.Maximized;
}
public static isFloatingState(state: WindowState): boolean {
return state === WindowState.Floating || state === WindowState.TiledAfloat;
}
public readonly id: string;
public readonly window: IDriverWindow;
public get actualGeometry(): Readonly<Rect> {
return this.window.geometry;
}
public get shouldFloat(): boolean {
return this.window.shouldFloat;
}
public get shouldIgnore(): boolean {
return this.window.shouldIgnore;
}
/** If this window ***can be*** tiled by layout. */
public get tileable(): boolean {
return Window.isTileableState(this.state);
}
/** If this window is ***already*** tiled, thus a part of the current layout. */
public get tiled(): boolean {
return Window.isTiledState(this.state);
}
/** If this window is floating, thus its geometry is not tightly managed. */
public get floating(): boolean {
return Window.isFloatingState(this.state);
}
public get geometryDelta(): RectDelta {
return RectDelta.fromRects(this.geometry, this.actualGeometry);
}
public floatGeometry: Rect;
public geometry: Rect;
public timestamp: number;
/**
* The current state of the window.
*
* This value affects what and how properties gets commited to the backend.
*
* Avoid comparing this value directly, and use `tileable`, `tiled`,
* `floating` as much as possible.
*/
public get state(): WindowState {
/* external states override the internal state. */
if (this.window.fullScreen) return WindowState.NativeFullscreen;
if (this.window.maximized) return WindowState.NativeMaximized;
return this.internalState;
}
public set state(value: WindowState) {
const state = this.state;
/* cannot transit to the current state */
if (state === value) return;
if (
(state === WindowState.Unmanaged || Window.isTileableState(state)) &&
Window.isFloatingState(value)
)
this.shouldCommitFloat = true;
else if (Window.isFloatingState(state) && Window.isTileableState(value))
/* save the current geometry before leaving floating state */
this.floatGeometry = this.actualGeometry;
this.internalState = value;
}
public get surface(): ISurface {
return this.window.surface;
}
public set surface(srf: ISurface) {
this.window.surface = srf;
}
public get weight(): number {
const srfID = this.window.surface.id;
const weight: number | undefined = this.weightMap[srfID];
if (weight === undefined) {
this.weightMap[srfID] = 1.0;
return 1.0;
}
return weight;
}
public static isTiledState(state: WindowState): boolean {
return (
(state === WindowState.Tiled)
|| (state === WindowState.Maximized)
);
public set weight(value: number) {
const srfID = this.window.surface.id;
this.weightMap[srfID] = value;
}
private internalState: WindowState;
private shouldCommitFloat: boolean;
private weightMap: { [key: string]: number };
constructor(window: IDriverWindow) {
this.id = window.id;
this.window = window;
this.floatGeometry = window.geometry;
this.geometry = window.geometry;
this.timestamp = 0;
this.internalState = WindowState.Unmanaged;
this.shouldCommitFloat = this.shouldFloat;
this.weightMap = {};
}
public commit() {
const state = this.state;
debugObj(() => ["Window#commit", { state: WindowState[state] }]);
switch (state) {
case WindowState.NativeMaximized:
this.window.commit(undefined, undefined, false);
break;
case WindowState.NativeFullscreen:
this.window.commit(undefined, undefined, undefined);
break;
case WindowState.Floating:
if (!this.shouldCommitFloat) break;
this.window.commit(this.floatGeometry, false, CONFIG.keepFloatAbove);
this.shouldCommitFloat = false;
break;
case WindowState.Maximized:
this.window.commit(this.geometry, true, false);
break;
case WindowState.Tiled:
this.window.commit(this.geometry, CONFIG.noTileBorder, false);
break;
case WindowState.TiledAfloat:
if (!this.shouldCommitFloat) break;
this.window.commit(this.floatGeometry, false, CONFIG.keepFloatAbove);
this.shouldCommitFloat = false;
break;
}
}
public static isFloatingState(state: WindowState): boolean {
return (
(state === WindowState.Floating)
|| (state === WindowState.TiledAfloat)
);
}
/**
* Force apply the geometry *immediately*.
*
* This method is a quick hack created for engine#resizeFloat, thus should
* not be used in other places.
*/
public forceSetGeometry(geometry: Rect) {
this.window.commit(geometry);
}
public readonly id: string;
public readonly window: IDriverWindow;
public visible(srf: ISurface): boolean {
return this.window.visible(srf);
}
public get actualGeometry(): Readonly<Rect> { return this.window.geometry; }
public get shouldFloat(): boolean { return this.window.shouldFloat; }
public get shouldIgnore(): boolean { return this.window.shouldIgnore; }
/** If this window ***can be*** tiled by layout. */
public get tileable(): boolean { return Window.isTileableState(this.state); }
/** If this window is ***already*** tiled, thus a part of the current layout. */
public get tiled(): boolean { return Window.isTiledState(this.state); }
/** If this window is floating, thus its geometry is not tightly managed. */
public get floating(): boolean { return Window.isFloatingState(this.state); }
public get geometryDelta(): RectDelta {
return RectDelta.fromRects(this.geometry, this.actualGeometry);
}
public floatGeometry: Rect;
public geometry: Rect;
public timestamp: number;
/**
* The current state of the window.
*
* This value affects what and how properties gets commited to the backend.
*
* Avoid comparing this value directly, and use `tileable`, `tiled`,
* `floating` as much as possible.
*/
public get state(): WindowState {
/* external states override the internal state. */
if (this.window.fullScreen)
return WindowState.NativeFullscreen;
if (this.window.maximized)
return WindowState.NativeMaximized;
return this.internalState;
}
public set state(value: WindowState) {
const state = this.state;
/* cannot transit to the current state */
if (state === value)
return;
if ((state === WindowState.Unmanaged || Window.isTileableState(state)) && Window.isFloatingState(value))
this.shouldCommitFloat = true;
else if (Window.isFloatingState(state) && Window.isTileableState(value))
/* save the current geometry before leaving floating state */
this.floatGeometry = this.actualGeometry;
this.internalState = value;
}
public get surface(): ISurface {
return this.window.surface;
}
public set surface(srf: ISurface) {
this.window.surface = srf;
}
public get weight(): number {
const srfID = this.window.surface.id;
const weight: number | undefined = this.weightMap[srfID];
if (weight === undefined) {
this.weightMap[srfID] = 1.0;
return 1.0;
}
return weight;
}
public set weight(value: number) {
const srfID = this.window.surface.id;
this.weightMap[srfID] = value;
}
private internalState: WindowState;
private shouldCommitFloat: boolean;
private weightMap: {[key: string]: number};
constructor(window: IDriverWindow) {
this.id = window.id;
this.window = window;
this.floatGeometry = window.geometry;
this.geometry = window.geometry;
this.timestamp = 0;
this.internalState = WindowState.Unmanaged;
this.shouldCommitFloat = this.shouldFloat;
this.weightMap = {};
}
public commit() {
const state = this.state;
debugObj(() => ["Window#commit", {state: WindowState[state]}]);
switch (state) {
case WindowState.NativeMaximized:
this.window.commit(undefined, undefined, false);
break;
case WindowState.NativeFullscreen:
this.window.commit(undefined, undefined, undefined);
break;
case WindowState.Floating:
if (!this.shouldCommitFloat) break;
this.window.commit(this.floatGeometry, false, CONFIG.keepFloatAbove);
this.shouldCommitFloat = false;
break;
case WindowState.Maximized:
this.window.commit(this.geometry, true, false);
break;
case WindowState.Tiled:
this.window.commit(this.geometry, CONFIG.noTileBorder, false);
break;
case WindowState.TiledAfloat:
if (!this.shouldCommitFloat) break;
this.window.commit(this.floatGeometry, false, CONFIG.keepFloatAbove);
this.shouldCommitFloat = false;
break;
}
}
/**
* Force apply the geometry *immediately*.
*
* This method is a quick hack created for engine#resizeFloat, thus should
* not be used in other places.
*/
public forceSetGeometry(geometry: Rect) {
this.window.commit(geometry);
}
public visible(srf: ISurface): boolean {
return this.window.visible(srf);
}
public toString(): string {
return "Window(" + String(this.window) + ")";
}
public toString(): string {
return "Window(" + String(this.window) + ")";
}
}

View File

@ -19,88 +19,84 @@
// DEALINGS IN THE SOFTWARE.
class WindowStore {
public list: Window[];
public list: Window[];
constructor(windows?: Window[]) {
this.list = windows || [];
}
constructor(windows?: Window[]) {
this.list = windows || [];
}
public move(srcWin: Window, destWin: Window, after?: boolean) {
const srcIdx = this.list.indexOf(srcWin);
const destIdx = this.list.indexOf(destWin);
if (srcIdx === -1 || destIdx === -1)
return;
public move(srcWin: Window, destWin: Window, after?: boolean) {
const srcIdx = this.list.indexOf(srcWin);
const destIdx = this.list.indexOf(destWin);
if (srcIdx === -1 || destIdx === -1) return;
this.list.splice(srcIdx, 1);
this.list.splice((after) ? (destIdx + 1) : destIdx, 0, srcWin);
}
this.list.splice(srcIdx, 1);
this.list.splice(after ? destIdx + 1 : destIdx, 0, srcWin);
}
public setMaster(window: Window) {
const idx = this.list.indexOf(window);
if (idx === -1) return;
this.list.splice(idx, 1);
this.list.splice(0, 0, window);
}
public setMaster(window: Window) {
const idx = this.list.indexOf(window);
if (idx === -1) return;
this.list.splice(idx, 1);
this.list.splice(0, 0, window);
}
public swap(alpha: Window, beta: Window) {
const alphaIndex = this.list.indexOf(alpha);
const betaIndex = this.list.indexOf(beta);
if (alphaIndex < 0 || betaIndex < 0)
return;
public swap(alpha: Window, beta: Window) {
const alphaIndex = this.list.indexOf(alpha);
const betaIndex = this.list.indexOf(beta);
if (alphaIndex < 0 || betaIndex < 0) return;
this.list[alphaIndex] = beta;
this.list[betaIndex] = alpha;
}
this.list[alphaIndex] = beta;
this.list[betaIndex] = alpha;
}
//#region Storage Operation
//#region Storage Operation
public get length(): number {
return this.list.length;
}
public get length(): number {
return this.list.length;
}
public at(idx: number) {
return this.list[idx];
}
public at(idx: number) {
return this.list[idx];
}
public indexOf(window: Window) {
return this.list.indexOf(window);
}
public indexOf(window: Window) {
return this.list.indexOf(window);
}
public push(window: Window) {
this.list.push(window);
}
public push(window: Window) {
this.list.push(window);
}
public remove(window: Window) {
const idx = this.list.indexOf(window);
if (idx >= 0)
this.list.splice(idx, 1);
}
public remove(window: Window) {
const idx = this.list.indexOf(window);
if (idx >= 0) this.list.splice(idx, 1);
}
public unshift(window: Window) {
this.list.unshift(window);
}
//#endregion
public unshift(window: Window) {
this.list.unshift(window);
}
//#endregion
//#region Querying Windows
//#region Querying Windows
/** Returns all visible windows on the given surface. */
public getVisibleWindows(srf: ISurface): Window[] {
return this.list.filter((win) => win.visible(srf));
}
/** Returns all visible windows on the given surface. */
public getVisibleWindows(srf: ISurface): Window[] {
return this.list.filter((win) => win.visible(srf));
}
/** Return all visible "Tile" windows on the given surface. */
public getVisibleTiles(srf: ISurface): Window[] {
return this.list.filter((win) =>
win.tiled && win.visible(srf));
}
/** Return all visible "Tile" windows on the given surface. */
public getVisibleTiles(srf: ISurface): Window[] {
return this.list.filter((win) => win.tiled && win.visible(srf));
}
/**
* Return all visible "tileable" windows on the given surface
* @see Window#tileable
*/
public getVisibleTileables(srf: ISurface): Window[] {
return this.list.filter((win) => win.tileable && win.visible(srf));
}
/**
* Return all visible "tileable" windows on the given surface
* @see Window#tileable
*/
public getVisibleTileables(srf: ISurface): Window[] {
return this.list.filter((win) => win.tileable && win.visible(srf));
}
//#endregion
//#endregion
}

View File

@ -28,7 +28,7 @@ declare var mousePoller: Plasma.PlasmaCore.DataSource;
declare var scriptRoot: object;
interface PopupDialog {
show(text: string): void;
show(text: string): void;
}
declare var popupDialog: PopupDialog;

172
src/extern/kwin.d.ts vendored
View File

@ -22,103 +22,103 @@
// https://techbase.kde.org/Development/Tutorials/KWin/Scripting/API_4.9
declare namespace KWin {
/* enum ClientAreaOption */
var PlacementArea: number;
/* enum ClientAreaOption */
var PlacementArea: number;
function readConfig(key: string, defaultValue?: any): any;
function readConfig(key: string, defaultValue?: any): any;
function registerShortcut(
title: string,
text: string,
keySequence: string,
callback: any
): boolean;
function registerShortcut(
title: string,
text: string,
keySequence: string,
callback: any
): boolean;
interface WorkspaceWrapper {
/* read-only */
readonly activeScreen: number;
readonly currentActivity: string;
readonly numScreens: number;
interface WorkspaceWrapper {
/* read-only */
readonly activeScreen: number;
readonly currentActivity: string;
readonly numScreens: number;
/* read-write */
activeClient: KWin.Client;
currentDesktop: number;
desktops: number;
/* read-write */
activeClient: KWin.Client;
currentDesktop: number;
desktops: number;
/* signals */
activitiesChanged: QSignal;
activityAdded: QSignal;
activityRemoved: QSignal;
clientAdded: QSignal;
clientFullScreenSet: QSignal;
clientMaximizeSet: QSignal;
clientMinimized: QSignal;
clientRemoved: QSignal;
clientUnminimized: QSignal;
currentActivityChanged: QSignal;
currentDesktopChanged: QSignal;
numberDesktopsChanged: QSignal;
numberScreensChanged: QSignal;
screenResized: QSignal;
/* signals */
activitiesChanged: QSignal;
activityAdded: QSignal;
activityRemoved: QSignal;
clientAdded: QSignal;
clientFullScreenSet: QSignal;
clientMaximizeSet: QSignal;
clientMinimized: QSignal;
clientRemoved: QSignal;
clientUnminimized: QSignal;
currentActivityChanged: QSignal;
currentDesktopChanged: QSignal;
numberDesktopsChanged: QSignal;
numberScreensChanged: QSignal;
screenResized: QSignal;
/* functions */
clientList(): Client[];
clientArea(option: number, screen: number, desktop: number): QRect;
}
/* functions */
clientList(): Client[];
clientArea(option: number, screen: number, desktop: number): QRect;
}
interface Options {
/* signal */
configChanged: QSignal;
}
interface Options {
/* signal */
configChanged: QSignal;
}
interface Toplevel {
/* read-only */
readonly activities: string[]; /* Not exactly `Array` */
readonly dialog: boolean;
readonly resourceClass: QByteArray;
readonly resourceName: QByteArray;
readonly screen: number;
readonly splash: boolean;
readonly utility: boolean;
readonly windowId: number;
readonly windowRole: QByteArray;
interface Toplevel {
/* read-only */
readonly activities: string[] /* Not exactly `Array` */;
readonly dialog: boolean;
readonly resourceClass: QByteArray;
readonly resourceName: QByteArray;
readonly screen: number;
readonly splash: boolean;
readonly utility: boolean;
readonly windowId: number;
readonly windowRole: QByteArray;
readonly clientPos: QPoint;
readonly clientSize: QSize;
readonly clientPos: QPoint;
readonly clientSize: QSize;
/* signal */
activitiesChanged: QSignal;
geometryChanged: QSignal;
screenChanged: QSignal;
windowShown: QSignal;
}
/* signal */
activitiesChanged: QSignal;
geometryChanged: QSignal;
screenChanged: QSignal;
windowShown: QSignal;
}
interface Client extends Toplevel {
/* read-only */
readonly active: boolean;
readonly caption: string;
readonly maxSize: QSize;
readonly minSize: QSize;
readonly modal: boolean;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
readonly specialWindow: boolean;
interface Client extends Toplevel {
/* read-only */
readonly active: boolean;
readonly caption: string;
readonly maxSize: QSize;
readonly minSize: QSize;
readonly modal: boolean;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
readonly specialWindow: boolean;
/* read-write */
desktop: number;
fullScreen: boolean;
geometry: QRect;
keepAbove: boolean;
keepBelow: boolean;
minimized: boolean;
noBorder: boolean;
onAllDesktops: boolean;
basicUnit: QSize;
/* read-write */
desktop: number;
fullScreen: boolean;
geometry: QRect;
keepAbove: boolean;
keepBelow: boolean;
minimized: boolean;
noBorder: boolean;
onAllDesktops: boolean;
basicUnit: QSize;
/* signals */
activeChanged: QSignal;
desktopChanged: QSignal;
moveResizedChanged: QSignal;
}
/* signals */
activeChanged: QSignal;
desktopChanged: QSignal;
moveResizedChanged: QSignal;
}
}

View File

@ -19,55 +19,55 @@
// DEALINGS IN THE SOFTWARE.
declare namespace Plasma {
namespace TaskManager {
/* reference: https://github.com/KDE/plasma-workspace/blob/master/libtaskmanager/activityinfo.h */
interface ActivityInfo {
/* read-only */
readonly currentActivity: string;
readonly numberOfRunningActivities: number;
namespace TaskManager {
/* reference: https://github.com/KDE/plasma-workspace/blob/master/libtaskmanager/activityinfo.h */
interface ActivityInfo {
/* read-only */
readonly currentActivity: string;
readonly numberOfRunningActivities: number;
/* methods */
runningActivities(): string[];
activityName(id: string): string;
/* methods */
runningActivities(): string[];
activityName(id: string): string;
/* signals */
currentActivityChanged: QSignal;
numberOfRunningActivitiesChanged: QSignal;
namesOfRunningActivitiesChanged: QSignal;
}
/* signals */
currentActivityChanged: QSignal;
numberOfRunningActivitiesChanged: QSignal;
namesOfRunningActivitiesChanged: QSignal;
}
}
namespace PlasmaCore {
/* reference: https://techbase.kde.org/Development/Tutorials/Plasma4/QML/API#DataSource */
interface DataSource {
readonly sources: string[];
readonly valid: boolean;
readonly data: {[key: string]: object}; /* variant map */
namespace PlasmaCore {
/* reference: https://techbase.kde.org/Development/Tutorials/Plasma4/QML/API#DataSource */
interface DataSource {
readonly sources: string[];
readonly valid: boolean;
readonly data: { [key: string]: object } /* variant map */;
interval: number;
engine: string;
connectedSources: string[];
interval: number;
engine: string;
connectedSources: string[];
/** (sourceName: string, data: object) */
onNewData: QSignal;
/** (source: string) */
onSourceAdded: QSignal;
/** (source: string) */
onSourceRemoved: QSignal;
/** (source: string) */
onSourceConnected: QSignal;
/** (source: string) */
onSourceDisconnected: QSignal;
onIntervalChanged: QSignal;
onEngineChanged: QSignal;
onDataChanged: QSignal;
onConnectedSourcesChanged: QSignal;
onSourcesChanged: QSignal;
/** (sourceName: string, data: object) */
onNewData: QSignal;
/** (source: string) */
onSourceAdded: QSignal;
/** (source: string) */
onSourceRemoved: QSignal;
/** (source: string) */
onSourceConnected: QSignal;
/** (source: string) */
onSourceDisconnected: QSignal;
onIntervalChanged: QSignal;
onEngineChanged: QSignal;
onDataChanged: QSignal;
onConnectedSourcesChanged: QSignal;
onSourcesChanged: QSignal;
keysForSource(source: string): string[];
serviceForSource(source: string): object; // TODO: returns Service
connectSource(source: string): void;
disconnectSource(source: string): void;
}
keysForSource(source: string): string[];
serviceForSource(source: string): object; // TODO: returns Service
connectSource(source: string): void;
disconnectSource(source: string): void;
}
}
}

43
src/extern/qt.d.ts vendored
View File

@ -18,49 +18,48 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
interface QByteArray {
/* keep it empty for now */
/* keep it empty for now */
}
interface QRect {
height: number;
width: number;
x: number;
y: number;
height: number;
width: number;
x: number;
y: number;
}
interface QPoint {
x: number;
y: number;
x: number;
y: number;
}
interface QSize {
width: number;
height: number;
width: number;
height: number;
}
interface QSignal {
connect(callback: any): void;
disconnect(callback: any): void;
connect(callback: any): void;
disconnect(callback: any): void;
}
/* Reference: http://doc.qt.io/qt-5/qml-qtqml-timer.html */
interface QQmlTimer {
interval: number;
repeat: boolean;
running: boolean;
triggeredOnStart: boolean;
interval: number;
repeat: boolean;
running: boolean;
triggeredOnStart: boolean;
triggered: QSignal;
triggered: QSignal;
restart(): void;
start(): void;
stop(): void;
restart(): void;
start(): void;
stop(): void;
}
declare namespace Qt {
function createQmlObject(qml: string, parent: object, filepath?: string): any;
function createQmlObject(qml: string, parent: object, filepath?: string): any;
function rect(x: number, y: number, width: number, height: number): QRect;
function rect(x: number, y: number, width: number, height: number): QRect;
}

View File

@ -19,86 +19,103 @@
// DEALINGS IN THE SOFTWARE.
enum CascadeDirection {
NorthWest = 0,
North = 1,
NorthEast = 2,
East = 3,
SouthEast = 4,
South = 5,
SouthWest = 6,
West = 7,
NorthWest = 0,
North = 1,
NorthEast = 2,
East = 3,
SouthEast = 4,
South = 5,
SouthWest = 6,
West = 7,
}
class CascadeLayout implements ILayout {
public static readonly id = "CascadeLayout";
public static readonly id = "CascadeLayout";
/** Decompose direction into vertical and horizontal steps */
public static decomposeDirection(dir: CascadeDirection): [-1|0|1, -1|0|1] {
switch (dir) {
case CascadeDirection.NorthWest: return [ -1, -1 ];
case CascadeDirection.North : return [ -1, 0 ];
case CascadeDirection.NorthEast: return [ -1, 1 ];
case CascadeDirection.East : return [ 0, 1 ];
case CascadeDirection.SouthEast: return [ 1, 1 ];
case CascadeDirection.South : return [ 1, 0 ];
case CascadeDirection.SouthWest: return [ 1, -1 ];
case CascadeDirection.West : return [ 0, -1 ];
}
/** Decompose direction into vertical and horizontal steps */
public static decomposeDirection(
dir: CascadeDirection
): [-1 | 0 | 1, -1 | 0 | 1] {
switch (dir) {
case CascadeDirection.NorthWest:
return [-1, -1];
case CascadeDirection.North:
return [-1, 0];
case CascadeDirection.NorthEast:
return [-1, 1];
case CascadeDirection.East:
return [0, 1];
case CascadeDirection.SouthEast:
return [1, 1];
case CascadeDirection.South:
return [1, 0];
case CascadeDirection.SouthWest:
return [1, -1];
case CascadeDirection.West:
return [0, -1];
}
}
public readonly classID = CascadeLayout.id;
public readonly classID = CascadeLayout.id;
public get description() {
return "Cascade [" + CascadeDirection[this.dir] + "]";
}
constructor(private dir: CascadeDirection = CascadeDirection.SouthEast) {
/* nothing */
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
const [vertStep, horzStep] = CascadeLayout.decomposeDirection(this.dir);
// TODO: adjustable step size
const stepSize = 25;
const windowWidth = (horzStep !== 0)
? area.width - (stepSize * (tileables.length - 1))
: area.width;
const windowHeight = (vertStep !== 0)
? area.height - (stepSize * (tileables.length - 1))
: area.height;
const baseX = (horzStep >= 0) ? area.x : area.maxX - windowWidth;
const baseY = (vertStep >= 0) ? area.y : area.maxY - windowHeight;
let x = baseX, y = baseY;
tileables.forEach((tile) => {
tile.state = WindowState.Tiled;
tile.geometry = new Rect(x, y, windowWidth, windowHeight);
x += horzStep * stepSize;
y += vertStep * stepSize;
});
}
public clone(): CascadeLayout {
return new CascadeLayout(this.dir);
}
public handleShortcut(ctx: EngineContext, input: Shortcut, data?: any): boolean {
switch (input) {
case Shortcut.Increase:
this.dir = (this.dir + 1 + 8) % 8;
ctx.showNotification(this.description);
break;
case Shortcut.Decrease:
this.dir = (this.dir - 1 + 8) % 8;
ctx.showNotification(this.description);
break;
default:
return false;
}
return true;
public get description() {
return "Cascade [" + CascadeDirection[this.dir] + "]";
}
constructor(private dir: CascadeDirection = CascadeDirection.SouthEast) {
/* nothing */
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
const [vertStep, horzStep] = CascadeLayout.decomposeDirection(this.dir);
// TODO: adjustable step size
const stepSize = 25;
const windowWidth =
horzStep !== 0
? area.width - stepSize * (tileables.length - 1)
: area.width;
const windowHeight =
vertStep !== 0
? area.height - stepSize * (tileables.length - 1)
: area.height;
const baseX = horzStep >= 0 ? area.x : area.maxX - windowWidth;
const baseY = vertStep >= 0 ? area.y : area.maxY - windowHeight;
let x = baseX,
y = baseY;
tileables.forEach((tile) => {
tile.state = WindowState.Tiled;
tile.geometry = new Rect(x, y, windowWidth, windowHeight);
x += horzStep * stepSize;
y += vertStep * stepSize;
});
}
public clone(): CascadeLayout {
return new CascadeLayout(this.dir);
}
public handleShortcut(
ctx: EngineContext,
input: Shortcut,
data?: any
): boolean {
switch (input) {
case Shortcut.Increase:
this.dir = (this.dir + 1 + 8) % 8;
ctx.showNotification(this.description);
break;
case Shortcut.Decrease:
this.dir = (this.dir - 1 + 8) % 8;
ctx.showNotification(this.description);
break;
default:
return false;
}
return true;
}
}

View File

@ -19,23 +19,24 @@
// DEALINGS IN THE SOFTWARE.
class FloatingLayout implements ILayout {
public static readonly id = "FloatingLayout ";
public static instance = new FloatingLayout();
public static readonly id = "FloatingLayout ";
public static instance = new FloatingLayout();
public readonly classID = FloatingLayout.id;
public readonly description: string = "Floating";
public readonly classID = FloatingLayout.id;
public readonly description: string = "Floating";
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach((tileable: Window) =>
tileable.state = WindowState.TiledAfloat);
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach(
(tileable: Window) => (tileable.state = WindowState.TiledAfloat)
);
}
public clone(): this {
/* fake clone */
return this;
}
public clone(): this {
/* fake clone */
return this;
}
public toString(): string {
return "FloatingLayout()";
}
public toString(): string {
return "FloatingLayout()";
}
}

View File

@ -18,237 +18,290 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
interface ILayoutPart {
adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): RectDelta;
apply(area: Rect, tiles: Window[]): Rect[];
adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): RectDelta;
apply(area: Rect, tiles: Window[]): Rect[];
}
class FillLayoutPart implements ILayoutPart {
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): RectDelta {
/* do nothing */
return delta;
}
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): RectDelta {
/* do nothing */
return delta;
}
public apply(area: Rect, tiles: Window[]): Rect[] {
return tiles.map((tile) => {
return area;
});
}
public apply(area: Rect, tiles: Window[]): Rect[] {
return tiles.map((tile) => {
return area;
});
}
}
class HalfSplitLayoutPart<L extends ILayoutPart, R extends ILayoutPart> implements ILayoutPart {
/** the rotation angle for this part.
*
* | angle | direction | primary |
* | ----- | ---------- | ------- |
* | 0 | horizontal | left |
* | 90 | vertical | top |
* | 180 | horizontal | right |
* | 270 | vertical | bottom |
*/
public angle: 0 | 90 | 180 | 270;
class HalfSplitLayoutPart<L extends ILayoutPart, R extends ILayoutPart>
implements ILayoutPart
{
/** the rotation angle for this part.
*
* | angle | direction | primary |
* | ----- | ---------- | ------- |
* | 0 | horizontal | left |
* | 90 | vertical | top |
* | 180 | horizontal | right |
* | 270 | vertical | bottom |
*/
public angle: 0 | 90 | 180 | 270;
public gap: number;
public primarySize: number;
public ratio: number;
public gap: number;
public primarySize: number;
public ratio: number;
private get horizontal(): boolean {
return this.angle === 0 || this.angle === 180;
private get horizontal(): boolean {
return this.angle === 0 || this.angle === 180;
}
private get reversed(): boolean {
return this.angle === 180 || this.angle === 270;
}
constructor(public primary: L, public secondary: R) {
this.angle = 0;
this.gap = 0;
this.primarySize = 1;
this.ratio = 0.5;
}
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): RectDelta {
const basisIndex = tiles.indexOf(basis);
if (basisIndex < 0) return delta;
if (tiles.length <= this.primarySize) {
/* primary only */
return this.primary.adjust(area, tiles, basis, delta);
} else if (this.primarySize === 0) {
/* secondary only */
return this.secondary.adjust(area, tiles, basis, delta);
} else {
/* both parts */
/** which part to adjust. 0 = primary, 1 = secondary */
const targetIndex = basisIndex < this.primarySize ? 0 : 1;
if (targetIndex === /* primary */ 0) {
delta = this.primary.adjust(
area,
tiles.slice(0, this.primarySize),
basis,
delta
);
} else {
delta = this.secondary.adjust(
area,
tiles.slice(this.primarySize),
basis,
delta
);
}
this.ratio = LayoutUtils.adjustAreaHalfWeights(
area,
this.reversed ? 1 - this.ratio : this.ratio,
this.gap,
this.reversed ? 1 - targetIndex : targetIndex,
delta,
this.horizontal
);
if (this.reversed) this.ratio = 1 - this.ratio;
switch (this.angle * 10 + targetIndex + 1) {
case 1: /* 0, Primary */
case 1802 /* 180, Secondary */:
return new RectDelta(0, delta.west, delta.south, delta.north);
case 2:
case 1801:
return new RectDelta(delta.east, 0, delta.south, delta.north);
case 901:
case 2702:
return new RectDelta(delta.east, delta.west, 0, delta.north);
case 902:
case 2701:
return new RectDelta(delta.east, delta.west, delta.south, 0);
}
return delta;
}
}
private get reversed(): boolean {
return this.angle === 180 || this.angle === 270;
}
constructor(
public primary: L,
public secondary: R,
) {
this.angle = 0;
this.gap = 0;
this.primarySize = 1;
this.ratio = 0.5;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): RectDelta {
const basisIndex = tiles.indexOf(basis);
if (basisIndex < 0)
return delta;
if (tiles.length <= this.primarySize) {
/* primary only */
return this.primary.adjust(area, tiles, basis, delta);
} else if (this.primarySize === 0) {
/* secondary only */
return this.secondary.adjust(area, tiles, basis, delta);
} else {
/* both parts */
/** which part to adjust. 0 = primary, 1 = secondary */
const targetIndex = (basisIndex < this.primarySize) ? 0 : 1;
if (targetIndex === /* primary */ 0) {
delta = this.primary.adjust(area, tiles.slice(0, this.primarySize), basis, delta);
} else {
delta = this.secondary.adjust(area, tiles.slice(this.primarySize), basis, delta);
}
this.ratio = LayoutUtils.adjustAreaHalfWeights(
area,
(this.reversed) ? 1 - this.ratio : this.ratio,
this.gap,
(this.reversed) ? 1 - targetIndex : targetIndex,
delta,
this.horizontal,
);
if (this.reversed)
this.ratio = 1 - this.ratio;
switch((this.angle * 10) + targetIndex + 1) {
case 1: /* 0, Primary */
case 1802: /* 180, Secondary */
return new RectDelta(0, delta.west, delta.south, delta.north);
case 2:
case 1801:
return new RectDelta(delta.east, 0, delta.south, delta.north);
case 901:
case 2702:
return new RectDelta(delta.east, delta.west, 0, delta.north);
case 902:
case 2701:
return new RectDelta(delta.east, delta.west, delta.south, 0);
}
return delta;
}
}
public apply(area: Rect, tiles: Window[]): Rect[] {
if (tiles.length <= this.primarySize) {
/* primary only */
return this.primary.apply(area, tiles);
} else if (this.primarySize === 0) {
/* secondary only */
return this.secondary.apply(area, tiles);
} else {
/* both parts */
const reversed = this.reversed;
const ratio = (reversed) ? 1 - this.ratio: this.ratio;
const [area1, area2] = LayoutUtils.splitAreaHalfWeighted(area, ratio, this.gap, this.horizontal);
const result1 = this.primary.apply((reversed) ? area2 : area1, tiles.slice(0, this.primarySize));
const result2 = this.secondary.apply((reversed) ? area1 : area2, tiles.slice(this.primarySize));
return result1.concat(result2);
}
public apply(area: Rect, tiles: Window[]): Rect[] {
if (tiles.length <= this.primarySize) {
/* primary only */
return this.primary.apply(area, tiles);
} else if (this.primarySize === 0) {
/* secondary only */
return this.secondary.apply(area, tiles);
} else {
/* both parts */
const reversed = this.reversed;
const ratio = reversed ? 1 - this.ratio : this.ratio;
const [area1, area2] = LayoutUtils.splitAreaHalfWeighted(
area,
ratio,
this.gap,
this.horizontal
);
const result1 = this.primary.apply(
reversed ? area2 : area1,
tiles.slice(0, this.primarySize)
);
const result2 = this.secondary.apply(
reversed ? area1 : area2,
tiles.slice(this.primarySize)
);
return result1.concat(result2);
}
}
}
class StackLayoutPart implements ILayoutPart {
public gap: number;
public gap: number;
constructor() {
this.gap = 0;
}
constructor() {
this.gap = 0;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): RectDelta {
const weights = LayoutUtils.adjustAreaWeights(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
tiles.indexOf(basis),
delta,
false
);
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): RectDelta {
const weights = LayoutUtils.adjustAreaWeights(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
tiles.indexOf(basis),
delta,
false
);
weights.forEach((weight, i) => {
tiles[i].weight = weight * tiles.length;
});
weights.forEach((weight, i) => {
tiles[i].weight = weight * tiles.length;
});
const idx = tiles.indexOf(basis);
return new RectDelta(
delta.east,
delta.west,
(idx === tiles.length - 1) ? delta.south : 0,
(idx === 0) ? delta.north : 0
);
}
public apply(area: Rect, tiles: Window[]): Rect[] {
const weights = tiles.map((tile) => tile.weight);
return LayoutUtils.splitAreaWeighted(area, weights, this.gap);
}
const idx = tiles.indexOf(basis);
return new RectDelta(
delta.east,
delta.west,
idx === tiles.length - 1 ? delta.south : 0,
idx === 0 ? delta.north : 0
);
}
public apply(area: Rect, tiles: Window[]): Rect[] {
const weights = tiles.map((tile) => tile.weight);
return LayoutUtils.splitAreaWeighted(area, weights, this.gap);
}
}
class RotateLayoutPart<T extends ILayoutPart> implements ILayoutPart {
constructor(
public inner: T,
public angle: 0 | 90 | 180 | 270 = 0,
) {
constructor(public inner: T, public angle: 0 | 90 | 180 | 270 = 0) {}
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): RectDelta {
// let area = area, delta = delta;
switch (this.angle) {
case 0:
break;
case 90:
area = new Rect(area.y, area.x, area.height, area.width);
delta = new RectDelta(delta.south, delta.north, delta.east, delta.west);
break;
case 180:
delta = new RectDelta(delta.west, delta.east, delta.south, delta.north);
break;
case 270:
area = new Rect(area.y, area.x, area.height, area.width);
delta = new RectDelta(delta.north, delta.south, delta.east, delta.west);
break;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): RectDelta {
// let area = area, delta = delta;
switch (this.angle) {
case 0 : break;
case 90 :
area = new Rect(area.y, area.x, area.height, area.width);
delta = new RectDelta(delta.south, delta.north, delta.east, delta.west); break;
case 180:
delta = new RectDelta(delta.west, delta.east, delta.south, delta.north); break;
case 270:
area = new Rect(area.y, area.x, area.height, area.width);
delta = new RectDelta(delta.north, delta.south, delta.east, delta.west); break;
}
delta = this.inner.adjust(area, tiles, basis, delta);
delta = this.inner.adjust(area, tiles, basis, delta);
switch (this.angle) {
case 0:
delta = delta;
break;
case 90:
delta = new RectDelta(delta.south, delta.north, delta.east, delta.west);
break;
case 180:
delta = new RectDelta(delta.west, delta.east, delta.south, delta.north);
break;
case 270:
delta = new RectDelta(delta.north, delta.south, delta.east, delta.west);
break;
}
return delta;
}
switch (this.angle) {
case 0 : delta = delta; break;
case 90 : delta = new RectDelta(delta.south, delta.north, delta.east, delta.west); break;
case 180: delta = new RectDelta(delta.west, delta.east, delta.south, delta.north); break;
case 270: delta = new RectDelta(delta.north, delta.south, delta.east, delta.west); break;
}
return delta;
public apply(area: Rect, tiles: Window[]): Rect[] {
switch (this.angle) {
case 0:
break;
case 90:
area = new Rect(area.y, area.x, area.height, area.width);
break;
case 180:
break;
case 270:
area = new Rect(area.y, area.x, area.height, area.width);
break;
}
public apply(area: Rect, tiles: Window[]): Rect[] {
switch (this.angle) {
case 0 : break;
case 90 : area = new Rect(area.y, area.x, area.height, area.width); break;
case 180: break;
case 270: area = new Rect(area.y, area.x, area.height, area.width); break;
}
const innerResult = this.inner.apply(area, tiles);
const innerResult = this.inner.apply(area, tiles);
switch (this.angle) {
case 0:
return innerResult;
case 90:
return innerResult.map((g) =>
new Rect(g.y, g.x, g.height, g.width));
case 180:
return innerResult.map((g) => {
const rx = g.x - area.x;
const newX = area.x + area.width - (rx + g.width);
return new Rect(newX, g.y, g.width, g.height)
});
case 270:
return innerResult.map((g) => {
const rx = g.x - area.x;
const newY = area.x + area.width - (rx + g.width);
return new Rect(g.y, newY, g.height, g.width)
});
}
switch (this.angle) {
case 0:
return innerResult;
case 90:
return innerResult.map((g) => new Rect(g.y, g.x, g.height, g.width));
case 180:
return innerResult.map((g) => {
const rx = g.x - area.x;
const newX = area.x + area.width - (rx + g.width);
return new Rect(newX, g.y, g.width, g.height);
});
case 270:
return innerResult.map((g) => {
const rx = g.x - area.x;
const newY = area.x + area.width - (rx + g.width);
return new Rect(g.y, newY, g.height, g.width);
});
}
}
public rotate(amount: -90 | 90): void {
// -90 | 0 | 90 | 180 | 270 | 360
let angle = this.angle + amount;
if (angle < 0)
angle = 270;
else if (angle >= 360)
angle = 0;
public rotate(amount: -90 | 90): void {
// -90 | 0 | 90 | 180 | 270 | 360
let angle = this.angle + amount;
if (angle < 0) angle = 270;
else if (angle >= 360) angle = 0;
this.angle = angle as (0 | 90 | 180 | 270);
}
this.angle = angle as 0 | 90 | 180 | 270;
}
}

View File

@ -19,188 +19,226 @@
// DEALINGS IN THE SOFTWARE.
class LayoutUtils {
/**
* Split a (virtual) line into weighted lines w/ gaps.
* @param length The length of the line to be splitted
* @param weights The weight of each part
* @param gap The size of gap b/w parts
* @returns An array of parts: [begin, length]
*/
public static splitWeighted(
[begin, length]: [number, number],
weights: number[],
gap: number,
): Array<[number, number]> {
gap = (gap !== undefined) ? gap : 0;
/**
* Split a (virtual) line into weighted lines w/ gaps.
* @param length The length of the line to be splitted
* @param weights The weight of each part
* @param gap The size of gap b/w parts
* @returns An array of parts: [begin, length]
*/
public static splitWeighted(
[begin, length]: [number, number],
weights: number[],
gap: number
): Array<[number, number]> {
gap = gap !== undefined ? gap : 0;
const n = weights.length;
const actualLength = length - (n - 1) * gap;
const weightSum = weights.reduce((sum, weight) => sum + weight, 0);
const n = weights.length;
const actualLength = length - (n - 1) * gap;
const weightSum = weights.reduce((sum, weight) => sum + weight, 0);
let weightAcc = 0;
return weights.map((weight, i) => {
const partBegin = actualLength * weightAcc / weightSum + (i * gap);
const partLength = actualLength * weight / weightSum;
weightAcc += weight;
return [begin + Math.floor(partBegin), Math.floor(partLength)];
});
let weightAcc = 0;
return weights.map((weight, i) => {
const partBegin = (actualLength * weightAcc) / weightSum + i * gap;
const partLength = (actualLength * weight) / weightSum;
weightAcc += weight;
return [begin + Math.floor(partBegin), Math.floor(partLength)];
});
}
/**
* Split an area into multiple parts based on weight.
* @param area The area to be splitted
* @param weights The weight of each part
* @param gap The size of gaps b/w parts
* @param horizontal If true, split horizontally. Otherwise, vertically.
*/
public static splitAreaWeighted(
area: Rect,
weights: number[],
gap?: number,
horizontal?: boolean
): Rect[] {
gap = gap !== undefined ? gap : 0;
horizontal = horizontal !== undefined ? horizontal : false;
const line: [number, number] = horizontal
? [area.x, area.width]
: [area.y, area.height];
const parts = LayoutUtils.splitWeighted(line, weights, gap);
return parts.map(([begin, length]) =>
horizontal
? new Rect(begin, area.y, length, area.height)
: new Rect(area.x, begin, area.width, length)
);
}
/**
* Split an area into two based on weight.
* @param area The area to be splitted
* @param weight The weight of the left/upper part.
* @param gap The size of gaps b/w parts
* @param horizontal If true, split horizontally. Otherwise, vertically.
*/
public static splitAreaHalfWeighted(
area: Rect,
weight: number,
gap?: number,
horizontal?: boolean
): Rect[] {
return LayoutUtils.splitAreaWeighted(
area,
[weight, 1 - weight],
gap,
horizontal
);
}
/**
* Recalculate the weights of subareas of the line, based on size change.
* @param line The line being aplitted
* @param weights The weight of each part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param deltaFw The amount of growth towards the origin.
* @param deltaBw The amount of growth towards the infinity.
*/
public static adjustWeights(
[begin, length]: [number, number],
weights: number[],
gap: number,
target: number,
deltaFw: number,
deltaBw: number
): number[] {
// TODO: configurable min length?
const minLength = 1;
const parts = this.splitWeighted([begin, length], weights, gap);
const [targetBase, targetLength] = parts[target];
/* apply backward delta */
if (target > 0 && deltaBw !== 0) {
const neighbor = target - 1;
const [neighborBase, neighborLength] = parts[neighbor];
/* limit delta to prevent squeezing windows */
const delta = clip(
deltaBw,
minLength - targetLength,
neighborLength - minLength
);
parts[target] = [targetBase - delta, targetLength + delta];
parts[neighbor] = [neighborBase, neighborLength - delta];
}
/**
* Split an area into multiple parts based on weight.
* @param area The area to be splitted
* @param weights The weight of each part
* @param gap The size of gaps b/w parts
* @param horizontal If true, split horizontally. Otherwise, vertically.
*/
public static splitAreaWeighted(area: Rect, weights: number[], gap?: number, horizontal?: boolean): Rect[] {
gap = (gap !== undefined) ? gap : 0;
horizontal = (horizontal !== undefined) ? horizontal : false;
/* apply forward delta */
if (target < parts.length - 1 && deltaFw !== 0) {
const neighbor = target + 1;
const [neighborBase, neighborLength] = parts[neighbor];
const line: [number, number] = (horizontal) ? [area.x, area.width] : [area.y, area.height];
const parts = LayoutUtils.splitWeighted(line, weights, gap);
/* limit delta to prevent squeezing windows */
const delta = clip(
deltaFw,
minLength - targetLength,
neighborLength - minLength
);
return parts.map(([begin, length]) =>
(horizontal)
? new Rect(begin, area.y, length, area.height)
: new Rect(area.x, begin, area.width, length),
);
parts[target] = [targetBase, targetLength + delta];
parts[neighbor] = [neighborBase + delta, neighborLength - delta];
}
/**
* Split an area into two based on weight.
* @param area The area to be splitted
* @param weight The weight of the left/upper part.
* @param gap The size of gaps b/w parts
* @param horizontal If true, split horizontally. Otherwise, vertically.
*/
public static splitAreaHalfWeighted(area: Rect, weight: number, gap?: number, horizontal?: boolean): Rect[] {
return LayoutUtils.splitAreaWeighted(area, [weight, 1 - weight], gap, horizontal);
}
return LayoutUtils.calculateWeights(parts);
}
/**
* Recalculate the weights of subareas of the line, based on size change.
* @param line The line being aplitted
* @param weights The weight of each part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param deltaFw The amount of growth towards the origin.
* @param deltaBw The amount of growth towards the infinity.
*/
public static adjustWeights(
[begin, length]: [number, number],
weights: number[],
gap: number,
target: number,
deltaFw: number,
deltaBw: number,
): number[] {
// TODO: configurable min length?
const minLength = 1;
/**
* Recalculate weights of subareas splitting the given area, based on size change.
* @param area The area being splitted
* @param weights The weight of each part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param delta The changes in dimension of the target
* @param horizontal If true, calculate horizontal weights, instead of vertical.
*/
public static adjustAreaWeights(
area: Rect,
weights: number[],
gap: number,
target: number,
delta: RectDelta,
horizontal?: boolean
): number[] {
const line: [number, number] = horizontal
? [area.x, area.width]
: [area.y, area.height];
const [deltaFw, deltaBw] = horizontal
? [delta.east, delta.west]
: [delta.south, delta.north];
return LayoutUtils.adjustWeights(
line,
weights,
gap,
target,
deltaFw,
deltaBw
);
}
const parts = this.splitWeighted([begin, length], weights, gap);
const [targetBase, targetLength] = parts[target];
/**
* Recalculate weights of two areas splitting the given area, based on size change.
* @param area The area being splitted
* @param weight The weight of the left/upper part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param delta The changes in dimension of the target
* @param horizontal If true, calculate horizontal weights, instead of vertical.
*/
public static adjustAreaHalfWeights(
area: Rect,
weight: number,
gap: number,
target: number,
delta: RectDelta,
horizontal?: boolean
): number {
const weights = [weight, 1 - weight];
const newWeights = LayoutUtils.adjustAreaWeights(
area,
weights,
gap,
target,
delta,
horizontal
);
return newWeights[0];
}
/* apply backward delta */
if (target > 0 && deltaBw !== 0) {
const neighbor = target - 1;
const [neighborBase, neighborLength] = parts[neighbor];
/**
* Calculates the weights of all parts, splitting a line.
*/
public static calculateWeights(parts: Array<[number, number]>): number[] {
const totalLength = parts.reduce((acc, [base, length]) => acc + length, 0);
return parts.map(([base, length]) => length / totalLength);
}
/* limit delta to prevent squeezing windows */
const delta = clip(deltaBw,
minLength - targetLength,
neighborLength - minLength,
);
/**
* Calculates the weights of all parts, splitting an area.
*/
public static calculateAreaWeights(
area: Rect,
geometries: Rect[],
gap?: number,
horizontal?: boolean
): number[] {
gap = gap !== undefined ? gap : 0;
horizontal = horizontal !== undefined ? horizontal : false;
parts[target] = [(targetBase - delta), (targetLength + delta)];
parts[neighbor] = [neighborBase, (neighborLength - delta)];
}
/* apply forward delta */
if (target < parts.length - 1 && deltaFw !== 0) {
const neighbor = target + 1;
const [neighborBase, neighborLength] = parts[neighbor];
/* limit delta to prevent squeezing windows */
const delta = clip(deltaFw,
minLength - targetLength,
neighborLength - minLength,
);
parts[target] = [targetBase, targetLength + delta];
parts[neighbor] = [neighborBase + delta, neighborLength - delta];
}
return LayoutUtils.calculateWeights(parts);
}
/**
* Recalculate weights of subareas splitting the given area, based on size change.
* @param area The area being splitted
* @param weights The weight of each part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param delta The changes in dimension of the target
* @param horizontal If true, calculate horizontal weights, instead of vertical.
*/
public static adjustAreaWeights(
area: Rect,
weights: number[],
gap: number,
target: number,
delta: RectDelta,
horizontal?: boolean,
): number[] {
const line: [number, number] = (horizontal) ? [area.x, area.width] : [area.y, area.height];
const [deltaFw, deltaBw] = (horizontal)
? [delta.east, delta.west]
: [delta.south, delta.north]
;
return LayoutUtils.adjustWeights(line, weights, gap, target, deltaFw, deltaBw);
}
/**
* Recalculate weights of two areas splitting the given area, based on size change.
* @param area The area being splitted
* @param weight The weight of the left/upper part
* @param gap The gap size b/w parts
* @param target The index of the part being changed.
* @param delta The changes in dimension of the target
* @param horizontal If true, calculate horizontal weights, instead of vertical.
*/
public static adjustAreaHalfWeights(
area: Rect,
weight: number,
gap: number,
target: number,
delta: RectDelta,
horizontal?: boolean,
): number {
const weights = [weight, 1 - weight];
const newWeights = LayoutUtils.adjustAreaWeights(area, weights, gap, target, delta, horizontal);
return newWeights[0];
}
/**
* Calculates the weights of all parts, splitting a line.
*/
public static calculateWeights(parts: Array<[number, number]>): number[] {
const totalLength = parts.reduce((acc, [base, length]) => acc + length, 0);
return parts.map(([base, length]) => length / totalLength);
}
/**
* Calculates the weights of all parts, splitting an area.
*/
public static calculateAreaWeights(area: Rect, geometries: Rect[], gap?: number, horizontal?: boolean): number[] {
gap = (gap !== undefined) ? gap : 0;
horizontal = (horizontal !== undefined) ? horizontal : false;
const line = (horizontal) ? area.width : area.height;
const parts: Array<[number, number]> = (horizontal)
? geometries.map((geometry) => [geometry.x, geometry.width])
: geometries.map((geometry) => [geometry.y, geometry.height])
;
return LayoutUtils.calculateWeights(parts);
}
const line = horizontal ? area.width : area.height;
const parts: Array<[number, number]> = horizontal
? geometries.map((geometry) => [geometry.x, geometry.width])
: geometries.map((geometry) => [geometry.y, geometry.height]);
return LayoutUtils.calculateWeights(parts);
}
}

View File

@ -19,60 +19,67 @@
// DEALINGS IN THE SOFTWARE.
class MonocleLayout implements ILayout {
public static readonly id = "MonocleLayout";
public readonly description: string = "Monocle";
public static readonly id = "MonocleLayout";
public readonly description: string = "Monocle";
public readonly classID = MonocleLayout.id;
public readonly classID = MonocleLayout.id;
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tile) => {
tile.state = (CONFIG.monocleMaximize)
? WindowState.Maximized
: WindowState.Tiled;
tile.geometry = area;
});
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tile) => {
tile.state = CONFIG.monocleMaximize
? WindowState.Maximized
: WindowState.Tiled;
tile.geometry = area;
});
/* KWin-specific `monocleMinimizeRest` option */
if (ctx.backend === KWinDriver.backendName && KWINCONFIG.monocleMinimizeRest) {
const tiles = [...tileables];
ctx.setTimeout(() => {
const current = ctx.currentWindow;
if (current && current.tiled) {
tiles.forEach((window) => {
if (window !== current)
(window.window as KWinWindow).client.minimized = true;
});
}
}, 50);
/* KWin-specific `monocleMinimizeRest` option */
if (
ctx.backend === KWinDriver.backendName &&
KWINCONFIG.monocleMinimizeRest
) {
const tiles = [...tileables];
ctx.setTimeout(() => {
const current = ctx.currentWindow;
if (current && current.tiled) {
tiles.forEach((window) => {
if (window !== current)
(window.window as KWinWindow).client.minimized = true;
});
}
}, 50);
}
}
public clone(): this {
/* fake clone */
return this;
}
public clone(): this {
/* fake clone */
return this;
}
public handleShortcut(ctx: EngineContext, input: Shortcut, data?: any): boolean {
switch (input) {
case Shortcut.Up:
case Shortcut.FocusUp:
case Shortcut.Left:
case Shortcut.FocusLeft:
ctx.cycleFocus(-1);
return true;
case Shortcut.Down:
case Shortcut.FocusDown:
case Shortcut.Right:
case Shortcut.FocusRight:
ctx.cycleFocus(1);
return true;
default:
return false;
}
public handleShortcut(
ctx: EngineContext,
input: Shortcut,
data?: any
): boolean {
switch (input) {
case Shortcut.Up:
case Shortcut.FocusUp:
case Shortcut.Left:
case Shortcut.FocusLeft:
ctx.cycleFocus(-1);
return true;
case Shortcut.Down:
case Shortcut.FocusDown:
case Shortcut.Right:
case Shortcut.FocusRight:
ctx.cycleFocus(1);
return true;
default:
return false;
}
}
public toString(): string {
return "MonocleLayout()";
}
public toString(): string {
return "MonocleLayout()";
}
}

View File

@ -19,116 +19,179 @@
// DEALINGS IN THE SOFTWARE.
class QuarterLayout implements ILayout {
public static readonly MAX_PROPORTION = 0.8;
public static readonly id = "QuarterLayout";
public static readonly MAX_PROPORTION = 0.8;
public static readonly id = "QuarterLayout";
public readonly classID = QuarterLayout.id;
public readonly description = "Quarter";
public readonly classID = QuarterLayout.id;
public readonly description = "Quarter";
public get capacity(): number {
return 4;
public get capacity(): number {
return 4;
}
private lhsplit: number;
private rhsplit: number;
private vsplit: number;
public constructor() {
this.lhsplit = 0.5;
this.rhsplit = 0.5;
this.vsplit = 0.5;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta) {
if (tiles.length <= 1 || tiles.length > 4) return;
const idx = tiles.indexOf(basis);
if (idx < 0) return;
/* vertical split */
if ((idx === 0 || idx === 3) && delta.east !== 0)
this.vsplit =
(Math.floor(area.width * this.vsplit) + delta.east) / area.width;
else if ((idx === 1 || idx === 2) && delta.west !== 0)
this.vsplit =
(Math.floor(area.width * this.vsplit) - delta.west) / area.width;
/* left-side horizontal split */
if (tiles.length === 4) {
if (idx === 0 && delta.south !== 0)
this.lhsplit =
(Math.floor(area.height * this.lhsplit) + delta.south) / area.height;
if (idx === 3 && delta.north !== 0)
this.lhsplit =
(Math.floor(area.height * this.lhsplit) - delta.north) / area.height;
}
private lhsplit: number;
private rhsplit: number;
private vsplit: number;
public constructor() {
this.lhsplit = 0.5;
this.rhsplit = 0.5;
this.vsplit = 0.5;
/* right-side horizontal split */
if (tiles.length >= 3) {
if (idx === 1 && delta.south !== 0)
this.rhsplit =
(Math.floor(area.height * this.rhsplit) + delta.south) / area.height;
if (idx === 2 && delta.north !== 0)
this.rhsplit =
(Math.floor(area.height * this.rhsplit) - delta.north) / area.height;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta) {
if (tiles.length <= 1 || tiles.length > 4)
return;
/* clipping */
this.vsplit = clip(
this.vsplit,
1 - QuarterLayout.MAX_PROPORTION,
QuarterLayout.MAX_PROPORTION
);
this.lhsplit = clip(
this.lhsplit,
1 - QuarterLayout.MAX_PROPORTION,
QuarterLayout.MAX_PROPORTION
);
this.rhsplit = clip(
this.rhsplit,
1 - QuarterLayout.MAX_PROPORTION,
QuarterLayout.MAX_PROPORTION
);
}
const idx = tiles.indexOf(basis);
if (idx < 0)
return;
public clone(): ILayout {
const other = new QuarterLayout();
other.lhsplit = this.lhsplit;
other.rhsplit = this.rhsplit;
other.vsplit = this.vsplit;
return other;
}
/* vertical split */
if ((idx === 0 || idx === 3) && delta.east !== 0)
this.vsplit = (Math.floor(area.width * this.vsplit) + delta.east) / area.width;
else if ((idx === 1 || idx === 2) && delta.west !== 0)
this.vsplit = (Math.floor(area.width * this.vsplit) - delta.west) / area.width;
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
for (let i = 0; i < 4 && i < tileables.length; i++)
tileables[i].state = WindowState.Tiled;
/* left-side horizontal split */
if (tiles.length === 4) {
if (idx === 0 && delta.south !== 0)
this.lhsplit = (Math.floor(area.height * this.lhsplit) + delta.south) / area.height;
if (idx === 3 && delta.north !== 0)
this.lhsplit = (Math.floor(area.height * this.lhsplit) - delta.north) / area.height;
}
if (tileables.length > 4)
tileables
.slice(4)
.forEach((tile) => (tile.state = WindowState.TiledAfloat));
/* right-side horizontal split */
if (tiles.length >= 3) {
if (idx === 1 && delta.south !== 0)
this.rhsplit = (Math.floor(area.height * this.rhsplit) + delta.south) / area.height;
if (idx === 2 && delta.north !== 0)
this.rhsplit = (Math.floor(area.height * this.rhsplit) - delta.north) / area.height;
}
/* clipping */
this.vsplit = clip(this.vsplit, 1 - QuarterLayout.MAX_PROPORTION, QuarterLayout.MAX_PROPORTION);
this.lhsplit = clip(this.lhsplit, 1 - QuarterLayout.MAX_PROPORTION, QuarterLayout.MAX_PROPORTION);
this.rhsplit = clip(this.rhsplit, 1 - QuarterLayout.MAX_PROPORTION, QuarterLayout.MAX_PROPORTION);
if (tileables.length === 1) {
tileables[0].geometry = area;
return;
}
public clone(): ILayout {
const other = new QuarterLayout();
other.lhsplit = this.lhsplit;
other.rhsplit = this.rhsplit;
other.vsplit = this.vsplit;
return other;
const gap1 = Math.floor(CONFIG.tileLayoutGap / 2);
const gap2 = CONFIG.tileLayoutGap - gap1;
const leftWidth = Math.floor(area.width * this.vsplit);
const rightWidth = area.width - leftWidth;
const rightX = area.x + leftWidth;
if (tileables.length === 2) {
tileables[0].geometry = new Rect(
area.x,
area.y,
leftWidth,
area.height
).gap(0, gap1, 0, 0);
tileables[1].geometry = new Rect(
rightX,
area.y,
rightWidth,
area.height
).gap(gap2, 0, 0, 0);
return;
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
for (let i = 0; i < 4 && i < tileables.length; i++)
tileables[i].state = WindowState.Tiled;
if (tileables.length > 4)
tileables.slice(4).forEach((tile) => tile.state = WindowState.TiledAfloat);
if (tileables.length === 1) {
tileables[0].geometry = area;
return;
}
const gap1 = Math.floor(CONFIG.tileLayoutGap / 2);
const gap2 = CONFIG.tileLayoutGap - gap1;
const leftWidth = Math.floor(area.width * this.vsplit);
const rightWidth = area.width - leftWidth;
const rightX = area.x + leftWidth;
if (tileables.length === 2) {
tileables[0].geometry = new Rect(area.x, area.y, leftWidth , area.height).gap(0, gap1, 0, 0);
tileables[1].geometry = new Rect(rightX, area.y, rightWidth, area.height).gap(gap2, 0, 0, 0);
return;
}
const rightTopHeight = Math.floor(area.height * this.rhsplit);
const rightBottomHeight = area.height - rightTopHeight;
const rightBottomY = area.y + rightTopHeight;
if (tileables.length === 3) {
tileables[0].geometry = new Rect(area.x, area.y , leftWidth , area.height ).gap(0, gap1, 0, 0);
tileables[1].geometry = new Rect(rightX, area.y , rightWidth, rightTopHeight ).gap(gap2, 0, 0, gap1);
tileables[2].geometry = new Rect(rightX, rightBottomY, rightWidth, rightBottomHeight).gap(gap2, 0, gap2, 0);
return;
}
const leftTopHeight = Math.floor(area.height * this.lhsplit);
const leftBottomHeight = area.height - leftTopHeight;
const leftBottomY = area.y + leftTopHeight;
if (tileables.length >= 4) {
tileables[0].geometry = new Rect(area.x, area.y , leftWidth , leftTopHeight ).gap(0, gap1, 0, gap1);
tileables[1].geometry = new Rect(rightX, area.y , rightWidth, rightTopHeight ).gap(gap2, 0, 0, gap1);
tileables[2].geometry = new Rect(rightX, rightBottomY, rightWidth, rightBottomHeight).gap(gap2, 0, gap2, 0);
tileables[3].geometry = new Rect(area.x, leftBottomY , leftWidth , leftBottomHeight ).gap(0, gap2, gap2, 0);
}
const rightTopHeight = Math.floor(area.height * this.rhsplit);
const rightBottomHeight = area.height - rightTopHeight;
const rightBottomY = area.y + rightTopHeight;
if (tileables.length === 3) {
tileables[0].geometry = new Rect(
area.x,
area.y,
leftWidth,
area.height
).gap(0, gap1, 0, 0);
tileables[1].geometry = new Rect(
rightX,
area.y,
rightWidth,
rightTopHeight
).gap(gap2, 0, 0, gap1);
tileables[2].geometry = new Rect(
rightX,
rightBottomY,
rightWidth,
rightBottomHeight
).gap(gap2, 0, gap2, 0);
return;
}
public toString(): string {
return "QuarterLayout()";
const leftTopHeight = Math.floor(area.height * this.lhsplit);
const leftBottomHeight = area.height - leftTopHeight;
const leftBottomY = area.y + leftTopHeight;
if (tileables.length >= 4) {
tileables[0].geometry = new Rect(
area.x,
area.y,
leftWidth,
leftTopHeight
).gap(0, gap1, 0, gap1);
tileables[1].geometry = new Rect(
rightX,
area.y,
rightWidth,
rightTopHeight
).gap(gap2, 0, 0, gap1);
tileables[2].geometry = new Rect(
rightX,
rightBottomY,
rightWidth,
rightBottomHeight
).gap(gap2, 0, gap2, 0);
tileables[3].geometry = new Rect(
area.x,
leftBottomY,
leftWidth,
leftBottomHeight
).gap(0, gap2, gap2, 0);
}
}
public toString(): string {
return "QuarterLayout()";
}
}

View File

@ -18,70 +18,85 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
type SpiralLayoutPart = HalfSplitLayoutPart<FillLayoutPart, SpiralLayoutPart | FillLayoutPart>;
type SpiralLayoutPart = HalfSplitLayoutPart<
FillLayoutPart,
SpiralLayoutPart | FillLayoutPart
>;
class SpiralLayout implements ILayout {
public readonly description = "Spiral";
public readonly description = "Spiral";
private depth: number;
private parts: SpiralLayoutPart;
private depth: number;
private parts: SpiralLayoutPart;
constructor() {
this.depth = 1;
this.parts = new HalfSplitLayoutPart(new FillLayoutPart(), new FillLayoutPart());
this.parts.angle = 0;
this.parts.gap = CONFIG.tileLayoutGap;
constructor() {
this.depth = 1;
this.parts = new HalfSplitLayoutPart(
new FillLayoutPart(),
new FillLayoutPart()
);
this.parts.angle = 0;
this.parts.gap = CONFIG.tileLayoutGap;
}
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): void {
this.parts.adjust(area, tiles, basis, delta);
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
this.bore(tileables.length);
this.parts.apply(area, tileables).forEach((geometry, i) => {
tileables[i].geometry = geometry;
});
}
//handleShortcut?(ctx: EngineContext, input: Shortcut, data?: any): boolean;
public toString(): string {
return "Spiral()";
}
private bore(depth: number): void {
if (this.depth >= depth) return;
let hpart = this.parts;
let i;
for (i = 0; i < this.depth - 1; i++) {
hpart = hpart.secondary as SpiralLayoutPart;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): void {
this.parts.adjust(area, tiles, basis, delta);
const lastFillPart = hpart.secondary as FillLayoutPart;
let npart: SpiralLayoutPart;
while (i < depth - 1) {
npart = new HalfSplitLayoutPart(new FillLayoutPart(), lastFillPart);
npart.gap = CONFIG.tileLayoutGap;
switch ((i + 1) % 4) {
case 0:
npart.angle = 0;
break;
case 1:
npart.angle = 90;
break;
case 2:
npart.angle = 180;
break;
case 3:
npart.angle = 270;
break;
}
hpart.secondary = npart;
hpart = npart;
i++;
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach((tileable) =>
tileable.state = WindowState.Tiled);
this.bore(tileables.length);
this.parts.apply(area, tileables)
.forEach((geometry, i) => {
tileables[i].geometry = geometry;
});
}
//handleShortcut?(ctx: EngineContext, input: Shortcut, data?: any): boolean;
public toString(): string {
return "Spiral()";
}
private bore(depth: number): void {
if (this.depth >= depth)
return;
let hpart = this.parts;
let i;
for(i = 0; i < this.depth - 1; i ++) {
hpart = hpart.secondary as SpiralLayoutPart;
}
const lastFillPart = hpart.secondary as FillLayoutPart;
let npart: SpiralLayoutPart;
while (i < depth - 1) {
npart = new HalfSplitLayoutPart(new FillLayoutPart(), lastFillPart);
npart.gap = CONFIG.tileLayoutGap;
switch((i + 1) % 4) {
case 0: npart.angle = 0; break;
case 1: npart.angle = 90; break;
case 2: npart.angle = 180; break;
case 3: npart.angle = 270; break;
}
hpart.secondary = npart;
hpart = npart;
i ++;
}
this.depth = depth;
}
}
this.depth = depth;
}
}

View File

@ -19,65 +19,65 @@
// DEALINGS IN THE SOFTWARE.
class SpreadLayout implements ILayout {
public static readonly id = "SpreadLayout";
public static readonly id = "SpreadLayout";
public readonly classID = SpreadLayout.id;
public readonly description = "Spread";
public readonly classID = SpreadLayout.id;
public readonly description = "Spread";
private space: number; /* in ratio */
private space: number; /* in ratio */
constructor() {
this.space = 0.07;
constructor() {
this.space = 0.07;
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
const tiles = tileables;
let numTiles = tiles.length;
const spaceWidth = Math.floor(area.width * this.space);
let cardWidth = area.width - spaceWidth * (numTiles - 1);
// TODO: define arbitrary constants
const miniumCardWidth = area.width * 0.4;
while (cardWidth < miniumCardWidth) {
cardWidth += spaceWidth;
numTiles -= 1;
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => tileable.state = WindowState.Tiled);
const tiles = tileables;
for (let i = 0; i < tiles.length; i++)
tiles[i].geometry = new Rect(
area.x + (i < numTiles ? spaceWidth * (numTiles - i - 1) : 0),
area.y,
cardWidth,
area.height
);
}
let numTiles = tiles.length;
const spaceWidth = Math.floor(area.width * this.space);
let cardWidth = area.width - (spaceWidth * (numTiles - 1));
public clone(): ILayout {
const other = new SpreadLayout();
other.space = this.space;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Decrease:
// TODO: define arbitrary constants
const miniumCardWidth = area.width * 0.40;
while (cardWidth < miniumCardWidth) {
cardWidth += spaceWidth;
numTiles -= 1;
}
for (let i = 0; i < tiles.length; i++)
tiles[i].geometry = new Rect(
area.x + ((i < numTiles) ? spaceWidth * (numTiles - i - 1) : 0),
area.y,
cardWidth,
area.height,
);
this.space = Math.max(0.04, this.space - 0.01);
break;
case Shortcut.Increase:
// TODO: define arbitrary constants
this.space = Math.min(0.1, this.space + 0.01);
break;
default:
return false;
}
return true;
}
public clone(): ILayout {
const other = new SpreadLayout();
other.space = this.space;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Decrease:
// TODO: define arbitrary constants
this.space = Math.max(0.04, this.space - 0.01);
break;
case Shortcut.Increase:
// TODO: define arbitrary constants
this.space = Math.min(0.10, this.space + 0.01);
break;
default:
return false;
}
return true;
}
public toString(): string {
return "SpreadLayout(" + this.space + ")";
}
public toString(): string {
return "SpreadLayout(" + this.space + ")";
}
}

View File

@ -19,62 +19,62 @@
// DEALINGS IN THE SOFTWARE.
class StairLayout implements ILayout {
public static readonly id = "StairLayout";
public static readonly id = "StairLayout";
public readonly classID = StairLayout.id;
public readonly description = "Stair";
public readonly classID = StairLayout.id;
public readonly description = "Stair";
private space: number; /* in PIXELS */
private space: number; /* in PIXELS */
constructor() {
this.space = 24;
constructor() {
this.space = 24;
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
const tiles = tileables;
const len = tiles.length;
const space = this.space;
// TODO: limit the maximum number of staired windows.
for (let i = 0; i < len; i++) {
const dx = space * (len - i - 1);
const dy = space * i;
tiles[i].geometry = new Rect(
area.x + dx,
area.y + dy,
area.width - dx,
area.height - dy
);
}
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => tileable.state = WindowState.Tiled);
const tiles = tileables;
public clone(): ILayout {
const other = new StairLayout();
other.space = this.space;
return other;
}
const len = tiles.length;
const space = this.space;
// TODO: limit the maximum number of staired windows.
for (let i = 0; i < len; i++) {
const dx = space * (len - i - 1);
const dy = space * i;
tiles[i].geometry = new Rect(
area.x + dx,
area.y + dy,
area.width - dx,
area.height - dy,
);
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Decrease:
// TODO: define arbitrary constants
this.space = Math.max(16, this.space - 8);
break;
case Shortcut.Increase:
// TODO: define arbitrary constants
this.space = Math.min(160, this.space + 8);
break;
default:
return false;
}
return true;
}
public clone(): ILayout {
const other = new StairLayout();
other.space = this.space;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Decrease:
// TODO: define arbitrary constants
this.space = Math.max(16, this.space - 8);
break;
case Shortcut.Increase:
// TODO: define arbitrary constants
this.space = Math.min(160, this.space + 8);
break;
default:
return false;
}
return true;
}
public toString(): string {
return "StairLayout(" + this.space + ")";
}
public toString(): string {
return "StairLayout(" + this.space + ")";
}
}

View File

@ -19,192 +19,211 @@
// DEALINGS IN THE SOFTWARE.
class ThreeColumnLayout implements ILayout {
public static readonly MIN_MASTER_RATIO = 0.2;
public static readonly MAX_MASTER_RATIO = 0.75;
public static readonly id = "ThreeColumnLayout";
public static readonly MIN_MASTER_RATIO = 0.2;
public static readonly MAX_MASTER_RATIO = 0.75;
public static readonly id = "ThreeColumnLayout";
public readonly classID = ThreeColumnLayout.id;
public readonly classID = ThreeColumnLayout.id;
public get description(): string {
return "Three-Column [" + (this.masterSize) + "]";
public get description(): string {
return "Three-Column [" + this.masterSize + "]";
}
private masterRatio: number;
private masterSize: number;
constructor() {
this.masterRatio = 0.6;
this.masterSize = 1;
}
public adjust(
area: Rect,
tiles: Window[],
basis: Window,
delta: RectDelta
): void {
const basisIndex = tiles.indexOf(basis);
if (basisIndex < 0) return;
if (tiles.length === 0)
/* no tiles */
return;
else if (tiles.length <= this.masterSize) {
/* one column */
LayoutUtils.adjustAreaWeights(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
tiles.indexOf(basis),
delta
).forEach((newWeight, i) => (tiles[i].weight = newWeight * tiles.length));
} else if (tiles.length === this.masterSize + 1) {
/* two columns */
/* adjust master-stack ratio */
this.masterRatio = LayoutUtils.adjustAreaHalfWeights(
area,
this.masterRatio,
CONFIG.tileLayoutGap,
basisIndex < this.masterSize ? 0 : 1,
delta,
true
);
/* adjust master tile weights */
if (basisIndex < this.masterSize) {
const masterTiles = tiles.slice(0, -1);
LayoutUtils.adjustAreaWeights(
area,
masterTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
basisIndex,
delta
).forEach(
(newWeight, i) =>
(masterTiles[i].weight = newWeight * masterTiles.length)
);
}
} else if (tiles.length > this.masterSize + 1) {
/* three columns */
let basisGroup;
if (basisIndex < this.masterSize) basisGroup = 1;
/* master */ else if (
basisIndex < Math.floor((this.masterSize + tiles.length) / 2)
)
basisGroup = 2;
/* R-stack */ else basisGroup = 0; /* L-stack */
/* adjust master-stack ratio */
const stackRatio = 1 - this.masterRatio;
const newRatios = LayoutUtils.adjustAreaWeights(
area,
[stackRatio, this.masterRatio, stackRatio],
CONFIG.tileLayoutGap,
basisGroup,
delta,
true
);
const newMasterRatio = newRatios[1];
const newStackRatio = basisGroup === 0 ? newRatios[0] : newRatios[2];
this.masterRatio = newMasterRatio / (newMasterRatio + newStackRatio);
/* adjust tile weight */
const rstackNumTile = Math.floor((tiles.length - this.masterSize) / 2);
const [masterTiles, rstackTiles, lstackTiles] =
partitionArrayBySizes<Window>(tiles, [this.masterSize, rstackNumTile]);
const groupTiles = [lstackTiles, masterTiles, rstackTiles][basisGroup];
LayoutUtils.adjustAreaWeights(
area /* we only need height */,
groupTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
groupTiles.indexOf(basis),
delta
).forEach(
(newWeight, i) => (groupTiles[i].weight = newWeight * groupTiles.length)
);
}
}
private masterRatio: number;
private masterSize: number;
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
const tiles = tileables;
constructor() {
this.masterRatio = 0.6;
this.masterSize = 1;
if (tiles.length <= this.masterSize) {
/* only master */
LayoutUtils.splitAreaWeighted(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap
).forEach((tileArea, i) => (tiles[i].geometry = tileArea));
} else if (tiles.length === this.masterSize + 1) {
/* master & R-stack (only 1 window in stack) */
const [masterArea, stackArea] = LayoutUtils.splitAreaHalfWeighted(
area,
this.masterRatio,
CONFIG.tileLayoutGap,
true
);
const masterTiles = tiles.slice(0, this.masterSize);
LayoutUtils.splitAreaWeighted(
masterArea,
masterTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap
).forEach((tileArea, i) => (masterTiles[i].geometry = tileArea));
tiles[tiles.length - 1].geometry = stackArea;
} else if (tiles.length > this.masterSize + 1) {
/* L-stack & master & R-stack */
const stackRatio = 1 - this.masterRatio;
/** Areas allocated to L-stack, master, and R-stack */
const groupAreas = LayoutUtils.splitAreaWeighted(
area,
[stackRatio, this.masterRatio, stackRatio],
CONFIG.tileLayoutGap,
true
);
const rstackSize = Math.floor((tiles.length - this.masterSize) / 2);
const [masterTiles, rstackTiles, lstackTiles] =
partitionArrayBySizes<Window>(tiles, [this.masterSize, rstackSize]);
[lstackTiles, masterTiles, rstackTiles].forEach((groupTiles, group) => {
LayoutUtils.splitAreaWeighted(
groupAreas[group],
groupTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap
).forEach((tileArea, i) => (groupTiles[i].geometry = tileArea));
});
}
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta): void {
const basisIndex = tiles.indexOf(basis);
if (basisIndex < 0)
return;
public clone(): ILayout {
const other = new ThreeColumnLayout();
other.masterRatio = this.masterRatio;
other.masterSize = this.masterSize;
return other;
}
if (tiles.length === 0)
/* no tiles */
return;
else if (tiles.length <= this.masterSize) {
/* one column */
LayoutUtils.adjustAreaWeights(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
tiles.indexOf(basis),
delta,
).forEach((newWeight, i) =>
tiles[i].weight = newWeight * tiles.length);
} else if (tiles.length === this.masterSize + 1) {
/* two columns */
/* adjust master-stack ratio */
this.masterRatio = LayoutUtils.adjustAreaHalfWeights(
area,
this.masterRatio,
CONFIG.tileLayoutGap,
(basisIndex < this.masterSize) ? 0 : 1,
delta,
true);
/* adjust master tile weights */
if (basisIndex < this.masterSize) {
const masterTiles = tiles.slice(0, -1);
LayoutUtils.adjustAreaWeights(
area,
masterTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
basisIndex,
delta,
).forEach((newWeight, i) =>
masterTiles[i].weight = newWeight * masterTiles.length);
}
} else if (tiles.length > this.masterSize + 1) {
/* three columns */
let basisGroup;
if (basisIndex < this.masterSize)
basisGroup = 1; /* master */
else if (basisIndex < Math.floor((this.masterSize + tiles.length) / 2))
basisGroup = 2; /* R-stack */
else
basisGroup = 0; /* L-stack */
/* adjust master-stack ratio */
const stackRatio = 1 - this.masterRatio;
const newRatios = LayoutUtils.adjustAreaWeights(
area,
[stackRatio, this.masterRatio, stackRatio],
CONFIG.tileLayoutGap,
basisGroup,
delta,
true);
const newMasterRatio = newRatios[1];
const newStackRatio = (basisGroup === 0) ? newRatios[0] : newRatios[2];
this.masterRatio = newMasterRatio / (newMasterRatio + newStackRatio);
/* adjust tile weight */
const rstackNumTile = Math.floor((tiles.length - this.masterSize) / 2);
const [masterTiles, rstackTiles, lstackTiles] =
partitionArrayBySizes<Window>(tiles, [this.masterSize, rstackNumTile]);
const groupTiles = [lstackTiles, masterTiles, rstackTiles][basisGroup];
LayoutUtils.adjustAreaWeights(
area, /* we only need height */
groupTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap,
groupTiles.indexOf(basis),
delta)
.forEach((newWeight, i) =>
groupTiles[i].weight = newWeight * groupTiles.length);
}
public handleShortcut(
ctx: EngineContext,
input: Shortcut,
data?: any
): boolean {
switch (input) {
case Shortcut.Increase:
this.resizeMaster(ctx, +1);
return true;
case Shortcut.Decrease:
this.resizeMaster(ctx, -1);
return true;
case Shortcut.Left:
this.masterRatio = clip(
slide(this.masterRatio, -0.05),
ThreeColumnLayout.MIN_MASTER_RATIO,
ThreeColumnLayout.MAX_MASTER_RATIO
);
return true;
case Shortcut.Right:
this.masterRatio = clip(
slide(this.masterRatio, +0.05),
ThreeColumnLayout.MIN_MASTER_RATIO,
ThreeColumnLayout.MAX_MASTER_RATIO
);
return true;
default:
return false;
}
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
/* Tile all tileables */
tileables.forEach((tileable) => tileable.state = WindowState.Tiled);
const tiles = tileables;
public toString(): string {
return "ThreeColumnLayout(nmaster=" + this.masterSize + ")";
}
if (tiles.length <= this.masterSize) {
/* only master */
LayoutUtils.splitAreaWeighted(
area,
tiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap)
.forEach((tileArea, i) =>
tiles[i].geometry = tileArea);
} else if (tiles.length === this.masterSize + 1) {
/* master & R-stack (only 1 window in stack) */
const [masterArea, stackArea] = LayoutUtils.splitAreaHalfWeighted(
area, this.masterRatio, CONFIG.tileLayoutGap, true);
const masterTiles = tiles.slice(0, this.masterSize);
LayoutUtils.splitAreaWeighted(
masterArea,
masterTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap)
.forEach((tileArea, i) =>
masterTiles[i].geometry = tileArea);
tiles[tiles.length - 1].geometry = stackArea;
} else if (tiles.length > this.masterSize + 1) {
/* L-stack & master & R-stack */
const stackRatio = 1 - this.masterRatio;
/** Areas allocated to L-stack, master, and R-stack */
const groupAreas = LayoutUtils.splitAreaWeighted(
area,
[stackRatio, this.masterRatio, stackRatio],
CONFIG.tileLayoutGap,
true);
const rstackSize = Math.floor((tiles.length - this.masterSize) / 2);
const [masterTiles, rstackTiles, lstackTiles] =
partitionArrayBySizes<Window>(tiles, [this.masterSize, rstackSize]);
[lstackTiles, masterTiles, rstackTiles].forEach((groupTiles, group) => {
LayoutUtils.splitAreaWeighted(
groupAreas[group],
groupTiles.map((tile) => tile.weight),
CONFIG.tileLayoutGap)
.forEach((tileArea, i) =>
groupTiles[i].geometry = tileArea);
});
}
}
public clone(): ILayout {
const other = new ThreeColumnLayout();
other.masterRatio = this.masterRatio;
other.masterSize = this.masterSize;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut, data?: any): boolean {
switch (input) {
case Shortcut.Increase: this.resizeMaster(ctx, +1); return true;
case Shortcut.Decrease: this.resizeMaster(ctx, -1); return true;
case Shortcut.Left:
this.masterRatio = clip(
slide(this.masterRatio, -0.05),
ThreeColumnLayout.MIN_MASTER_RATIO,
ThreeColumnLayout.MAX_MASTER_RATIO);
return true;
case Shortcut.Right:
this.masterRatio = clip(
slide(this.masterRatio, +0.05),
ThreeColumnLayout.MIN_MASTER_RATIO,
ThreeColumnLayout.MAX_MASTER_RATIO);
return true;
default:
return false;
}
}
public toString(): string {
return "ThreeColumnLayout(nmaster=" + this.masterSize + ")";
}
private resizeMaster(ctx: EngineContext, step: -1 | 1): void {
this.masterSize = clip(this.masterSize + step,
1, 10);
ctx.showNotification(this.description);
}
private resizeMaster(ctx: EngineContext, step: -1 | 1): void {
this.masterSize = clip(this.masterSize + step, 1, 10);
ctx.showNotification(this.description);
}
}

View File

@ -19,111 +19,114 @@
// DEALINGS IN THE SOFTWARE.
class TileLayout implements ILayout {
public static readonly MIN_MASTER_RATIO = 0.2;
public static readonly MAX_MASTER_RATIO = 0.8;
public static readonly id = "TileLayout";
public static readonly MIN_MASTER_RATIO = 0.2;
public static readonly MAX_MASTER_RATIO = 0.8;
public static readonly id = "TileLayout";
public readonly classID = TileLayout.id;
public readonly classID = TileLayout.id;
public get description(): string {
return "Tile [" + this.numMaster + "]";
}
public get description(): string {
return "Tile [" + this.numMaster + "]";
}
private parts: (
RotateLayoutPart<
HalfSplitLayoutPart<
RotateLayoutPart<StackLayoutPart>,
StackLayoutPart
>>
private parts: RotateLayoutPart<
HalfSplitLayoutPart<RotateLayoutPart<StackLayoutPart>, StackLayoutPart>
>;
private get numMaster(): number {
return this.parts.inner.primarySize;
}
private set numMaster(value: number) {
this.parts.inner.primarySize = value;
}
private get masterRatio(): number {
return this.parts.inner.ratio;
}
private set masterRatio(value: number) {
this.parts.inner.ratio = value;
}
constructor() {
this.parts = new RotateLayoutPart(
new HalfSplitLayoutPart(
new RotateLayoutPart(new StackLayoutPart()),
new StackLayoutPart()
)
);
private get numMaster(): number {
return this.parts.inner.primarySize;
}
private set numMaster(value: number) {
this.parts.inner.primarySize = value;
}
const masterPart = this.parts.inner;
masterPart.gap =
masterPart.primary.inner.gap =
masterPart.secondary.gap =
CONFIG.tileLayoutGap;
}
private get masterRatio(): number {
return this.parts.inner.ratio;
}
private set masterRatio(value: number) {
this.parts.inner.ratio = value;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta) {
this.parts.adjust(area, tiles, basis, delta);
}
constructor() {
this.parts = new RotateLayoutPart(new HalfSplitLayoutPart(
new RotateLayoutPart(new StackLayoutPart()),
new StackLayoutPart(),
));
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach((tileable) => (tileable.state = WindowState.Tiled));
const masterPart = this.parts.inner;
masterPart.gap =
masterPart.primary.inner.gap =
masterPart.secondary.gap = CONFIG.tileLayoutGap;
this.parts.apply(area, tileables).forEach((geometry, i) => {
tileables[i].geometry = geometry;
});
}
public clone(): ILayout {
const other = new TileLayout();
other.masterRatio = this.masterRatio;
other.numMaster = this.numMaster;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Left:
this.masterRatio = clip(
slide(this.masterRatio, -0.05),
TileLayout.MIN_MASTER_RATIO,
TileLayout.MAX_MASTER_RATIO
);
break;
case Shortcut.Right:
this.masterRatio = clip(
slide(this.masterRatio, +0.05),
TileLayout.MIN_MASTER_RATIO,
TileLayout.MAX_MASTER_RATIO
);
break;
case Shortcut.Increase:
// TODO: define arbitrary constant
if (this.numMaster < 10) this.numMaster += 1;
ctx.showNotification(this.description);
break;
case Shortcut.Decrease:
if (this.numMaster > 0) this.numMaster -= 1;
ctx.showNotification(this.description);
break;
case Shortcut.Rotate:
this.parts.rotate(90);
break;
case Shortcut.RotatePart:
this.parts.inner.primary.rotate(90);
break;
default:
return false;
}
return true;
}
public adjust(area: Rect, tiles: Window[], basis: Window, delta: RectDelta) {
this.parts.adjust(area, tiles, basis, delta);
}
public apply(ctx: EngineContext, tileables: Window[], area: Rect): void {
tileables.forEach((tileable) =>
tileable.state = WindowState.Tiled);
this.parts.apply(area, tileables)
.forEach((geometry, i) => {
tileables[i].geometry = geometry;
});
}
public clone(): ILayout {
const other = new TileLayout();
other.masterRatio = this.masterRatio;
other.numMaster = this.numMaster;
return other;
}
public handleShortcut(ctx: EngineContext, input: Shortcut) {
switch (input) {
case Shortcut.Left:
this.masterRatio = clip(
slide(this.masterRatio, -0.05),
TileLayout.MIN_MASTER_RATIO,
TileLayout.MAX_MASTER_RATIO);
break;
case Shortcut.Right:
this.masterRatio = clip(
slide(this.masterRatio, +0.05),
TileLayout.MIN_MASTER_RATIO,
TileLayout.MAX_MASTER_RATIO);
break;
case Shortcut.Increase:
// TODO: define arbitrary constant
if (this.numMaster < 10)
this.numMaster += 1;
ctx.showNotification(this.description);
break;
case Shortcut.Decrease:
if (this.numMaster > 0)
this.numMaster -= 1;
ctx.showNotification(this.description);
break;
case Shortcut.Rotate:
this.parts.rotate(90);
break;
case Shortcut.RotatePart:
this.parts.inner.primary.rotate(90);
break;
default:
return false;
}
return true;
}
public toString(): string {
return "TileLayout(nmaster=" + this.numMaster + ", ratio=" + this.masterRatio + ")";
}
public toString(): string {
return (
"TileLayout(nmaster=" +
this.numMaster +
", ratio=" +
this.masterRatio +
")"
);
}
}

View File

@ -19,25 +19,24 @@
// DEALINGS IN THE SOFTWARE.
const DEBUG = {
enabled: false,
started: new Date().getTime(),
enabled: false,
started: new Date().getTime(),
};
function debug(f: () => any) {
if (DEBUG.enabled) {
const timestamp = (new Date().getTime() - DEBUG.started) / 1000;
console.log("[" + timestamp + "]", f()); // tslint:disable-line:no-console
}
if (DEBUG.enabled) {
const timestamp = (new Date().getTime() - DEBUG.started) / 1000;
console.log("[" + timestamp + "]", f()); // tslint:disable-line:no-console
}
}
function debugObj(f: () => [string, any]) {
if (DEBUG.enabled) {
const timestamp = (new Date().getTime() - DEBUG.started) / 1000;
const [name, obj] = f();
const buf = [];
for (const i in obj)
buf.push(i + "=" + obj[i]);
if (DEBUG.enabled) {
const timestamp = (new Date().getTime() - DEBUG.started) / 1000;
const [name, obj] = f();
const buf = [];
for (const i in obj) buf.push(i + "=" + obj[i]);
console.log("[" + timestamp + "]", name + ": " + buf.join(" ")); // tslint:disable-line:no-console
}
console.log("[" + timestamp + "]", name + ": " + buf.join(" ")); // tslint:disable-line:no-console
}
}

View File

@ -19,33 +19,27 @@
// DEALINGS IN THE SOFTWARE.
function clip(value: number, min: number, max: number): number {
if (value < min)
return min;
if (value > max)
return max;
return value;
if (value < min) return min;
if (value > max) return max;
return value;
}
function slide(value: number, step: number): number {
if (step === 0)
return value;
return Math.floor(value / step + 1.000001) * step;
if (step === 0) return value;
return Math.floor(value / step + 1.000001) * step;
}
function matchWords(str: string, words: string[]): number {
for (let i = 0; i < words.length; i++) {
if (str.indexOf(words[i]) >= 0)
return i;
}
return -1;
for (let i = 0; i < words.length; i++) {
if (str.indexOf(words[i]) >= 0) return i;
}
return -1;
}
function wrapIndex(index: number, length: number): number {
if (index < 0)
return index + length;
if (index >= length)
return index - length;
return index;
if (index < 0) return index + length;
if (index >= length) return index - length;
return index;
}
/**
@ -55,11 +49,17 @@ function wrapIndex(index: number, length: number): number {
* @param predicate A function which accepts an item and returns a boolean value.
* @return A tuple containing an array of true(matched) items, and an array of false(unmatched) items.
*/
function partitionArray<T>(array: T[], predicate: (item: T, index: number) => boolean): [T[], T[]] {
return array.reduce((parts: [T[], T[]], item: T, index: number) => {
parts[predicate(item, index) ? 0 : 1].push(item);
return parts;
}, [[], []]);
function partitionArray<T>(
array: T[],
predicate: (item: T, index: number) => boolean
): [T[], T[]] {
return array.reduce(
(parts: [T[], T[]], item: T, index: number) => {
parts[predicate(item, index) ? 0 : 1].push(item);
return parts;
},
[[], []]
);
}
/**
@ -74,15 +74,15 @@ function partitionArray<T>(array: T[], predicate: (item: T, index: number) => bo
* items.
*/
function partitionArrayBySizes<T>(array: T[], sizes: number[]): T[][] {
let base = 0;
const chunks = sizes.map((size): T[] => {
const chunk = array.slice(base, base + size);
base += size;
return chunk;
});
chunks.push(array.slice(base));
let base = 0;
const chunks = sizes.map((size): T[] => {
const chunk = array.slice(base, base + size);
base += size;
return chunk;
});
chunks.push(array.slice(base));
return chunks;
return chunks;
}
/**
@ -92,9 +92,14 @@ function partitionArrayBySizes<T>(array: T[], sizes: number[]): T[][] {
* @param min2 Range 2, begin
* @param max2 Range 2, end
*/
function overlap(min1: number, max1: number, min2: number, max2: number): boolean {
const min = Math.min;
const max = Math.max;
const dx = max(0, min(max1, max2) - max(min1, min2));
return (dx > 0);
function overlap(
min1: number,
max1: number,
min2: number,
max2: number
): boolean {
const min = Math.min;
const max = Math.max;
const dx = max(0, min(max1, max2) - max(min1, min2));
return dx > 0;
}

View File

@ -19,9 +19,9 @@
// DEALINGS IN THE SOFTWARE.
function toQRect(rect: Rect) {
return Qt.rect(rect.x, rect.y, rect.width, rect.height);
return Qt.rect(rect.x, rect.y, rect.width, rect.height);
}
function toRect(qrect: QRect) {
return new Rect(qrect.x, qrect.y, qrect.width, qrect.height);
return new Rect(qrect.x, qrect.y, qrect.width, qrect.height);
}

View File

@ -19,80 +19,85 @@
// DEALINGS IN THE SOFTWARE.
class Rect {
constructor(
public x: number,
public y: number,
public width: number,
public height: number,
) {
}
constructor(
public x: number,
public y: number,
public width: number,
public height: number
) {}
public get maxX(): number { return this.x + this.width; }
public get maxY(): number { return this.y + this.height; }
public get maxX(): number {
return this.x + this.width;
}
public get maxY(): number {
return this.y + this.height;
}
public get center(): [number, number] {
return [
this.x + Math.floor(this.width / 2),
this.y + Math.floor(this.height / 2),
];
}
public get center(): [number, number] {
return [
this.x + Math.floor(this.width / 2),
this.y + Math.floor(this.height / 2),
];
}
public clone(): Rect {
return new Rect(this.x, this.y, this.width, this.height);
}
public clone(): Rect {
return new Rect(this.x, this.y, this.width, this.height);
}
public equals(other: Rect): boolean {
return (
this.x === other.x &&
this.y === other.y &&
this.width === other.width &&
this.height === other.height
);
}
public equals(other: Rect): boolean {
return (
this.x === other.x &&
this.y === other.y &&
this.width === other.width &&
this.height === other.height
);
}
public gap(left: number, right: number, top: number, bottom: number): Rect {
return new Rect(
this.x + left,
this.y + top,
this.width - (left + right),
this.height - (top + bottom),
);
}
public gap(left: number, right: number, top: number, bottom: number): Rect {
return new Rect(
this.x + left,
this.y + top,
this.width - (left + right),
this.height - (top + bottom)
);
}
public gap_mut(left: number, right: number, top: number, bottom: number): this {
this.x += left;
this.y += top;
this.width -= (left + right);
this.height -= (top + bottom);
return this;
}
public gap_mut(
left: number,
right: number,
top: number,
bottom: number
): this {
this.x += left;
this.y += top;
this.width -= left + right;
this.height -= top + bottom;
return this;
}
public includes(other: Rect): boolean {
return (
this.x <= other.x &&
this.y <= other.y &&
other.maxX < this.maxX &&
other.maxY < this.maxY
);
}
public includes(other: Rect): boolean {
return (
this.x <= other.x &&
this.y <= other.y &&
other.maxX < this.maxX &&
other.maxY < this.maxY
);
}
public includesPoint([x, y]: [number, number]): boolean {
return (
(this.x <= x && x <= this.maxX)
&& (this.y <= y && y <= this.maxY)
);
}
public includesPoint([x, y]: [number, number]): boolean {
return this.x <= x && x <= this.maxX && this.y <= y && y <= this.maxY;
}
public subtract(other: Rect): Rect {
return new Rect(
this.x - other.x,
this.y - other.y,
this.width - other.width,
this.height - other.height,
);
}
public subtract(other: Rect): Rect {
return new Rect(
this.x - other.x,
this.y - other.y,
this.width - other.width,
this.height - other.height
);
}
public toString(): string {
return "Rect(" + [this.x, this.y, this.width, this.height].join(", ") + ")";
}
public toString(): string {
return "Rect(" + [this.x, this.y, this.width, this.height].join(", ") + ")";
}
}

View File

@ -23,31 +23,34 @@
* Outward changes are in positive, and inward changes are in negative.
*/
class RectDelta {
/** Generate a delta that transforms basis to target. */
public static fromRects(basis: Rect, target: Rect): RectDelta {
const diff = target.subtract(basis);
return new RectDelta(
diff.width + diff.x,
-diff.x,
diff.height + diff.y,
-diff.y,
);
}
/** Generate a delta that transforms basis to target. */
public static fromRects(basis: Rect, target: Rect): RectDelta {
const diff = target.subtract(basis);
return new RectDelta(
diff.width + diff.x,
-diff.x,
diff.height + diff.y,
-diff.y
);
}
constructor(
public readonly east: number,
public readonly west: number,
public readonly south: number,
public readonly north: number,
) {
}
constructor(
public readonly east: number,
public readonly west: number,
public readonly south: number,
public readonly north: number
) {}
public toString(): string {
return "WindowResizeDelta(" + [
"east=" + this.east,
"west=" + this.west,
"north=" + this.north,
"south=" + this.south,
].join(" ") + ")";
}
public toString(): string {
return (
"WindowResizeDelta(" +
[
"east=" + this.east,
"west=" + this.west,
"north=" + this.north,
"south=" + this.south,
].join(" ") +
")"
);
}
}

View File

@ -19,35 +19,35 @@
// DEALINGS IN THE SOFTWARE.
class WrapperMap<F, T> {
private items: { [key: string]: T };
private items: { [key: string]: T };
constructor(
public readonly hasher: (item: F) => string,
public readonly wrapper: (item: F) => T,
) {
this.items = {};
}
constructor(
public readonly hasher: (item: F) => string,
public readonly wrapper: (item: F) => T
) {
this.items = {};
}
public add(item: F): T {
const key = this.hasher(item);
if (this.items[key] !== undefined)
throw "WrapperMap: the key [" + key + "] already exists!";
const wrapped = this.wrapper(item);
this.items[key] = wrapped;
return wrapped;
}
public add(item: F): T {
const key = this.hasher(item);
if (this.items[key] !== undefined)
throw "WrapperMap: the key [" + key + "] already exists!";
const wrapped = this.wrapper(item);
this.items[key] = wrapped;
return wrapped;
}
public get(item: F): T | null {
const key = this.hasher(item);
return this.items[key] || null;
}
public get(item: F): T | null {
const key = this.hasher(item);
return this.items[key] || null;
}
public getByKey(key: string): T | null {
return this.items[key] || null;
}
public getByKey(key: string): T | null {
return this.items[key] || null;
}
public remove(item: F): boolean {
const key = this.hasher(item);
return (delete this.items[key]);
}
public remove(item: F): boolean {
const key = this.hasher(item);
return delete this.items[key];
}
}

View File

@ -18,25 +18,25 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
let assert = require('assert');
let K = require('../krohnkite');
let assert = require("assert");
let K = require("../krohnkite");
describe('TileLayout', function() {
describe('#apply', function() {
describe("TileLayout", function () {
describe("#apply", function () {
let layout = new K.TileLayout();
let area = new K.Rect(0, 0, 1000, 1000);
let srf = new K.TestContext(0);
let gap;
K.setTestConfig('tileLayoutGap', gap = 0);
K.setTestConfig("tileLayoutGap", (gap = 0));
it('tiles sole master to full screen', function() {
it("tiles sole master to full screen", function () {
let win = new K.Window(new K.TestWindow(srf));
layout.apply([win], area);
assert(win.geometry.equals(area));
});
it('corretly applies master ratio', function() {
it("corretly applies master ratio", function () {
let master = new K.Window(new K.TestWindow(srf));
let stack = new K.Window(new K.TestWindow(srf));
@ -48,7 +48,7 @@ describe('TileLayout', function() {
assert.equal(stack.geometry.width, area.width - masterWidth);
});
it('supports non-origin screen', function() {
it("supports non-origin screen", function () {
const base = 30;
const size = 1000;
const area = new K.Rect(base, base, size, size);
@ -61,7 +61,7 @@ describe('TileLayout', function() {
for (let j = 0; j <= i; j++) {
assert(tiles[i].geometry.x >= base);
assert(tiles[i].geometry.y >= base);
assert(tiles[i].geometry.x + tiles[i].geometry.width <= base + size);
assert(tiles[i].geometry.x + tiles[i].geometry.width <= base + size);
assert(tiles[i].geometry.y + tiles[i].geometry.height <= base + size);
}
}

View File

@ -4,19 +4,12 @@
"target": "es2016",
"noEmitOnError": false,
"removeComments": true,
"lib": [
"es2016"
],
"lib": ["es2016"],
"alwaysStrict": true,
"strict": true
},
"typedocOptions": {
"entryPoints": [
"src"
]
"entryPoints": ["src"]
},
"include": [
"src/**/*",
"test/**/*"
]
"include": ["src/**/*", "test/**/*"]
}

View File

@ -1,16 +1,14 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"rules": {
"curly": false,
"max-classes-per-file": false,
"no-conditional-assignment": false,
"one-variable-per-declaration": false,
"prefer-for-of": false,
"variable-name": [true, "allow-leading-underscore"]
},
"rulesDirectory": []
}
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"curly": false,
"max-classes-per-file": false,
"no-conditional-assignment": false,
"one-variable-per-declaration": false,
"prefer-for-of": false,
"variable-name": [true, "allow-leading-underscore"]
},
"rulesDirectory": []
}