From d97049a2544ae1b30fef080f1979756bedb853c3 Mon Sep 17 00:00:00 2001 From: bingenito Date: Wed, 28 Mar 2018 10:29:58 -0400 Subject: [PATCH] Add support for linking windows to state changes of other windows. Minimize and restore all windows from the main window or within window groups. electron.js, app.js: - Change default behavior of example to leverage the new window state tracking of main or group. desktop.ts: - Expose through default export the new base GroupWindowManager and the WindowStateTracking enum window.ts: - Add GroupWindowManager for tracking window state changes to drive state of other windows. - Refactor SnapAssistWindowManager core window tracking to new base GroupWindowManager. --- examples/electron/electron.js | 5 +- examples/web/assets/js/app.js | 5 +- src/desktop.ts | 4 +- src/window.ts | 98 ++++++++++++++++++++++++++++------- tests/unit/window.spec.ts | 74 +++++++++++++++++++++++++- 5 files changed, 162 insertions(+), 24 deletions(-) diff --git a/examples/electron/electron.js b/examples/electron/electron.js index ae60e090..ad2e83cb 100644 --- a/examples/electron/electron.js +++ b/examples/electron/electron.js @@ -10,7 +10,10 @@ function createWindow() { desktopJS.ContainerWindow.addListener("window-created", (e) => console.log("Window created - static (ContainerWindow): " + e.windowId + ", " + e.windowName)); - snapAssist = new desktopJS.SnapAssistWindowManager(container); + snapAssist = new desktopJS.SnapAssistWindowManager(container, + { + windowStateTracking: desktopJS.WindowStateTracking.Main | desktopJS.WindowStateTracking.Group + }); container.createWindow('http://localhost:8000', { name: "desktopJS", main: true }).then(win => mainWindow = win); diff --git a/examples/web/assets/js/app.js b/examples/web/assets/js/app.js index 1562b650..177cb8fa 100644 --- a/examples/web/assets/js/app.js +++ b/examples/web/assets/js/app.js @@ -83,7 +83,10 @@ document.addEventListener("DOMContentLoaded", function (event) { $('[data-toggle="popover"]').popover(); if (container.getCurrentWindow().id === "desktopJS") { - snapAssist = new desktopJS.SnapAssistWindowManager(container); + snapAssist = new desktopJS.SnapAssistWindowManager(container, + { + windowStateTracking: desktopJS.WindowStateTracking.Main | desktopJS.WindowStateTracking.Group + }); } }); diff --git a/src/desktop.ts b/src/desktop.ts index cbfc03ec..e074ed7f 100644 --- a/src/desktop.ts +++ b/src/desktop.ts @@ -2,7 +2,7 @@ import { resolveContainer, registerContainer, ContainerRegistration } from "./re import { Container } from "./container"; import { ContainerNotification } from "./notification"; import { ObjectTransform } from "./propertymapping"; -import { ContainerWindow, SnapAssistWindowManager } from "./window"; +import { ContainerWindow, WindowStateTracking, GroupWindowManager, SnapAssistWindowManager } from "./window"; import * as Default from "./Default/default"; import * as Electron from "./Electron/electron"; import * as OpenFin from "./OpenFin/openfin"; @@ -18,5 +18,7 @@ export default class Desktop { //tslint:disable-line static get Default(): typeof Default { return Default; } static get Electron(): typeof Electron { return Electron; } static get OpenFin(): typeof OpenFin { return OpenFin; } + static get WindowStateTracking(): typeof WindowStateTracking { return WindowStateTracking; } + static get GroupWindowManager(): typeof GroupWindowManager { return GroupWindowManager; } static get SnapAssistWindowManager(): typeof SnapAssistWindowManager { return SnapAssistWindowManager; } } diff --git a/src/window.ts b/src/window.ts index 618a12e9..66986db5 100644 --- a/src/window.ts +++ b/src/window.ts @@ -259,8 +259,82 @@ export class PersistedWindowLayout { type WindowGroupStatus = {window: ContainerWindow, isGrouped: boolean}; -export class SnapAssistWindowManager { - private readonly container: Container; +/** Specifies the tracking behavior for window state linking */ +export enum WindowStateTracking { + /** No window state tracking */ + None = 0, + + /** All windows will follow the window state of the main window */ + Main = 1 << 0, + + /** All grouped windows will follow the window state of each other */ + Group = 1 << 1 +} + +export class GroupWindowManager { + protected readonly container: Container; + public windowStateTracking: WindowStateTracking = WindowStateTracking.None; + + public constructor(container: Container, options? : any) { + this.container = container; + + if (options) { + if ("windowStateTracking" in options) { + this.windowStateTracking = options.windowStateTracking; + } + } + + this.attach(); + } + + public attach(win?: ContainerWindow) { + if (win) { + win.addListener((typeof fin !== "undefined") ? "minimized" : "minimize", (e) => { + if ((this.windowStateTracking & WindowStateTracking.Main) && this.container.getMainWindow().id === e.sender.id) { + this.container.getAllWindows().then(windows => { + windows.forEach(window => window.innerWindow.minimize()); + }); + } + + if (this.windowStateTracking & WindowStateTracking.Group) { + e.sender.getGroup().then(windows => { + windows.forEach(window => window.innerWindow.minimize()); + }); + } + }); + + win.addListener((typeof fin !== "undefined") ? "restored" : "restore", (e) => { + if ((this.windowStateTracking & WindowStateTracking.Main) && this.container.getMainWindow().id === e.sender.id) { + this.container.getAllWindows().then(windows => { + windows.forEach(window => window.innerWindow.restore()); + }); + } + + if (this.windowStateTracking & WindowStateTracking.Group) { + e.sender.getGroup().then(windows => { + windows.forEach(window => window.innerWindow.restore()); + }); + } + }); + } else { + // Attach handlers to any new windows that open + ContainerWindow.addListener("window-created", (args) => { + this.container.getWindowById(args.windowId).then(window => { + this.attach(window); + }); + }); + + // Attach handlers to any windows already open + if (this.container) { + this.container.getAllWindows().then(windows => { + windows.forEach(window => this.attach(window)); + }); + } + } + } +} + +export class SnapAssistWindowManager extends GroupWindowManager { private readonly floater: ContainerWindow; public autoGrouping: boolean = true; public snapThreshold: number = 20; @@ -269,7 +343,7 @@ export class SnapAssistWindowManager { protected readonly targetGroup: Map = new Map(); public constructor(container: Container, options?: any) { - this.container = container; + super(container, options); if (options) { if ("snapThreshold" in options) { @@ -284,8 +358,6 @@ export class SnapAssistWindowManager { this.autoGrouping = options.autoGrouping; } } - - this.attach(); } /** @@ -310,6 +382,8 @@ export class SnapAssistWindowManager { } public attach(win?: ContainerWindow) { + super.attach(win); + if (win) { this.onAttached(win); @@ -367,20 +441,6 @@ export class SnapAssistWindowManager { }); }); }); - } else { - // Attach handlers to any new windows that open - ContainerWindow.addListener("window-created", (args) => { - this.container.getWindowById(args.windowId).then(window => { - this.attach(window); - }); - }); - - // Attach handlers to any windows already open - if (this.container) { - this.container.getAllWindows().then(windows => { - windows.forEach(window => this.attach(window)); - }); - } } } diff --git a/tests/unit/window.spec.ts b/tests/unit/window.spec.ts index aa756d83..a29968af 100644 --- a/tests/unit/window.spec.ts +++ b/tests/unit/window.spec.ts @@ -1,9 +1,12 @@ import { Container } from "../../src/container"; -import { ContainerWindow, WindowEventType, WindowEventArgs, SnapAssistWindowManager, Rectangle } from "../../src/window"; +import { ContainerWindow, WindowEventType, WindowEventArgs, WindowStateTracking, GroupWindowManager, SnapAssistWindowManager, Rectangle } from "../../src/window"; import { EventArgs, EventEmitter } from "../../src/events"; import { TestContainer, MockMessageBus } from "./container.spec"; class MockWindow extends ContainerWindow { + protected attachListener(eventName: WindowEventType, listener: (event: EventArgs) => void): void { + return; + } } describe ("static events", () => { @@ -90,6 +93,73 @@ describe("Rectangle", () => { }); }); + +describe("GroupWindowManager", () => { + it ("default options", () => { + const mgr = new GroupWindowManager(null); + expect(mgr.windowStateTracking).toEqual(0); + }); + + it ("overrides read from options", () => { + const mgr = new GroupWindowManager(null, { windowStateTracking: WindowStateTracking.Main }); + expect(mgr.windowStateTracking).toEqual(WindowStateTracking.Main); + }); + + it ("Minimize with Main", () => { + const innerWin = jasmine.createSpyObj("innerwindow", ["minimize", "addListener"]); + const container = jasmine.createSpyObj("container", ["getAllWindows", "getMainWindow"]); + const win = new MockWindow(innerWin); + spyOn(win, "getGroup").and.returnValue(Promise.resolve([win])); + container.getAllWindows.and.returnValue(Promise.resolve([win])); + container.getMainWindow.and.returnValue(win); + const mgr = new GroupWindowManager(container, { windowStateTracking: WindowStateTracking.Main }); + mgr.attach(win); + win.emit("minimize", {name: "minimize", sender: win }); + expect(container.getAllWindows).toHaveBeenCalledTimes(2); // Once for initial GroupWindowManager ctor + expect(win.getGroup).toHaveBeenCalledTimes(0); + }); + + it ("Minimize with Group", () => { + const innerWin = jasmine.createSpyObj("innerwindow", ["minimize", "addListener"]); + const container = jasmine.createSpyObj("container", ["getAllWindows", "getMainWindow"]); + const win = new MockWindow(innerWin); + spyOn(win, "getGroup").and.returnValue(Promise.resolve([win])); + container.getAllWindows.and.returnValue(Promise.resolve([win])); + const mgr = new GroupWindowManager(container, { windowStateTracking: WindowStateTracking.Group }); + mgr.attach(win); + win.emit("minimize", {name: "minimize", sender: win }); + expect(container.getAllWindows).toHaveBeenCalledTimes(1); // Once for initial GroupWindowManager ctor + expect(win.getGroup).toHaveBeenCalled(); + }); + + it ("Restore with Main", () => { + const innerWin = jasmine.createSpyObj("innerwindow", ["restore", "addListener"]); + const container = jasmine.createSpyObj("container", ["getAllWindows", "getMainWindow"]); + const win = new MockWindow(innerWin); + spyOn(win, "getGroup").and.returnValue(Promise.resolve([win])); + container.getAllWindows.and.returnValue(Promise.resolve([win])); + container.getMainWindow.and.returnValue(win); + const mgr = new GroupWindowManager(container, { windowStateTracking: WindowStateTracking.Main }); + mgr.attach(win); + win.emit("restore", {name: "restore", sender: win }); + expect(container.getAllWindows).toHaveBeenCalledTimes(2); // Once for initial GroupWindowManager ctor + expect(win.getGroup).toHaveBeenCalledTimes(0); + }); + + it ("Restore with Group", () => { + const innerWin = jasmine.createSpyObj("innerwindow", ["restore", "addListener"]); + const container = jasmine.createSpyObj("container", ["getAllWindows", "getMainWindow"]); + const win = new MockWindow(innerWin); + spyOn(win, "getGroup").and.returnValue(Promise.resolve([win])); + container.getAllWindows.and.returnValue(Promise.resolve([win])); + const mgr = new GroupWindowManager(container, { windowStateTracking: WindowStateTracking.Group }); + mgr.attach(win); + win.emit("restore", {name: "restore", sender: win }); + expect(container.getAllWindows).toHaveBeenCalledTimes(1); // Once for initial GroupWindowManager ctor + expect(win.getGroup).toHaveBeenCalled(); + }); +}); + describe("SnapAssistWindowManager", () => { it ("default options", () => { const mgr = new SnapAssistWindowManager(null); @@ -309,4 +379,4 @@ describe("SnapAssistWindowManager", () => { }); }); }); -}); \ No newline at end of file +});