diff --git a/examples/web/assets/js/app.js b/examples/web/assets/js/app.js index 7ec586d8..d596b503 100644 --- a/examples/web/assets/js/app.js +++ b/examples/web/assets/js/app.js @@ -203,3 +203,13 @@ loadlayoutButton.onclick = function () { savelayoutButton.onclick = function () { container.saveLayout("Layout"); }; + +/** Invoked by window layout saving to maintain state specific to this window */ +function getState() { + return { value: "Foo" }; +} + +/** Invoked by window layout loading to restore state specific to this window */ +function setState(state) { + this.container.log("info", state); +} diff --git a/src/Default/default.ts b/src/Default/default.ts index 6d20217d..fd3d2c06 100644 --- a/src/Default/default.ts +++ b/src/Default/default.ts @@ -107,6 +107,22 @@ export class DefaultContainerWindow extends ContainerWindow { }); } + public getState(): Promise { + return new Promise(resolve => { + (this.nativeWindow && (this.nativeWindow).getState) ? resolve((this.nativeWindow).getState()) : resolve(undefined); + }); + } + + public setState(state: any): Promise { + return new Promise(resolve => { + if (this.nativeWindow && (this.nativeWindow).setState) { + (this.nativeWindow).setState(state); + } + + resolve(); + }); + } + protected attachListener(eventName: string, listener: (...args: any[]) => void): void { this.innerWindow.addEventListener(windowEventMap[eventName] || eventName, listener); } @@ -329,7 +345,7 @@ export class DefaultContainer extends WebContainerBase { const windows: ContainerWindow[] = []; const trackedWindows = this.globalWindow[DefaultContainer.windowsPropertyKey]; for (const key in trackedWindows) { - windows.push(trackedWindows[key]); + windows.push(this.wrapWindow(trackedWindows[key])); } resolve(windows); }); @@ -360,16 +376,33 @@ export class DefaultContainer extends WebContainerBase { const layout = new PersistedWindowLayout(); return new Promise((resolve, reject) => { - const windows = this.globalWindow[DefaultContainer.windowsPropertyKey]; - for (const key in windows) { - const win = windows[key]; - if (this.globalWindow !== win) { - layout.windows.push({ name: win.name, url: win.location.toString(), bounds: { x: win.screenX, y: win.screenY, width: win.outerWidth, height: win.outerHeight }, options: win[Container.windowOptionsPropertyKey] }); - } - } - - this.saveLayoutToStorage(name, layout); - resolve(layout); + const promises: Promise[] = []; + + this.getAllWindows().then(windows => { + windows.forEach(window => { + promises.push(new Promise(async (innerResolve) => { + const nativeWin = window.nativeWindow; + if (this.globalWindow !== nativeWin) { + layout.windows.push( + { + name: window.name, + url: nativeWin.location.toString(), + id: window.id, + bounds: { x: nativeWin.screenX, y: nativeWin.screenY, width: nativeWin.outerWidth, height: nativeWin.outerHeight }, + options: nativeWin[Container.windowOptionsPropertyKey], + state: await window.getState() + } + ); + } + innerResolve(); + })); + }); + + Promise.all(promises).then(() => { + this.saveLayoutToStorage(name, layout); + resolve(layout); + }).catch(reason => reject(reason)); + }); }); } } diff --git a/src/Electron/electron.ts b/src/Electron/electron.ts index 3b965fd9..7d86420f 100644 --- a/src/Electron/electron.ts +++ b/src/Electron/electron.ts @@ -188,6 +188,22 @@ export class ElectronContainerWindow extends ContainerWindow { }); } + public getState(): Promise { + if (this.innerWindow && this.innerWindow.webContents) { + return this.innerWindow.webContents.executeJavaScript("window.getState ? window.getState() : undefined"); + } else { + return Promise.resolve(undefined); + } + } + + public setState(state: any): Promise { + if (this.innerWindow && this.innerWindow.webContents) { + return this.innerWindow.webContents.executeJavaScript(`if (window.setState) { window.setState(JSON.parse(\`${JSON.stringify(state)}\`)); }`); + } else { + return Promise.resolve(); + } + } + protected attachListener(eventName: string, listener: (...args: any[]) => void): void { if (eventName === "beforeunload") { const win = this.window || window; @@ -482,13 +498,14 @@ export class ElectronContainer extends WebContainerBase { this.getAllWindows().then(windows => { windows.forEach(window => { promises.push(new Promise((innerResolve, innerReject) => { - window.getGroup().then(group => { + window.getGroup().then(async group => { layout.windows.push( { id: window.id, name: window.name, url: window.innerWindow.webContents.getURL(), main: (mainWindow === window.innerWindow), + state: await window.getState(), options: window.innerWindow[Container.windowOptionsPropertyKey], bounds: window.innerWindow.getBounds(), group: group.map(win => win.id) diff --git a/src/OpenFin/openfin.ts b/src/OpenFin/openfin.ts index 80c5d6d6..145e2062 100644 --- a/src/OpenFin/openfin.ts +++ b/src/OpenFin/openfin.ts @@ -163,6 +163,22 @@ export class OpenFinContainerWindow extends ContainerWindow { }); } + public getState(): Promise { + return new Promise(resolve => { + (this.nativeWindow && (this.nativeWindow).getState) ? resolve((this.nativeWindow).getState()) : resolve(undefined); + }); + } + + public setState(state: any): Promise { + return new Promise(resolve => { + if (this.nativeWindow && (this.nativeWindow).setState) { + (this.nativeWindow).setState(state); + } + + resolve(); + }); + } + protected attachListener(eventName: string, listener: (...args: any[]) => void): void { if (eventName === "beforeunload") { this.innerWindow.addEventListener("close-requested", (e) => { @@ -612,41 +628,42 @@ export class OpenFinContainer extends WebContainerBase { public saveLayout(name: string): Promise { const layout = new PersistedWindowLayout(); - return new Promise((resolve, reject) => { - this.desktop.Application.getCurrent().getChildWindows(windows => { - const promises: Promise[] = []; - const mainWindow = this.desktop.Application.getCurrent().getWindow(); - - windows.concat(mainWindow) - .filter(window => window.name !== "queueCounter" && !window.name.startsWith(OpenFinContainer.notificationGuid)) - .forEach(window => { - promises.push(new Promise((innerResolve, innerReject) => { - window.getBounds(bounds => { - window.getOptions(options => { - delete (options).show; // show is an undocumented option that interferes with the createWindow mapping of show -> autoShow - window.getGroup(group => { - layout.windows.push( - { - name: window.name, - id: window.name, - url: window.getNativeWindow() ? window.getNativeWindow().location.toString() : options.url, - main: (mainWindow.name === window.name), - options: options, - bounds: { x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height }, - group: group.map(win => win.name) - }); - innerResolve(); - }, innerReject); + return new Promise(async (resolve, reject) => { + const windows = await this.getAllWindows(); + const mainWindow = this.getMainWindow(); + const promises: Promise[] = []; + + windows.filter(window => window.name !== "queueCounter" && !window.name.startsWith(OpenFinContainer.notificationGuid)) + .forEach(djsWindow => { + promises.push(new Promise(async (innerResolve, innerReject) => { + const state = await djsWindow.getState(); + const window = djsWindow.innerWindow; + window.getBounds(bounds => { + window.getOptions(options => { + delete (options).show; // show is an undocumented option that interferes with the createWindow mapping of show -> autoShow + window.getGroup(group => { + layout.windows.push( + { + name: window.name, + id: window.name, + url: window.getNativeWindow() ? window.getNativeWindow().location.toString() : options.url, + main: (mainWindow && (mainWindow.name === window.name)), + options: options, + state: state, + bounds: { x: bounds.left, y: bounds.top, width: bounds.width, height: bounds.height }, + group: group.map(win => win.name) + }); + innerResolve(); }, innerReject); }, innerReject); - })); - }); + }, innerReject); + })); + }); - Promise.all(promises).then(() => { - this.saveLayoutToStorage(name, layout); - resolve(layout); - }).catch(reason => reject(reason)); - }); + Promise.all(promises).then(() => { + this.saveLayoutToStorage(name, layout); + resolve(layout); + }).catch(reason => reject(reason)); }); } } diff --git a/src/container.ts b/src/container.ts index a3b00baa..d1504e1d 100644 --- a/src/container.ts +++ b/src/container.ts @@ -193,8 +193,12 @@ export abstract class ContainerBase extends Container { // de-dupe window grouping windows.forEach(window => { - let found = false; + const matchingWindow = layout.windows.find(win => win.name === window.name); + if (matchingWindow && matchingWindow.state && window.setState) { + window.setState(matchingWindow.state).catch(e => this.log("error", "Error invoking setState: " + e)); + } + let found = false; groupMap.forEach((targets, win) => { if (!found && targets.indexOf(window.id) >= 0) { found = true; @@ -202,7 +206,6 @@ export abstract class ContainerBase extends Container { }); if (!found) { - const matchingWindow = layout.windows.find(win => win.name === window.name); const group = matchingWindow ? matchingWindow.group : undefined; if (group && group.length > 0) { groupMap.set(window, group.filter(id => id !== window.id)); diff --git a/src/window.ts b/src/window.ts index 5f2afa9d..7706b779 100644 --- a/src/window.ts +++ b/src/window.ts @@ -144,6 +144,16 @@ export abstract class ContainerWindow extends EventEmitter { public abstract getOptions(): Promise; + /** Retrieves custom window state from underlying native window by invoking 'window.getState()' if defined. */ + public getState(): Promise { + return Promise.resolve(undefined); + } + + /** Provide custom window state to underlying native window by invoking 'window.setState()' if defined */ + public setState(state: any): Promise { + return Promise.resolve(); + } + /** Gets the underlying native JavaScript window object. As some containers * do not support native access, check for undefined. */ @@ -265,6 +275,7 @@ export class PersistedWindow { public id: string; public bounds: any; public options?: any; + public state?: any; public url?: string; public main?: boolean; public group?: string[]; diff --git a/tests/unit/Default/default.spec.ts b/tests/unit/Default/default.spec.ts index b6dd9440..13d3e349 100644 --- a/tests/unit/Default/default.spec.ts +++ b/tests/unit/Default/default.spec.ts @@ -3,6 +3,7 @@ import { DefaultContainerWindow, DefaultContainer, DefaultMessageBus } from "../ class MockWindow { public listener: any; + public name: string = "Name"; public focus(): void { }; public show(): void { }; public close(): Promise { return Promise.resolve(); }; @@ -12,6 +13,8 @@ class MockWindow { public postMessage(message: string, origin: string): void { }; public moveTo(x: number, y: number): void { }; public resizeTo(width: number, height: number): void { } + public getState(): Promise { return Promise.resolve(undefined); } + public setState(): Promise { return Promise.resolve(); } public screenX: any = 0; public screenY: any = 1; public outerWidth: any = 2; @@ -68,6 +71,46 @@ describe("DefaultContainerWindow", () => { expect(win.isShowing()).toBeDefined(); }); + describe("getState", () => { + it("getState undefined", (done) => { + let mockWindow = new MockWindow(); + delete mockWindow.getState; + let win = new DefaultContainerWindow(mockWindow); + + win.getState().then(state => { + expect(state).toBeUndefined(); + }).then(done); + }); + + it("getState defined", (done) => { + const mockState = { value: "Foo" }; + spyOn(win.innerWindow, "getState").and.returnValue(Promise.resolve(mockState)); + win.getState().then(state => { + expect(win.innerWindow.getState).toHaveBeenCalled(); + expect(state).toEqual(mockState); + }).then(done); + }); + }); + + describe("setState", () => { + it("setState undefined", (done) => { + let mockWindow = new MockWindow(); + delete mockWindow.setState; + let win = new DefaultContainerWindow(mockWindow); + + win.setState({}).then(done); + }); + + it("setState defined", (done) => { + const mockState = { value: "Foo" }; + spyOn(win.innerWindow, "setState").and.returnValue(Promise.resolve()); + + win.setState(mockState).then(() => { + expect(win.innerWindow.setState).toHaveBeenCalledWith(mockState); + }).then(done); + }); + }); + it("getSnapshot rejects", (done) => { let success: boolean = false; @@ -333,6 +376,7 @@ describe("DefaultContainer", () => { container.getAllWindows().then(wins => { expect(wins).not.toBeNull(); expect(wins.length).toEqual(2); + wins.forEach(win => expect(win instanceof DefaultContainerWindow).toBeTruthy("Window is not of type DefaultContainerWindow")); done(); }); }); diff --git a/tests/unit/Electron/electron.spec.ts b/tests/unit/Electron/electron.spec.ts index 39ad6013..0f345449 100644 --- a/tests/unit/Electron/electron.spec.ts +++ b/tests/unit/Electron/electron.spec.ts @@ -31,7 +31,6 @@ class MockWindow extends MockEventEmitter { public id: number; public group: string; private bounds: any = { x: 0, y: 1, width: 2, height: 3 } - public innerWindow: any = {}; constructor(name?: string) { super(); @@ -55,7 +54,8 @@ class MockWindow extends MockEventEmitter { public webContents: any = { send(channel: string, ...args: any[]) { }, - getURL() { return "url"; } + getURL() { return "url"; }, + executeJavaScript() {} } public getBounds(): any { return this.bounds; } @@ -192,6 +192,47 @@ describe("ElectronContainerWindow", () => { }).then(done); }); + describe("getState", () => { + it("getState undefined", (done) => { + let mockWindow = new MockWindow(); + mockWindow.webContents = null; + let win = new ElectronContainerWindow(mockWindow, container); + + win.getState().then(state => { + expect(state).toBeUndefined(); + }).then(done); + }); + + it("getState defined", (done) => { + const mockState = { value: "Foo" }; + spyOn(innerWin.webContents, "executeJavaScript").and.returnValue(Promise.resolve(mockState)); + + win.getState().then(state => { + expect(innerWin.webContents.executeJavaScript).toHaveBeenCalled(); + expect(state).toEqual(mockState); + }).then(done); + }); + }); + + describe("setState", () => { + it("setState undefined", (done) => { + let mockWindow = new MockWindow(); + mockWindow.webContents = null; + let win = new ElectronContainerWindow(mockWindow, container); + + win.setState({}).then(done); + }); + + it("setState defined", (done) => { + const mockState = { value: "Foo" }; + spyOn(innerWin.webContents, "executeJavaScript").and.returnValue(Promise.resolve()); + + win.setState(mockState).then(() => { + expect(innerWin.webContents.executeJavaScript).toHaveBeenCalled(); + }).then(done); + }); + }); + it("getSnapshot", (done) => { spyOn(innerWin, "capturePage").and.callThrough(); let success: boolean = false; diff --git a/tests/unit/OpenFin/openfin.spec.ts b/tests/unit/OpenFin/openfin.spec.ts index eeec6c8a..d3d41622 100644 --- a/tests/unit/OpenFin/openfin.spec.ts +++ b/tests/unit/OpenFin/openfin.spec.ts @@ -57,7 +57,7 @@ class MockInterApplicationBus { class MockWindow { static singleton: MockWindow = new MockWindow("Singleton"); - public nativeWindow: Window = jasmine.createSpyObj("window", ["location"]); + public nativeWindow: Window = jasmine.createSpyObj("window", ["location", "getState", "setState"]); constructor(name?: string) { this.name = name; @@ -259,6 +259,48 @@ describe("OpenFinContainerWindow", () => { }).then(done); }); + + describe("getState", () => { + it("getState undefined", (done) => { + let mockWindow = new MockWindow(); + delete (mockWindow.nativeWindow).getState; + let win = new OpenFinContainerWindow(innerWin); + + win.getState().then(state => { + expect(state).toBeUndefined(); + }).then(done); + }); + + it("getState defined", (done) => { + const mockState = { value: "Foo" }; + innerWin.nativeWindow.getState.and.returnValue(mockState); + + win.getState().then(state => { + expect(innerWin.nativeWindow.getState).toHaveBeenCalled(); + expect(state).toEqual(mockState); + }).then(done); + }); + }); + + describe("setState", () => { + it("setState undefined", (done) => { + let mockWindow = new MockWindow(); + delete (mockWindow.nativeWindow).setState; + let win = new OpenFinContainerWindow(innerWin); + + win.setState({}).then(done); + }); + + it("setState defined", (done) => { + const mockState = { value: "Foo" }; + innerWin.nativeWindow.setState.and.returnValue(Promise.resolve()); + + win.setState(mockState).then(() => { + expect(innerWin.nativeWindow.setState).toHaveBeenCalledWith(mockState); + }).then(done); + }); + }); + describe("getSnapshot", () => { it("getSnapshot invokes underlying getSnapshot", (done) => { spyOn(innerWin, "getSnapshot").and.callThrough(); diff --git a/tests/unit/container.spec.ts b/tests/unit/container.spec.ts index df706bbb..1b77e0df 100644 --- a/tests/unit/container.spec.ts +++ b/tests/unit/container.spec.ts @@ -28,8 +28,10 @@ export class MockMessageBus implements MessageBus { // tslint:disable-line export class TestContainer extends ContainerBase { getMainWindow(): ContainerWindow { - const win = jasmine.createSpyObj("ContainerWindow", ["setBounds"]); + const win = jasmine.createSpyObj("ContainerWindow", ["setBounds", "getState", "setState"]); Object.defineProperty(win, "name", { value: "1" }); + win.getState.and.returnValue(Promise.resolve({})); + win.setState.and.returnValue(Promise.resolve()); return win; } @@ -40,9 +42,11 @@ export class TestContainer extends ContainerBase { } createWindow(url: string, options?: any): Promise { - const win = jasmine.createSpyObj("ContainerWindow", ["id"]); + const win = jasmine.createSpyObj("ContainerWindow", ["id", "getState", "setState"]); Object.defineProperty(win, "name", { value: options.name || "1" }); Object.defineProperty(win, "id", { value: options.name || "1" }); + win.getState.and.returnValue(Promise.resolve({})); + win.setState.and.returnValue(Promise.resolve()); return Promise.resolve(win); } @@ -54,7 +58,7 @@ export class TestContainer extends ContainerBase { this.storage = { getItem(key: string): string { const layout: PersistedWindowLayout = new PersistedWindowLayout(); - layout.windows.push({ name: "1", id: "1", url: "url", bounds: {}, group: ["1", "2", "3"]}); + layout.windows.push({ name: "1", id: "1", url: "url", bounds: {}, state: { "value": "foo" }, group: ["1", "2", "3"]}); layout.windows.push({ name: "2", id: "2", main: true, url: "url", bounds: {}, group: ["1", "2", "3"]}); layout.windows.push({ name: "3", id: "3", url: "url", bounds: {}, group: ["1", "2", "3"]}); layout.name = "Test"; diff --git a/tests/unit/window.spec.ts b/tests/unit/window.spec.ts index aea6a0f5..0c8ce405 100644 --- a/tests/unit/window.spec.ts +++ b/tests/unit/window.spec.ts @@ -16,6 +16,18 @@ describe ("ContainerWindow", () => { it("nativeWindow returns undefined", () => { expect(new MockWindow(undefined).nativeWindow).toBeUndefined(); }); + + it("getState returns undefined", (done) => { + new MockWindow(undefined).getState().then(state => { + expect(state).toBeUndefined(); + }).then(done); + }); + + it("setState returns", (done) => { + new MockWindow(undefined).setState({}).then(() => { + expect(true); + }).then(done); + }); }); describe ("static events", () => {