Skip to content

Commit

Permalink
Add ability for windows to save custom state as part of the persisted…
Browse files Browse the repository at this point in the history
… window layout (#197)

 Add ability for windows to save custom state as part of the persisted window layout and be provided that state back on load of the layout.  If the underlying native js window has getState/setState defined they will be invoked.
  • Loading branch information
bingenito authored Nov 5, 2018
1 parent d8dd2e1 commit f0f7741
Show file tree
Hide file tree
Showing 11 changed files with 286 additions and 52 deletions.
10 changes: 10 additions & 0 deletions examples/web/assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
55 changes: 44 additions & 11 deletions src/Default/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ export class DefaultContainerWindow extends ContainerWindow {
});
}

public getState(): Promise<any> {
return new Promise<any>(resolve => {
(this.nativeWindow && (<any>this.nativeWindow).getState) ? resolve((<any>this.nativeWindow).getState()) : resolve(undefined);
});
}

public setState(state: any): Promise<void> {
return new Promise<void>(resolve => {
if (this.nativeWindow && (<any>this.nativeWindow).setState) {
(<any>this.nativeWindow).setState(state);
}

resolve();
});
}

protected attachListener(eventName: string, listener: (...args: any[]) => void): void {
this.innerWindow.addEventListener(windowEventMap[eventName] || eventName, listener);
}
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -360,16 +376,33 @@ export class DefaultContainer extends WebContainerBase {
const layout = new PersistedWindowLayout();

return new Promise<PersistedWindowLayout>((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<void>[] = [];

this.getAllWindows().then(windows => {
windows.forEach(window => {
promises.push(new Promise<void>(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));
});
});
}
}
Expand Down
19 changes: 18 additions & 1 deletion src/Electron/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ export class ElectronContainerWindow extends ContainerWindow {
});
}

public getState(): Promise<any> {
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<void> {
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;
Expand Down Expand Up @@ -482,13 +498,14 @@ export class ElectronContainer extends WebContainerBase {
this.getAllWindows().then(windows => {
windows.forEach(window => {
promises.push(new Promise<void>((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)
Expand Down
81 changes: 49 additions & 32 deletions src/OpenFin/openfin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,22 @@ export class OpenFinContainerWindow extends ContainerWindow {
});
}

public getState(): Promise<any> {
return new Promise<any>(resolve => {
(this.nativeWindow && (<any>this.nativeWindow).getState) ? resolve((<any>this.nativeWindow).getState()) : resolve(undefined);
});
}

public setState(state: any): Promise<void> {
return new Promise<void>(resolve => {
if (this.nativeWindow && (<any>this.nativeWindow).setState) {
(<any>this.nativeWindow).setState(state);
}

resolve();
});
}

protected attachListener(eventName: string, listener: (...args: any[]) => void): void {
if (eventName === "beforeunload") {
this.innerWindow.addEventListener("close-requested", (e) => {
Expand Down Expand Up @@ -612,41 +628,42 @@ export class OpenFinContainer extends WebContainerBase {
public saveLayout(name: string): Promise<PersistedWindowLayout> {
const layout = new PersistedWindowLayout();

return new Promise<PersistedWindowLayout>((resolve, reject) => {
this.desktop.Application.getCurrent().getChildWindows(windows => {
const promises: Promise<void>[] = [];
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<void>((innerResolve, innerReject) => {
window.getBounds(bounds => {
window.getOptions(options => {
delete (<any>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<PersistedWindowLayout>(async (resolve, reject) => {
const windows = await this.getAllWindows();
const mainWindow = this.getMainWindow();
const promises: Promise<void>[] = [];

windows.filter(window => window.name !== "queueCounter" && !window.name.startsWith(OpenFinContainer.notificationGuid))
.forEach(djsWindow => {
promises.push(new Promise<void>(async (innerResolve, innerReject) => {
const state = await djsWindow.getState();
const window = djsWindow.innerWindow;
window.getBounds(bounds => {
window.getOptions(options => {
delete (<any>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));
});
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,19 @@ 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;
}
});

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));
Expand Down
11 changes: 11 additions & 0 deletions src/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ export abstract class ContainerWindow extends EventEmitter {

public abstract getOptions(): Promise<any>;

/** Retrieves custom window state from underlying native window by invoking 'window.getState()' if defined. */
public getState(): Promise<any> {
return Promise.resolve(undefined);
}

/** Provide custom window state to underlying native window by invoking 'window.setState()' if defined */
public setState(state: any): Promise<void> {
return Promise.resolve();
}

/** Gets the underlying native JavaScript window object. As some containers
* do not support native access, check for undefined.
*/
Expand Down Expand Up @@ -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[];
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/Default/default.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> { return Promise.resolve(); };
Expand All @@ -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<any> { return Promise.resolve(undefined); }
public setState(): Promise<void> { return Promise.resolve(); }
public screenX: any = 0;
public screenY: any = 1;
public outerWidth: any = 2;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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();
});
});
Expand Down
Loading

0 comments on commit f0f7741

Please sign in to comment.