atom-window.js
1 const { BrowserWindow, app, dialog, ipcMain } = require('electron'); 2 const getAppName = require('../get-app-name'); 3 const path = require('path'); 4 const url = require('url'); 5 const { EventEmitter } = require('events'); 6 const StartupTime = require('../startup-time'); 7 8 const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png'); 9 10 let includeShellLoadTime = true; 11 let nextId = 0; 12 13 module.exports = class AtomWindow extends EventEmitter { 14 constructor(atomApplication, fileRecoveryService, settings = {}) { 15 StartupTime.addMarker('main-process:atom-window:start'); 16 17 super(); 18 19 this.id = nextId++; 20 this.atomApplication = atomApplication; 21 this.fileRecoveryService = fileRecoveryService; 22 this.isSpec = settings.isSpec; 23 this.headless = settings.headless; 24 this.safeMode = settings.safeMode; 25 this.devMode = settings.devMode; 26 this.resourcePath = settings.resourcePath; 27 28 const locationsToOpen = settings.locationsToOpen || []; 29 30 this.loadedPromise = new Promise(resolve => { 31 this.resolveLoadedPromise = resolve; 32 }); 33 this.closedPromise = new Promise(resolve => { 34 this.resolveClosedPromise = resolve; 35 }); 36 37 const options = { 38 show: false, 39 title: getAppName(), 40 tabbingIdentifier: 'atom', 41 webPreferences: { 42 // Prevent specs from throttling when the window is in the background: 43 // this should result in faster CI builds, and an improvement in the 44 // local development experience when running specs through the UI (which 45 // now won't pause when e.g. minimizing the window). 46 backgroundThrottling: !this.isSpec, 47 // Disable the `auxclick` feature so that `click` events are triggered in 48 // response to a middle-click. 49 // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) 50 disableBlinkFeatures: 'Auxclick' 51 } 52 }; 53 54 // Don't set icon on Windows so the exe's ico will be used as window and 55 // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. 56 if (process.platform === 'linux') options.icon = ICON_PATH; 57 if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'; 58 if (this.shouldAddCustomInsetTitleBar()) 59 options.titleBarStyle = 'hiddenInset'; 60 if (this.shouldHideTitleBar()) options.frame = false; 61 62 const BrowserWindowConstructor = 63 settings.browserWindowConstructor || BrowserWindow; 64 this.browserWindow = new BrowserWindowConstructor(options); 65 66 Object.defineProperty(this.browserWindow, 'loadSettingsJSON', { 67 get: () => 68 JSON.stringify( 69 Object.assign( 70 { 71 userSettings: !this.isSpec 72 ? this.atomApplication.configFile.get() 73 : null 74 }, 75 this.loadSettings 76 ) 77 ) 78 }); 79 80 this.handleEvents(); 81 82 this.loadSettings = Object.assign({}, settings); 83 this.loadSettings.appVersion = app.getVersion(); 84 this.loadSettings.appName = getAppName(); 85 this.loadSettings.resourcePath = this.resourcePath; 86 this.loadSettings.atomHome = process.env.ATOM_HOME; 87 if (this.loadSettings.devMode == null) this.loadSettings.devMode = false; 88 if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false; 89 if (this.loadSettings.clearWindowState == null) 90 this.loadSettings.clearWindowState = false; 91 92 this.addLocationsToOpen(locationsToOpen); 93 94 this.loadSettings.hasOpenFiles = locationsToOpen.some( 95 location => location.pathToOpen && !location.isDirectory 96 ); 97 this.loadSettings.initialProjectRoots = this.projectRoots; 98 99 StartupTime.addMarker('main-process:atom-window:end'); 100 101 // Expose the startup markers to the renderer process, so we can have unified 102 // measures about startup time between the main process and the renderer process. 103 Object.defineProperty(this.browserWindow, 'startupMarkers', { 104 get: () => { 105 // We only want to make the main process startup data available once, 106 // so if the window is refreshed or a new window is opened, the 107 // renderer process won't use it again. 108 const timingData = StartupTime.exportData(); 109 StartupTime.deleteData(); 110 111 return timingData; 112 } 113 }); 114 115 // Only send to the first non-spec window created 116 if (includeShellLoadTime && !this.isSpec) { 117 includeShellLoadTime = false; 118 if (!this.loadSettings.shellLoadTime) { 119 this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime; 120 } 121 } 122 123 if (!this.loadSettings.env) this.env = this.loadSettings.env; 124 125 this.browserWindow.on('window:loaded', () => { 126 this.disableZoom(); 127 this.emit('window:loaded'); 128 this.resolveLoadedPromise(); 129 }); 130 131 this.browserWindow.on('window:locations-opened', () => { 132 this.emit('window:locations-opened'); 133 }); 134 135 this.browserWindow.on('enter-full-screen', () => { 136 this.browserWindow.webContents.send('did-enter-full-screen'); 137 }); 138 139 this.browserWindow.on('leave-full-screen', () => { 140 this.browserWindow.webContents.send('did-leave-full-screen'); 141 }); 142 143 this.browserWindow.loadURL( 144 url.format({ 145 protocol: 'file', 146 pathname: `${this.resourcePath}/static/index.html`, 147 slashes: true 148 }) 149 ); 150 151 this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this); 152 153 if (this.isSpec) this.browserWindow.focusOnWebView(); 154 155 const hasPathToOpen = !( 156 locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null 157 ); 158 if (hasPathToOpen && !this.isSpecWindow()) 159 this.openLocations(locationsToOpen); 160 } 161 162 hasProjectPaths() { 163 return this.projectRoots.length > 0; 164 } 165 166 setupContextMenu() { 167 const ContextMenu = require('./context-menu'); 168 169 this.browserWindow.on('context-menu', menuTemplate => { 170 return new ContextMenu(menuTemplate, this); 171 }); 172 } 173 174 containsLocations(locations) { 175 return locations.every(location => this.containsLocation(location)); 176 } 177 178 containsLocation(location) { 179 if (!location.pathToOpen) return false; 180 181 return this.projectRoots.some(projectPath => { 182 if (location.pathToOpen === projectPath) return true; 183 if (location.pathToOpen.startsWith(path.join(projectPath, path.sep))) { 184 if (!location.exists) return true; 185 if (!location.isDirectory) return true; 186 } 187 return false; 188 }); 189 } 190 191 handleEvents() { 192 this.browserWindow.on('close', async event => { 193 if ( 194 (!this.atomApplication.quitting || 195 this.atomApplication.quittingForUpdate) && 196 !this.unloading 197 ) { 198 event.preventDefault(); 199 this.unloading = true; 200 this.atomApplication.saveCurrentWindowOptions(false); 201 if (await this.prepareToUnload()) this.close(); 202 } 203 }); 204 205 this.browserWindow.on('closed', () => { 206 this.fileRecoveryService.didCloseWindow(this); 207 this.atomApplication.removeWindow(this); 208 this.resolveClosedPromise(); 209 }); 210 211 this.browserWindow.on('unresponsive', () => { 212 if (this.isSpec) return; 213 dialog.showMessageBox( 214 this.browserWindow, 215 { 216 type: 'warning', 217 buttons: ['Force Close', 'Keep Waiting'], 218 cancelId: 1, // Canceling should be the least destructive action 219 message: 'Editor is not responding', 220 detail: 221 'The editor is not responding. Would you like to force close it or just keep waiting?' 222 }, 223 response => { 224 if (response === 0) this.browserWindow.destroy(); 225 } 226 ); 227 }); 228 229 this.browserWindow.webContents.on('crashed', async () => { 230 if (this.headless) { 231 console.log('Renderer process crashed, exiting'); 232 this.atomApplication.exit(100); 233 return; 234 } 235 236 await this.fileRecoveryService.didCrashWindow(this); 237 dialog.showMessageBox( 238 this.browserWindow, 239 { 240 type: 'warning', 241 buttons: ['Close Window', 'Reload', 'Keep It Open'], 242 cancelId: 2, // Canceling should be the least destructive action 243 message: 'The editor has crashed', 244 detail: 'Please report this issue to https://github.com/atom/atom' 245 }, 246 response => { 247 switch (response) { 248 case 0: 249 return this.browserWindow.destroy(); 250 case 1: 251 return this.browserWindow.reload(); 252 } 253 } 254 ); 255 }); 256 257 this.browserWindow.webContents.on('will-navigate', (event, url) => { 258 if (url !== this.browserWindow.webContents.getURL()) 259 event.preventDefault(); 260 }); 261 262 this.setupContextMenu(); 263 264 // Spec window's web view should always have focus 265 if (this.isSpec) 266 this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView()); 267 } 268 269 async prepareToUnload() { 270 if (this.isSpecWindow()) return true; 271 272 this.lastPrepareToUnloadPromise = new Promise(resolve => { 273 const callback = (event, result) => { 274 if ( 275 BrowserWindow.fromWebContents(event.sender) === this.browserWindow 276 ) { 277 ipcMain.removeListener('did-prepare-to-unload', callback); 278 if (!result) { 279 this.unloading = false; 280 this.atomApplication.quitting = false; 281 } 282 resolve(result); 283 } 284 }; 285 ipcMain.on('did-prepare-to-unload', callback); 286 this.browserWindow.webContents.send('prepare-to-unload'); 287 }); 288 289 return this.lastPrepareToUnloadPromise; 290 } 291 292 openPath(pathToOpen, initialLine, initialColumn) { 293 return this.openLocations([{ pathToOpen, initialLine, initialColumn }]); 294 } 295 296 async openLocations(locationsToOpen) { 297 this.addLocationsToOpen(locationsToOpen); 298 await this.loadedPromise; 299 this.sendMessage('open-locations', locationsToOpen); 300 } 301 302 didChangeUserSettings(settings) { 303 this.sendMessage('did-change-user-settings', settings); 304 } 305 306 didFailToReadUserSettings(message) { 307 this.sendMessage('did-fail-to-read-user-settings', message); 308 } 309 310 addLocationsToOpen(locationsToOpen) { 311 const roots = new Set(this.projectRoots || []); 312 for (const { pathToOpen, isDirectory } of locationsToOpen) { 313 if (isDirectory) { 314 roots.add(pathToOpen); 315 } 316 } 317 318 this.projectRoots = Array.from(roots); 319 this.projectRoots.sort(); 320 } 321 322 replaceEnvironment(env) { 323 this.browserWindow.webContents.send('environment', env); 324 } 325 326 sendMessage(message, detail) { 327 this.browserWindow.webContents.send('message', message, detail); 328 } 329 330 sendCommand(command, ...args) { 331 if (this.isSpecWindow()) { 332 if (!this.atomApplication.sendCommandToFirstResponder(command)) { 333 switch (command) { 334 case 'window:reload': 335 return this.reload(); 336 case 'window:toggle-dev-tools': 337 return this.toggleDevTools(); 338 case 'window:close': 339 return this.close(); 340 } 341 } 342 } else if (this.isWebViewFocused()) { 343 this.sendCommandToBrowserWindow(command, ...args); 344 } else if (!this.atomApplication.sendCommandToFirstResponder(command)) { 345 this.sendCommandToBrowserWindow(command, ...args); 346 } 347 } 348 349 sendURIMessage(uri) { 350 this.browserWindow.webContents.send('uri-message', uri); 351 } 352 353 sendCommandToBrowserWindow(command, ...args) { 354 const action = 355 args[0] && args[0].contextCommand ? 'context-command' : 'command'; 356 this.browserWindow.webContents.send(action, command, ...args); 357 } 358 359 getDimensions() { 360 const [x, y] = Array.from(this.browserWindow.getPosition()); 361 const [width, height] = Array.from(this.browserWindow.getSize()); 362 return { x, y, width, height }; 363 } 364 365 shouldAddCustomTitleBar() { 366 return ( 367 !this.isSpec && 368 process.platform === 'darwin' && 369 this.atomApplication.config.get('core.titleBar') === 'custom' 370 ); 371 } 372 373 shouldAddCustomInsetTitleBar() { 374 return ( 375 !this.isSpec && 376 process.platform === 'darwin' && 377 this.atomApplication.config.get('core.titleBar') === 'custom-inset' 378 ); 379 } 380 381 shouldHideTitleBar() { 382 return ( 383 !this.isSpec && 384 process.platform === 'darwin' && 385 this.atomApplication.config.get('core.titleBar') === 'hidden' 386 ); 387 } 388 389 close() { 390 return this.browserWindow.close(); 391 } 392 393 focus() { 394 return this.browserWindow.focus(); 395 } 396 397 minimize() { 398 return this.browserWindow.minimize(); 399 } 400 401 maximize() { 402 return this.browserWindow.maximize(); 403 } 404 405 unmaximize() { 406 return this.browserWindow.unmaximize(); 407 } 408 409 restore() { 410 return this.browserWindow.restore(); 411 } 412 413 setFullScreen(fullScreen) { 414 return this.browserWindow.setFullScreen(fullScreen); 415 } 416 417 setAutoHideMenuBar(autoHideMenuBar) { 418 return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar); 419 } 420 421 handlesAtomCommands() { 422 return !this.isSpecWindow() && this.isWebViewFocused(); 423 } 424 425 isFocused() { 426 return this.browserWindow.isFocused(); 427 } 428 429 isMaximized() { 430 return this.browserWindow.isMaximized(); 431 } 432 433 isMinimized() { 434 return this.browserWindow.isMinimized(); 435 } 436 437 isWebViewFocused() { 438 return this.browserWindow.isWebViewFocused(); 439 } 440 441 isSpecWindow() { 442 return this.isSpec; 443 } 444 445 reload() { 446 this.loadedPromise = new Promise(resolve => { 447 this.resolveLoadedPromise = resolve; 448 }); 449 this.prepareToUnload().then(canUnload => { 450 if (canUnload) this.browserWindow.reload(); 451 }); 452 return this.loadedPromise; 453 } 454 455 showSaveDialog(options, callback) { 456 options = Object.assign( 457 { 458 title: 'Save File', 459 defaultPath: this.projectRoots[0] 460 }, 461 options 462 ); 463 464 if (typeof callback === 'function') { 465 // Async 466 dialog.showSaveDialog(this.browserWindow, options, callback); 467 } else { 468 // Sync 469 return dialog.showSaveDialog(this.browserWindow, options); 470 } 471 } 472 473 toggleDevTools() { 474 return this.browserWindow.toggleDevTools(); 475 } 476 477 openDevTools() { 478 return this.browserWindow.openDevTools(); 479 } 480 481 closeDevTools() { 482 return this.browserWindow.closeDevTools(); 483 } 484 485 setDocumentEdited(documentEdited) { 486 return this.browserWindow.setDocumentEdited(documentEdited); 487 } 488 489 setRepresentedFilename(representedFilename) { 490 return this.browserWindow.setRepresentedFilename(representedFilename); 491 } 492 493 setProjectRoots(projectRootPaths) { 494 this.projectRoots = projectRootPaths; 495 this.projectRoots.sort(); 496 this.loadSettings.initialProjectRoots = this.projectRoots; 497 return this.atomApplication.saveCurrentWindowOptions(); 498 } 499 500 didClosePathWithWaitSession(path) { 501 this.atomApplication.windowDidClosePathWithWaitSession(this, path); 502 } 503 504 copy() { 505 return this.browserWindow.copy(); 506 } 507 508 disableZoom() { 509 return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1); 510 } 511 512 getLoadedPromise() { 513 return this.loadedPromise; 514 } 515 };