├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.MD ├── README.md ├── appveyor.yml ├── main.js ├── package.json ├── spec ├── helpers │ ├── pseudoBrowserWindow.js │ └── stubs.js ├── init.spec.js ├── io.spec.js ├── screen.spec.js ├── support │ └── jasmine.json └── utils │ └── utils.js └── src ├── lib ├── checkState.js ├── loadState.js ├── saveState.js └── windowState.js └── utils └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | build 3 | logs 4 | node_modules 5 | .npm 6 | .node_repl_history 7 | 8 | # Files 9 | .DS_Store 10 | .thumbs_db 11 | npm-debug.log* 12 | *.log 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .jshintrc 2 | spec 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.1" 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.3.2 2 | **Changes**: 3 | 4 | - Merged hkuclion pull request so that the app data path and the app name is retreived via electron's native functions rather than via the manifest file. 5 | - Adjusted unit tests to work with this change 6 | 7 | ## v0.3.1 8 | **Bugfixes**: 9 | 10 | - Fixed a bug, where the application crashed when loading a currupt window states json file 11 | 12 | ## v0.3.0 13 | **Features**: 14 | 15 | - Added Support for multi monitor setups. 16 | - The application now checks if a previously saved window is in the viewport of a monitor. If it is not (e.g. monitor constellation has changed since last using your electron app) the window will be centered on the primary monitor. 17 | 18 | ## v0.2.2 19 | **Bugfixes**: 20 | 21 | - Fixed a bug, where the maximized state of a window was saved incorrectly 22 | 23 | ## v0.2.1 24 | Initial release 25 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2016 Sethorax 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-window-state-manager [![Build Status](https://travis-ci.org/Sethorax/electron-window-state-manager.svg?branch=master)](https://travis-ci.org/Sethorax/electron-window-state-manager) [![Build status](https://ci.appveyor.com/api/projects/status/eude9rmxi0oi1pe8/branch/master?svg=true)](https://ci.appveyor.com/project/Sethorax/electron-window-state-manager/branch/master) 2 | 3 | The Electron Window State Manager is a small package that gives you the ability to save the state of a `BrowserWindow` and retreive the saved data of the state later. 4 | 5 | # Installation 6 | ``` 7 | npm install electron-window-state-manager 8 | ``` 9 | 10 | # What does it do? 11 | 12 | The Window State Manager can store the dimensions (width and height), the position (x and y coordinates) and the current state (maximized or not) of a `BrowserWindow` and save it to a json file. 13 | The saved state can than later be retreived when using the same window name at the instance creation. 14 | The saved state's data will be automatically retreived when creating a `WindowStateManager`instance with an already saved window name. 15 | 16 | ## Usage 17 | 18 | To be able to use this package in your project you need to require it: 19 | ```javascript 20 | const WindowStateManager = require('electron-window-state-manager'); 21 | ``` 22 | 23 | #### Class: WindowStateManager 24 | 25 | It creates a new `WindowStateManager` with a `name` and default properties as set by the `options`. 26 | 27 | ### `new WindowStateManager(name, [options])` 28 | 29 | * `name` String - Name of the window. 30 | * `options` Object 31 | * `defaultWidth` Integer - Default window width in pixels. 32 | * `defaultHeight` Integer - Default window height in pixels. 33 | 34 | The value of `name` is used to reference a saved state in the json file. If you create a new instance of `WindowStateManager` with a name which was already saved previously, the data of this state will be loaded. 35 | 36 | If a state with the value of `name` cannot be found in the json file or a saved state has wrong data, the default values assigned in the `options` Object will be used. 37 | 38 | ### Methods 39 | 40 | The `WindowStateManager` class has the following methods: 41 | 42 | #### `WindowStateManager.saveState(window)` 43 | 44 | * `window` [BrowserWindow](https://github.com/atom/electron/blob/master/docs/api/browser-window.md) 45 | 46 | Saves the state of the passed `BrowserWindow` and returns `true`or `false` depending on whether the state was successfully saved to the json file. 47 | 48 | In case the state of a window in fullscreen is saved, the saving process will not succeed because we don't want to save the dimensions of a fullscreen window. 49 | 50 | The method returns `false` and will not save anything if a window in fullscreen is saved, because we don't want to save the dimensions of a fullscreen window. 51 | 52 | If a maximized window is saved, the dimensions and position of the window will not be stored, only the previously saved values or the default values will be saved. 53 | However the maximized state of the window will be saved, so that you can open the window in a maximized state again later if the window was closed in a maximized state. 54 | 55 | 56 | ## Example 57 | ```javascript 58 | const app = require('electron').app; 59 | const BrowserWindow = require('electron').BrowserWindow; 60 | const WindowStateManager = require('electron-window-state-manager'); 61 | 62 | const mainWindow; 63 | 64 | // Create a new instance of the WindowStateManager 65 | // and pass it the name and the default properties 66 | const mainWindowState = new WindowStateManager('mainWindow', { 67 | defaultWidth: 1024, 68 | defaultHeight: 768 69 | }); 70 | 71 | app.on('ready', () => { 72 | // When creating a new BrowserWindow 73 | // you can assign the properties of the mainWindowState. 74 | // If a window with the name 'main' was saved before, 75 | // the saved values will now be assigned to the BrowserWindow again 76 | mainWindow = new BrowserWindow({ 77 | width: mainWindowState.width, 78 | height: mainWindowState.height, 79 | x: mainWindowState.x, 80 | y: mainWindowState.y, 81 | }); 82 | 83 | // You can check if the window was closed in a maximized saveState 84 | // If so you can maximize the BrowserWindow again 85 | if (mainWindowState.maximized) { 86 | mainWindow.maximize(); 87 | } 88 | 89 | // Don't forget to save the current state 90 | // of the Browser window when it's about to be closed 91 | mainWindow.on('close', () => { 92 | mainWindowState.saveState(mainWindow); 93 | }); 94 | }); 95 | ``` 96 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Test against the latest version of this Node.js version 2 | environment: 3 | nodejs_version: "6" 4 | 5 | # Install scripts. (runs after repo cloning) 6 | install: 7 | # Get the latest stable version of Node.js or io.js 8 | - ps: Install-Product node $env:nodejs_version 9 | # install modules 10 | - npm install 11 | 12 | # Post-install test scripts. 13 | test_script: 14 | # Output useful info for debugging. 15 | - node --version 16 | - npm --version 17 | # run tests 18 | - npm test 19 | 20 | # Don't actually build. 21 | build: off -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const WindowStateManager = require('./src/lib/windowState'); 4 | 5 | module.exports = WindowStateManager; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-window-state-manager", 3 | "version": "0.3.2", 4 | "description": "Window-state manager for electron", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "jasmine" 8 | }, 9 | "keywords": [ 10 | "electron" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Sethorax/electron-window-state-manager.git" 15 | }, 16 | "author": "sethorax", 17 | "license": "MIT", 18 | "dependencies": { 19 | "app-root-path": "^1.3.0", 20 | "fs-jetpack": "^0.9.2" 21 | }, 22 | "devDependencies": { 23 | "jasmine": "^2.4.1", 24 | "proxyquire": "^1.7.10" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/helpers/pseudoBrowserWindow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class PseudoBrowserWindow { 4 | 5 | constructor(options) { 6 | this._width = options.width; 7 | this._height = options.height; 8 | this._x = options.x !== 'undefined' ? options.x : 0; 9 | this._y = options.y !== 'undefined' ? options.y : 0; 10 | 11 | this._isFullScreen = options.isFullScreen; 12 | this._isMaximized = options.isMaximized; 13 | } 14 | 15 | getBounds() { 16 | return { 17 | width: this._width, 18 | height: this._height, 19 | x: this._x, 20 | y: this._y 21 | }; 22 | } 23 | 24 | isFullScreen() { 25 | return this._isFullScreen; 26 | } 27 | 28 | isMaximized() { 29 | return this._isMaximized; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /spec/helpers/stubs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const utils = require('../utils/utils'); 4 | 5 | 6 | var baseStub = { 7 | app: { 8 | getPath: (type) => { 9 | if (type === 'appData') { 10 | return utils.getAppDataPath(); 11 | } 12 | }, 13 | getName: () => { 14 | return utils.getManifestData().name 15 | }, 16 | } 17 | }; 18 | 19 | module.exports = { 20 | electron: () => { 21 | return { 22 | screenRight: { 23 | electron: Object.assign({ 24 | screen: { 25 | getPrimaryDisplay: function () { 26 | return { 27 | bounds: { 28 | x: 0, 29 | y: 0, 30 | width: 1920, 31 | height: 1080 32 | } 33 | }; 34 | }, 35 | 36 | getAllDisplays: function() { 37 | return { 38 | find: function (cb) { 39 | cb({ 40 | bounds: { 41 | x: 1920, 42 | y: 0, 43 | width: 1920, 44 | height: 1080 45 | } 46 | }); 47 | return { 48 | bounds: { 49 | x: 1920, 50 | y: 0, 51 | width: 1920, 52 | height: 1080 53 | } 54 | }; 55 | } 56 | }; 57 | } 58 | }, 59 | '@global': true, 60 | '@noCallThru': true 61 | }, baseStub) 62 | }, 63 | 64 | screenLeft: { 65 | electron: Object.assign({ 66 | screen: { 67 | getPrimaryDisplay: function () { 68 | return { 69 | bounds: { 70 | x: 0, 71 | y: 0, 72 | width: 1920, 73 | height: 1080 74 | } 75 | }; 76 | }, 77 | 78 | getAllDisplays: function() { 79 | return { 80 | find: function (cb) { 81 | cb({ 82 | bounds: { 83 | x: -1920, 84 | y: 0, 85 | width: 1920, 86 | height: 1080 87 | } 88 | }); 89 | return { 90 | bounds: { 91 | x: -1920, 92 | y: 0, 93 | width: 1920, 94 | height: 1080 95 | } 96 | }; 97 | } 98 | }; 99 | } 100 | }, 101 | '@global': true, 102 | '@noCallThru': true 103 | }, baseStub) 104 | }, 105 | 106 | screenTop: { 107 | electron: Object.assign({ 108 | screen: { 109 | getPrimaryDisplay: function () { 110 | return { 111 | bounds: { 112 | x: 0, 113 | y: 0, 114 | width: 1920, 115 | height: 1080 116 | } 117 | }; 118 | }, 119 | 120 | getAllDisplays: function() { 121 | return { 122 | find: function (cb) { 123 | cb({ 124 | bounds: { 125 | x: 0, 126 | y: -1080, 127 | width: 1920, 128 | height: 1080 129 | } 130 | }); 131 | return { 132 | bounds: { 133 | x: 0, 134 | y: -1080, 135 | width: 1920, 136 | height: 1080 137 | } 138 | }; 139 | } 140 | }; 141 | } 142 | }, 143 | '@global': true, 144 | '@noCallThru': true 145 | }, baseStub) 146 | }, 147 | 148 | screenBottom: { 149 | electron: Object.assign({ 150 | screen: { 151 | getPrimaryDisplay: function () { 152 | return { 153 | bounds: { 154 | x: 0, 155 | y: 0, 156 | width: 1920, 157 | height: 1080 158 | } 159 | }; 160 | }, 161 | 162 | getAllDisplays: function() { 163 | return { 164 | find: function (cb) { 165 | cb({ 166 | bounds: { 167 | x: 0, 168 | y: 1080, 169 | width: 1920, 170 | height: 1080 171 | } 172 | }); 173 | return { 174 | bounds: { 175 | x: 0, 176 | y: 1080, 177 | width: 1920, 178 | height: 1080 179 | } 180 | }; 181 | } 182 | }; 183 | } 184 | }, 185 | '@global': true, 186 | '@noCallThru': true 187 | }, baseStub) 188 | }, 189 | 190 | tripleMonitor: { 191 | electron: Object.assign({ 192 | screen: { 193 | getPrimaryDisplay: function () { 194 | return { 195 | bounds: { 196 | x: 0, 197 | y: 0, 198 | width: 1920, 199 | height: 1080 200 | } 201 | }; 202 | }, 203 | 204 | getAllDisplays: function() { 205 | return { 206 | find: function (cb) { 207 | cb({ 208 | bounds: { 209 | x: 1920, 210 | y: 0, 211 | width: 1920, 212 | height: 1080 213 | } 214 | }); 215 | return [ 216 | { 217 | bounds: { 218 | x: 1920, 219 | y: 0, 220 | width: 1920, 221 | height: 1080 222 | } 223 | }, 224 | { 225 | bounds: { 226 | x: -1920, 227 | y: 0, 228 | width: 1920, 229 | height: 1080 230 | } 231 | } 232 | ]; 233 | } 234 | }; 235 | } 236 | }, 237 | '@global': true, 238 | '@noCallThru': true 239 | }, baseStub) 240 | } 241 | }; 242 | } 243 | }; 244 | -------------------------------------------------------------------------------- /spec/init.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const proxyquire = require('proxyquire'); 4 | const stubs = require('./helpers/stubs.js'); 5 | const WindowStateManager = proxyquire('../main.js', stubs.electron().screenRight); 6 | 7 | describe('Initialize and set default values', function() { 8 | let name = 'testWindow'; 9 | let defaultWidth = 1024; 10 | let defaultHeight = 768; 11 | 12 | var wsm = new WindowStateManager(name, { 13 | defaultWidth: defaultWidth, 14 | defaultHeight: defaultHeight 15 | }); 16 | 17 | it("should set the name", () => { 18 | expect(wsm.name).toBe(name); 19 | }); 20 | 21 | it("should set the default width", () => { 22 | expect(wsm.width).toBe(defaultWidth); 23 | }); 24 | 25 | it("should set the default height", () => { 26 | expect(wsm.height).toBe(defaultHeight); 27 | }); 28 | 29 | it("should set the bounds", () => { 30 | expect(wsm.bounds.width).toBe(defaultWidth); 31 | expect(wsm.bounds.height).toBe(defaultHeight); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /spec/io.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const jetpack = require('fs-jetpack'); 5 | const proxyquire = require('proxyquire'); 6 | const stubs = require('./helpers/stubs.js'); 7 | const PseudoBrowserWindow = require('./helpers/pseudoBrowserWindow'); 8 | const WindowStateManager = proxyquire('../main.js', stubs.electron().screenRight); 9 | const utils = require('../src/utils/utils.js'); 10 | 11 | const stateFile = path.join(utils.getAppDataPath(), utils.getManifestData().name, 'windowStates.json'); 12 | 13 | describe('IO', () => { 14 | var name = 'testWindowIO'; 15 | var defaultWidth = 1024; 16 | var defaultHeight = 768; 17 | 18 | it("should handle emtpy file correctly", () => { 19 | let wsm = new WindowStateManager(name, { 20 | defaultWidth: 900, 21 | defaultHeight: 500 22 | }); 23 | 24 | jetpack.write(stateFile, '+### Weird corrupted Json ..1!"2'); 25 | 26 | expect(wsm.width).toBe(900); 27 | expect(wsm.height).toBe(500); 28 | }); 29 | 30 | it("should save the state", () => { 31 | let wsm = new WindowStateManager(name, { 32 | defaultWidth: defaultWidth, 33 | defaultHeight: defaultHeight 34 | }); 35 | 36 | let window = new PseudoBrowserWindow({ 37 | width: 800, 38 | height: 600, 39 | x: 50, 40 | y: 60, 41 | isFullScreen: false, 42 | isMaximized: false 43 | }); 44 | 45 | expect(wsm.saveState(window)).toBe(true); 46 | }); 47 | 48 | it("should load the state", () => { 49 | let wsm = new WindowStateManager(name, { 50 | defaultWidth: defaultWidth, 51 | defaultHeight: defaultHeight 52 | }); 53 | 54 | expect(wsm.width).toBe(800); 55 | expect(wsm.height).toBe(600); 56 | expect(wsm.x).toBe(50); 57 | expect(wsm.y).toBe(60); 58 | expect(wsm.maximized).toBe(false); 59 | }); 60 | 61 | it("should not save on fullscreen window", () => { 62 | let wsm = new WindowStateManager(name, { 63 | defaultWidth: defaultWidth, 64 | defaultHeight: defaultHeight 65 | }); 66 | 67 | let window = new PseudoBrowserWindow({ 68 | width: 1920, 69 | height: 1080, 70 | x: 0, 71 | y: 0, 72 | isFullScreen: true, 73 | isMaximized: false 74 | }); 75 | 76 | expect(wsm.saveState(window)).toBe(false); 77 | }); 78 | 79 | it("should not load saved values from a fullscreen window", () => { 80 | let wsm = new WindowStateManager(name, { 81 | defaultWidth: defaultWidth, 82 | defaultHeight: defaultHeight 83 | }); 84 | 85 | expect(wsm.width).not.toBe(1920); 86 | expect(wsm.height).not.toBe(1080); 87 | expect(wsm.x).not.toBe(0); 88 | expect(wsm.y).not.toBe(0); 89 | expect(wsm.maximized).toBe(false); 90 | }); 91 | 92 | it("should not save the bounds of a maximized window", () => { 93 | let wsm = new WindowStateManager(name, { 94 | defaultWidth: defaultWidth, 95 | defaultHeight: defaultHeight 96 | }); 97 | 98 | let window = new PseudoBrowserWindow({ 99 | width: 1600, 100 | height: 800, 101 | x: 5, 102 | y: 7, 103 | isFullScreen: false, 104 | isMaximized: true 105 | }); 106 | 107 | expect(wsm.saveState(window)).toBe(true); 108 | }); 109 | 110 | it("should not load the bounds of a previously maximized window", () => { 111 | let wsm = new WindowStateManager(name, { 112 | defaultWidth: defaultWidth, 113 | defaultHeight: defaultHeight 114 | }); 115 | 116 | expect(wsm.width).not.toBe(1600); 117 | expect(wsm.height).not.toBe(800); 118 | expect(wsm.x).not.toBe(5); 119 | expect(wsm.y).not.toBe(7); 120 | expect(wsm.maximized).toBe(true); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /spec/screen.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const proxyquire = require('proxyquire'); 4 | const stubs = require('./helpers/stubs.js'); 5 | var WindowStateManager = proxyquire('../main.js', stubs.electron().screenRight); 6 | const PseudoBrowserWindow = require('./helpers/pseudoBrowserWindow'); 7 | 8 | describe('Check multi monitor functionality', function() { 9 | var name = 'testWindowIO'; 10 | var defaultWidth = 1024; 11 | var defaultHeight = 768; 12 | 13 | it("should save the position on the second monitor to the right", () => { 14 | let wsm = new WindowStateManager(name, { 15 | defaultWidth: defaultWidth, 16 | defaultHeight: defaultHeight 17 | }); 18 | 19 | let window = new PseudoBrowserWindow({ 20 | width: 800, 21 | height: 600, 22 | x: 2500, 23 | y: 800, 24 | isFullScreen: false, 25 | isMaximized: false 26 | }); 27 | 28 | expect(wsm.saveState(window)).toBe(true); 29 | }); 30 | 31 | it("should load the window on the second monitor to the right", () => { 32 | let wsm = new WindowStateManager(name, { 33 | defaultWidth: defaultWidth, 34 | defaultHeight: defaultHeight 35 | }); 36 | 37 | expect(wsm.x).toBe(2500); 38 | expect(wsm.y).toBe(800); 39 | }); 40 | 41 | it("should reset window position if out of bounds on right monitor", () => { 42 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenLeft); 43 | let wsm = new WindowStateManager(name, { 44 | defaultWidth: defaultWidth, 45 | defaultHeight: defaultHeight 46 | }); 47 | 48 | expect(wsm.x).not.toBe(2500); 49 | expect(wsm.y).not.toBe(800); 50 | expect(wsm.x).toBe(448); 51 | expect(wsm.y).toBe(156); 52 | }); 53 | 54 | /** 55 | * Left 56 | */ 57 | 58 | 59 | it("should save the position on the second monitor to the left", () => { 60 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenLeft); 61 | let wsm = new WindowStateManager(name, { 62 | defaultWidth: defaultWidth, 63 | defaultHeight: defaultHeight 64 | }); 65 | 66 | let window = new PseudoBrowserWindow({ 67 | width: 800, 68 | height: 600, 69 | x: -900, 70 | y: 800, 71 | isFullScreen: false, 72 | isMaximized: false 73 | }); 74 | 75 | expect(wsm.saveState(window)).toBe(true); 76 | }); 77 | 78 | it("should load the window on the second monitor to the left", () => { 79 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenLeft); 80 | let wsm = new WindowStateManager(name, { 81 | defaultWidth: defaultWidth, 82 | defaultHeight: defaultHeight 83 | }); 84 | 85 | expect(wsm.x).toBe(-900); 86 | expect(wsm.y).toBe(800); 87 | }); 88 | 89 | /** 90 | * Top 91 | */ 92 | 93 | it("should save the position on the second monitor to the top", () => { 94 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenTop); 95 | let wsm = new WindowStateManager(name, { 96 | defaultWidth: defaultWidth, 97 | defaultHeight: defaultHeight 98 | }); 99 | 100 | let window = new PseudoBrowserWindow({ 101 | width: 800, 102 | height: 600, 103 | x: 1200, 104 | y: -700, 105 | isFullScreen: false, 106 | isMaximized: false 107 | }); 108 | 109 | expect(wsm.saveState(window)).toBe(true); 110 | }); 111 | 112 | it("should load the window on the second monitor to the top", () => { 113 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenTop); 114 | let wsm = new WindowStateManager(name, { 115 | defaultWidth: defaultWidth, 116 | defaultHeight: defaultHeight 117 | }); 118 | 119 | expect(wsm.x).toBe(1200); 120 | expect(wsm.y).toBe(-700); 121 | }); 122 | 123 | /** 124 | * Bottom 125 | */ 126 | 127 | it("should save the position on the second monitor to the bottom", () => { 128 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenBottom); 129 | let wsm = new WindowStateManager(name, { 130 | defaultWidth: defaultWidth, 131 | defaultHeight: defaultHeight 132 | }); 133 | 134 | let window = new PseudoBrowserWindow({ 135 | width: 800, 136 | height: 600, 137 | x: 1200, 138 | y: 1500, 139 | isFullScreen: false, 140 | isMaximized: false 141 | }); 142 | 143 | expect(wsm.saveState(window)).toBe(true); 144 | }); 145 | 146 | it("should load the window on the second monitor to the bottom", () => { 147 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenBottom); 148 | let wsm = new WindowStateManager(name, { 149 | defaultWidth: defaultWidth, 150 | defaultHeight: defaultHeight 151 | }); 152 | 153 | expect(wsm.x).toBe(1200); 154 | expect(wsm.y).toBe(1500); 155 | }); 156 | 157 | it("should reset window position if out of bounds on bottom monitor", () => { 158 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenTop); 159 | let wsm = new WindowStateManager(name, { 160 | defaultWidth: defaultWidth, 161 | defaultHeight: defaultHeight 162 | }); 163 | 164 | expect(wsm.x).not.toBe(1200); 165 | expect(wsm.y).not.toBe(1500); 166 | expect(wsm.x).toBe(448); 167 | expect(wsm.y).toBe(156); 168 | }); 169 | 170 | /** 171 | * Triple Monitor 172 | */ 173 | 174 | it("should save the position on the right monitor (triple monitor)", () => { 175 | WindowStateManager = proxyquire('../main.js', stubs.electron().tripleMonitor); 176 | let wsm = new WindowStateManager(name, { 177 | defaultWidth: defaultWidth, 178 | defaultHeight: defaultHeight 179 | }); 180 | 181 | let window = new PseudoBrowserWindow({ 182 | width: 800, 183 | height: 600, 184 | x: 2500, 185 | y: 500, 186 | isFullScreen: false, 187 | isMaximized: false 188 | }); 189 | 190 | expect(wsm.saveState(window)).toBe(true); 191 | }); 192 | 193 | it("should load the position on the right monitor (triple monitor)", () => { 194 | WindowStateManager = proxyquire('../main.js', stubs.electron().tripleMonitor); 195 | let wsm = new WindowStateManager(name, { 196 | defaultWidth: defaultWidth, 197 | defaultHeight: defaultHeight 198 | }); 199 | 200 | expect(wsm.x).toBe(2500); 201 | expect(wsm.y).toBe(500); 202 | }); 203 | 204 | it("should save the position on the left monitor (triple monitor)", () => { 205 | WindowStateManager = proxyquire('../main.js', stubs.electron().tripleMonitor); 206 | let wsm = new WindowStateManager(name, { 207 | defaultWidth: defaultWidth, 208 | defaultHeight: defaultHeight 209 | }); 210 | 211 | let window = new PseudoBrowserWindow({ 212 | width: 800, 213 | height: 600, 214 | x: -800, 215 | y: 500, 216 | isFullScreen: false, 217 | isMaximized: false 218 | }); 219 | 220 | expect(wsm.saveState(window)).toBe(true); 221 | }); 222 | 223 | it("should load the position on the left monitor (triple monitor)", () => { 224 | WindowStateManager = proxyquire('../main.js', stubs.electron().tripleMonitor); 225 | let wsm = new WindowStateManager(name, { 226 | defaultWidth: defaultWidth, 227 | defaultHeight: defaultHeight 228 | }); 229 | 230 | expect(wsm.x).toBe(-800); 231 | expect(wsm.y).toBe(500); 232 | }); 233 | 234 | it("should reset window position if left monitor is unplugged (triple monitor)", () => { 235 | WindowStateManager = proxyquire('../main.js', stubs.electron().screenRight); 236 | let wsm = new WindowStateManager(name, { 237 | defaultWidth: defaultWidth, 238 | defaultHeight: defaultHeight 239 | }); 240 | 241 | expect(wsm.x).not.toBe(-800); 242 | expect(wsm.y).not.toBe(500); 243 | expect(wsm.x).toBe(448); 244 | expect(wsm.y).toBe(156); 245 | }); 246 | }); 247 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /spec/utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const jetpack = require('fs-jetpack'); 6 | const appRoot = require('app-root-path'); 7 | 8 | var getAppDataPath = () => { 9 | let appDataPath; 10 | 11 | switch (os.platform()) { 12 | case 'darwin': 13 | appDataPath = 'Library/Application Support/'; 14 | break; 15 | 16 | case 'linux': 17 | appDataPath = '.config'; 18 | break; 19 | 20 | case 'win32': 21 | appDataPath = 'AppData\\Roaming'; 22 | break; 23 | } 24 | 25 | return path.join(os.homedir(), appDataPath); 26 | }; 27 | 28 | var getManifestData = () => { 29 | let manifest = jetpack.read(path.join(appRoot.toString(), 'package.json'), 'json'); 30 | 31 | return manifest; 32 | }; 33 | 34 | var checkExistingKeys = (object, keys) => { 35 | let noKeyMissing = true; 36 | 37 | for (let val of keys) { 38 | if (typeof object[val] === 'undefined') noKeyMissing = false; 39 | } 40 | 41 | return noKeyMissing; 42 | }; 43 | 44 | var getKeyIndex = (array, name) => { 45 | var found = false; 46 | 47 | for (var i = 0; i < array.length; i++) { 48 | if (Object.keys(array[i])[0] === name) { 49 | found = true; 50 | break; 51 | } 52 | } 53 | 54 | if (found) { 55 | return i; 56 | } else { 57 | return false; 58 | } 59 | }; 60 | 61 | module.exports.getAppDataPath = getAppDataPath; 62 | module.exports.getManifestData = getManifestData; 63 | module.exports.checkExistingKeys = checkExistingKeys; 64 | module.exports.getKeyIndex = getKeyIndex; 65 | -------------------------------------------------------------------------------- /src/lib/checkState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jetpack = require('fs-jetpack'); 4 | const utils = require('../utils/utils'); 5 | 6 | var setDefaultValues = (state, defaultWidth, defaultHeight) => { 7 | state.width = defaultWidth; 8 | state.height = defaultHeight; 9 | state.x = 'undefined'; 10 | state.y = 'undefined'; 11 | state.maximized = false; 12 | 13 | return state; 14 | }; 15 | 16 | var checkSavedValues = (state, defaultWidth, defaultHeight) => { 17 | if ('dimensions' in state) { 18 | if (!('width' in state.dimensions)) { 19 | state.dimensions.width = defaultWidth; 20 | } 21 | 22 | if (!('height' in state.dimensions)) { 23 | state.dimensions.height = defaultHeight; 24 | } 25 | } else { 26 | state.dimensions = {}; 27 | state.dimensions.width = defaultWidth; 28 | state.dimensions.height = defaultHeight; 29 | } 30 | 31 | if ('positions' in state) { 32 | if (!('x' in state.positions) || !('y' in state.positions)) { 33 | state.positions.x = 'undefined'; 34 | state.positions.y = 'undefined'; 35 | } 36 | } else { 37 | state.dimensions = {}; 38 | state.positions.x = 'undefined'; 39 | state.positions.y = 'undefined'; 40 | } 41 | 42 | if ('windowState' in state) { 43 | if (!('maximized' in state.windowState)) { 44 | state.windowState.maximized = false; 45 | } 46 | } else { 47 | state.windowState = {}; 48 | state.windowState.maximized = false; 49 | } 50 | 51 | return state; 52 | }; 53 | 54 | var sync = (name, stateFile, defaultWidth, defaultHeight) => { 55 | var state = {}; 56 | 57 | if(!jetpack.exists(stateFile)) { 58 | state = setDefaultValues(state, defaultWidth, defaultHeight); 59 | } else { 60 | try { 61 | var savedState = jetpack.read(stateFile, 'json'); 62 | var singleState = savedState.states[utils.getKeyIndex(savedState.states, name)]; 63 | 64 | if (typeof singleState !== 'undefined' && singleState !== null) { 65 | let savedName = Object.keys(singleState)[0]; 66 | let secureState = checkSavedValues(singleState[savedName], defaultWidth, defaultHeight); 67 | 68 | state.width = secureState.dimensions.width; 69 | state.height = secureState.dimensions.height; 70 | state.x = secureState.positions.x; 71 | state.y = secureState.positions.y; 72 | state.maximized = secureState.windowState.maximized; 73 | } else { 74 | state = setDefaultValues(state, defaultWidth, defaultHeight); 75 | } 76 | } catch (e) { 77 | state = setDefaultValues(state, defaultWidth, defaultHeight); 78 | } 79 | } 80 | 81 | return state; 82 | }; 83 | 84 | module.exports.sync = sync; 85 | -------------------------------------------------------------------------------- /src/lib/loadState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jetpack = require('fs-jetpack'); 4 | 5 | var sync = (stateFile) => { 6 | if (!jetpack.exists(stateFile)) { 7 | return {states: []}; 8 | } else { 9 | try { 10 | return jetpack.read(stateFile, 'json'); 11 | } catch (e) { 12 | return {states: []}; 13 | } 14 | } 15 | }; 16 | 17 | module.exports.sync = sync; -------------------------------------------------------------------------------- /src/lib/saveState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jetpack = require('fs-jetpack'); 4 | const utils = require('../utils/utils'); 5 | const loadState = require('./loadState'); 6 | 7 | var sync = (name, window, stateFile, defaultWidth, defaultHeight) => { 8 | var existingStates = loadState.sync(stateFile); 9 | var entryIndex = utils.getKeyIndex(existingStates.states, name); 10 | 11 | // We don't want to save the dimensions and position of a fullscreen window 12 | if (!window.isFullScreen()) { 13 | var newState; 14 | 15 | if (!window.isMaximized()) { 16 | newState = { 17 | [name]: { 18 | dimensions: { 19 | width: window.getBounds().width, 20 | height: window.getBounds().height 21 | }, 22 | positions: { 23 | x: window.getBounds().x, 24 | y: window.getBounds().y 25 | }, 26 | windowState: { 27 | maximized: window.isMaximized() 28 | } 29 | } 30 | }; 31 | } else { 32 | if (entryIndex === false) { 33 | newState = { 34 | [name]: { 35 | dimensions: { 36 | width: defaultWidth, 37 | height: defaultHeight 38 | }, 39 | positions: { 40 | x: 'undefined', 41 | y: 'undefined' 42 | }, 43 | windowState: { 44 | maximized: window.isMaximized() 45 | } 46 | } 47 | }; 48 | } else { 49 | newState = existingStates.states[entryIndex]; 50 | newState[name].windowState.maximized = window.isMaximized(); 51 | } 52 | } 53 | 54 | if (entryIndex === false) { 55 | existingStates.states.push(newState); 56 | } else { 57 | existingStates.states[entryIndex] = newState; 58 | } 59 | 60 | try { 61 | jetpack.write(stateFile, existingStates); 62 | return true; 63 | } catch (error) { 64 | return false; 65 | } 66 | } else { 67 | return false; 68 | } 69 | }; 70 | 71 | module.exports.sync = sync; 72 | -------------------------------------------------------------------------------- /src/lib/windowState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const electron = require('electron'); 4 | const path = require('path'); 5 | const jetpack = require('fs-jetpack'); 6 | const checkState = require('./checkState'); 7 | const saveState = require('./saveState'); 8 | 9 | const stateFile = path.join(electron.app.getPath('appData'), electron.app.getName(), 'windowStates.json'); 10 | 11 | 12 | module.exports = class WindowStateManager { 13 | constructor(name, options) { 14 | this._isInitialized = false; 15 | 16 | this._name = name; 17 | this._defaultWidth = options.defaultWidth; 18 | this._defaultHeight = options.defaultHeight; 19 | } 20 | 21 | get name() { 22 | return this._name; 23 | } 24 | 25 | get width() { 26 | this._checkState(); 27 | return this._width; 28 | } 29 | 30 | get height() { 31 | this._checkState(); 32 | return this._height; 33 | } 34 | 35 | get x() { 36 | this._checkState(); 37 | return this._x; 38 | } 39 | 40 | get y() { 41 | this._checkState(); 42 | return this._y; 43 | } 44 | 45 | get maximized() { 46 | this._checkState(); 47 | return this._maximized; 48 | } 49 | 50 | get bounds() { 51 | this._checkState(); 52 | return { 53 | width: this._width, 54 | height: this._height 55 | }; 56 | } 57 | 58 | saveState(window) { 59 | return saveState.sync(this._name, window, stateFile, this._defaultWidth, this._defaultHeight); 60 | } 61 | 62 | _checkState() { 63 | if (!this._isInitialized) { 64 | var _this = this; 65 | let state = checkState.sync(this._name, stateFile, this._defaultWidth, this._defaultHeight); 66 | 67 | this._checkWindowPosition(state, function() { 68 | let primaryDisplayBounds = electron.screen.getPrimaryDisplay().bounds; 69 | 70 | state.width = _this._defaultWidth; 71 | state.height = _this._defaultHeight; 72 | state.x = (primaryDisplayBounds.width - _this._defaultWidth) / 2; 73 | state.y = (primaryDisplayBounds.height - _this._defaultHeight) / 2; 74 | }); 75 | 76 | this._width = state.width; 77 | this._height = state.height; 78 | this._x = state.x; 79 | this._y = state.y; 80 | this._maximized = state.maximized; 81 | 82 | this._isInitialized = true; 83 | } 84 | } 85 | 86 | _checkWindowPosition(state, callback) { 87 | var allDisplays = electron.screen.getAllDisplays(), 88 | primaryDisplay = electron.screen.getPrimaryDisplay(), 89 | inBounds = false; 90 | 91 | //Check if window is in bounds of primary display 92 | if (state.x >= 0 && state.y >= 0 && state.x < primaryDisplay.bounds.width && state.y < primaryDisplay.bounds.height) { 93 | inBounds = true; 94 | } else { 95 | //Find all external displays 96 | var externalDisplays = allDisplays.find((display) => { 97 | return display.bounds.x !== 0 || display.bounds.y !== 0; 98 | }); 99 | 100 | //Check if there are external displays present 101 | if (externalDisplays) { 102 | //Create an array if it is a single display 103 | if (typeof externalDisplays.length === 'undefined') { 104 | let singleExternal = externalDisplays; 105 | externalDisplays = []; 106 | externalDisplays.push(singleExternal); 107 | } 108 | 109 | //Iterate over each display 110 | for (let i = 0; i < externalDisplays.length; i++) { 111 | let display = externalDisplays[i]; 112 | 113 | //Check if window is in bounds of this external display 114 | if (state.x >= display.bounds.x && state.y >= display.bounds.y && state.x < (display.bounds.x + display.bounds.width) && state.y < (display.bounds.y + display.bounds.height)) { 115 | inBounds = true; 116 | } 117 | } 118 | } 119 | } 120 | 121 | if (!inBounds) 122 | callback(); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const os = require('os'); 5 | const jetpack = require('fs-jetpack'); 6 | const appRoot = require('app-root-path'); 7 | 8 | var getAppDataPath = () => { 9 | let appDataPath; 10 | 11 | switch (os.platform()) { 12 | case 'darwin': 13 | appDataPath = 'Library/Application Support/'; 14 | break; 15 | 16 | case 'linux': 17 | appDataPath = '.config'; 18 | break; 19 | 20 | case 'win32': 21 | appDataPath = 'AppData\\Roaming'; 22 | break; 23 | } 24 | 25 | return path.join(os.homedir(), appDataPath); 26 | }; 27 | 28 | var getManifestData = () => { 29 | let manifest = jetpack.read(path.join(appRoot.toString(), 'package.json'), 'json'); 30 | 31 | return manifest; 32 | }; 33 | 34 | var checkExistingKeys = (object, keys) => { 35 | let noKeyMissing = true; 36 | 37 | for (let val of keys) { 38 | if (typeof object[val] === 'undefined') noKeyMissing = false; 39 | } 40 | 41 | return noKeyMissing; 42 | }; 43 | 44 | var getKeyIndex = (array, name) => { 45 | var found = false; 46 | 47 | for (var i = 0; i < array.length; i++) { 48 | if (Object.keys(array[i])[0] === name) { 49 | found = true; 50 | break; 51 | } 52 | } 53 | 54 | if (found) { 55 | return i; 56 | } else { 57 | return false; 58 | } 59 | }; 60 | 61 | module.exports.getAppDataPath = getAppDataPath; 62 | module.exports.getManifestData = getManifestData; 63 | module.exports.checkExistingKeys = checkExistingKeys; 64 | module.exports.getKeyIndex = getKeyIndex; 65 | --------------------------------------------------------------------------------