mirror of
https://github.com/Bismuth-Forge/bismuth.git
synced 2024-09-11 08:25:39 +03:00
refactor: use prettier
This commit is contained in:
parent
96cb46ef00
commit
50bd6aea61
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
build
|
||||
node_modules
|
||||
*.md
|
||||
|
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^6.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"typedoc": "^0.21.6",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
|
174
src/common.ts
174
src/common.ts
@ -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;
|
||||
|
@ -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 */
|
||||
|
@ -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 */
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(", ") +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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) + ")";
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
2
src/extern/global.d.ts
vendored
2
src/extern/global.d.ts
vendored
@ -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
172
src/extern/kwin.d.ts
vendored
@ -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;
|
||||
}
|
||||
}
|
||||
|
86
src/extern/plasma.d.ts
vendored
86
src/extern/plasma.d.ts
vendored
@ -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
43
src/extern/qt.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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()";
|
||||
}
|
||||
}
|
||||
|
@ -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()";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
137
src/util/rect.ts
137
src/util/rect.ts
@ -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(", ") + ")";
|
||||
}
|
||||
}
|
||||
|
@ -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(" ") +
|
||||
")"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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/**/*"]
|
||||
}
|
||||
|
28
tslint.json
28
tslint.json
@ -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": []
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user