Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability for windows to save custom state as part of the persisted window layout #197

Merged
merged 3 commits into from
Nov 5, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("getState ? 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 (setState) setState(JSON.parse(\`${JSON.stringify(state)}\`))`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We sure there is no better expression of the intent here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psmulovics What do you mean by intent?

Copy link
Member Author

@bingenito bingenito Oct 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's certainly not pretty but there is no access to the underlying native window (cross process) and doing it via ipc would require having two way send but if the other side doesn't have any code in place to handle it the caller would be waiting for a reply that isn't coming.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (setState) would throw an error if setState isn't defined; should this be if (typeof setState !== "undefined") or if (window.setState) ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1j01 I assumed since it was in the global that window.setState is equivalent to setState. Testing it is a bit muddied since it goes through executeJavaScript which can be masking this from me. I'll update it if not for a fix but definitely for clarity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@1j01 @psmulovics Updated to add explicit use of methods on window global and formatting with braces where appropriate.

} 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