From 490e16798bb904067e988eab2efaf3de5e709acb Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 14 Feb 2018 15:34:17 -0500 Subject: [PATCH] persist and restore window state --- .vscode/launch.json | 18 ++++++ package.json | 2 +- src/main/window.main.ts | 135 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000..1658421b4e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceRoot}/build", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" + }, + "args": [ + "." + ] + } + ] +} diff --git a/package.json b/package.json index f05a23572b..d4a3753dee 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build:main": "webpack --config webpack.main.js", "build:renderer": "webpack --config webpack.renderer.js", "build:renderer:watch": "webpack --config webpack.renderer.js --watch", - "electron": "npm run build:main && (electron ./build --watch | npm run build:renderer:watch)", + "electron": "npm run build:main && (electron --inspect=5858 ./build --watch | npm run build:renderer:watch)", "pack": "electron-builder --dir", "dist": "electron-builder" }, diff --git a/src/main/window.main.ts b/src/main/window.main.ts index cb3f108330..1cbb7c01e4 100644 --- a/src/main/window.main.ts +++ b/src/main/window.main.ts @@ -5,9 +5,17 @@ import * as url from 'url'; import { Main } from '../main'; import { isDev } from '../scripts/utils'; +const WindowEventHandlingDelay = 100; +const Keys = { + mainWindowSize: 'mainWindowSize', +}; + export class WindowMain { win: BrowserWindow; + private windowStateChangeTimer: NodeJS.Timer; + private windowStates: { [key: string]: any; } = {}; + constructor(private main: Main) { } init(): Promise { @@ -16,8 +24,8 @@ export class WindowMain { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. - app.on('ready', () => { - this.createWindow(); + app.on('ready', async () => { + await this.createWindow(); resolve(); }); @@ -30,11 +38,11 @@ export class WindowMain { } }); - app.on('activate', () => { + app.on('activate', async () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (this.win === null) { - this.createWindow(); + await this.createWindow(); } }); @@ -46,20 +54,30 @@ export class WindowMain { }); } - private createWindow() { - const primaryScreenSize = screen.getPrimaryDisplay().workAreaSize; + private async createWindow() { + this.windowStates[Keys.mainWindowSize] = await this.getWindowState(Keys.mainWindowSize, 950, 600); // Create the browser window. this.win = new BrowserWindow({ - width: primaryScreenSize.width < 950 ? primaryScreenSize.width : 950, - height: primaryScreenSize.height < 600 ? primaryScreenSize.height : 600, + width: this.windowStates[Keys.mainWindowSize].width, + height: this.windowStates[Keys.mainWindowSize].height, minWidth: 680, minHeight: 500, + x: this.windowStates[Keys.mainWindowSize].x, + y: this.windowStates[Keys.mainWindowSize].y, title: app.getName(), darkTheme: true, vibrancy: 'ultra-dark', + show: false, }); + if (this.windowStates[Keys.mainWindowSize].isMaximized) { + this.win.maximize(); + } + + // Show it later since it might need to be maximized. + this.win.show(); + // and load the index.html of the app. this.win.loadURL(url.format({ protocol: 'file:', @@ -73,11 +91,110 @@ export class WindowMain { } // Emitted when the window is closed. - this.win.on('closed', () => { + this.win.on('closed', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + // Dereference the window object, usually you would store window // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. this.win = null; }); + + this.win.on('close', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('maximize', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('unmaximize', async () => { + await this.updateWindowState(Keys.mainWindowSize, this.win); + }); + + this.win.on('resize', () => { + this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + }); + + this.win.on('move', () => { + this.windowStateChangeHandler(Keys.mainWindowSize, this.win); + }); + } + + private windowStateChangeHandler(configKey: string, win: BrowserWindow) { + global.clearTimeout(this.windowStateChangeTimer); + this.windowStateChangeTimer = setTimeout(async () => { + await this.updateWindowState(configKey, win); + }, WindowEventHandlingDelay); + } + + private async updateWindowState(configKey: string, win: BrowserWindow) { + if (win == null) { + return; + } + + try { + const bounds = win.getBounds(); + + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = await this.main.storageService.get(configKey); + if (this.windowStates[configKey] == null) { + this.windowStates[configKey] = {}; + } + } + + this.windowStates[configKey].isMaximized = win.isMaximized(); + this.windowStates[configKey].displayBounds = screen.getDisplayMatching(bounds).bounds; + + if (!win.isMaximized() && !win.isMinimized() && !win.isFullScreen()) { + this.windowStates[configKey].x = bounds.x; + this.windowStates[configKey].y = bounds.y; + this.windowStates[configKey].width = bounds.width; + this.windowStates[configKey].height = bounds.height; + } + + await this.main.storageService.save(configKey, this.windowStates[configKey]); + } catch (e) { } + } + + private async getWindowState(configKey: string, defaultWidth: number, defaultHeight: number) { + let state = await this.main.storageService.get(configKey); + + const isValid = state != null && (this.stateHasBounds(state) || state.isMaximized); + let displayBounds: Electron.Rectangle = null; + if (!isValid) { + state = { + width: defaultWidth, + height: defaultHeight, + }; + + displayBounds = screen.getPrimaryDisplay().bounds; + } else if (this.stateHasBounds(state) && state.displayBounds) { + // Check if the display where the window was last open is still available + displayBounds = screen.getDisplayMatching(state.displayBounds).bounds; + + if (displayBounds.width !== state.displayBounds.width || + displayBounds.height !== state.displayBounds.height || + displayBounds.x !== state.displayBounds.x || + displayBounds.y !== state.displayBounds.y) { + state.x = undefined; + state.y = undefined; + displayBounds = screen.getPrimaryDisplay().bounds; + } + } + + if (state.width > displayBounds.width) { + state.width = displayBounds.width; + } + if (state.height > displayBounds.height) { + state.height = displayBounds.height; + } + + return state; + } + + private stateHasBounds(state: any): boolean { + return state != null && Number.isInteger(state.x) && Number.isInteger(state.y) && + Number.isInteger(state.width) && state.width > 0 && Number.isInteger(state.height) && state.height > 0; } }