From 822900f40ebbd18b2c4f0fc9f6ddc7440caa4f78 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 5 Jan 2018 10:01:44 -0800 Subject: [PATCH] Decaffeinate AtomWindow --- src/main-process/atom-window.coffee | 323 --------------------- src/main-process/atom-window.js | 422 ++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+), 323 deletions(-) delete mode 100644 src/main-process/atom-window.coffee create mode 100644 src/main-process/atom-window.js diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee deleted file mode 100644 index ca3995c05..000000000 --- a/src/main-process/atom-window.coffee +++ /dev/null @@ -1,323 +0,0 @@ -{BrowserWindow, app, dialog, ipcMain} = require 'electron' -path = require 'path' -fs = require 'fs' -url = require 'url' -{EventEmitter} = require 'events' - -module.exports = -class AtomWindow - Object.assign @prototype, EventEmitter.prototype - - @iconPath: path.resolve(__dirname, '..', '..', 'resources', 'atom.png') - @includeShellLoadTime: true - - browserWindow: null - loaded: null - isSpec: null - - constructor: (@atomApplication, @fileRecoveryService, settings={}) -> - {@resourcePath, pathToOpen, locationsToOpen, @isSpec, @headless, @safeMode, @devMode} = settings - locationsToOpen ?= [{pathToOpen}] if pathToOpen - locationsToOpen ?= [] - - @loadedPromise = new Promise((@resolveLoadedPromise) =>) - @closedPromise = new Promise((@resolveClosedPromise) =>) - - options = - show: false - title: 'Atom' - tabbingIdentifier: 'atom' - webPreferences: - # Prevent specs from throttling when the window is in the background: - # this should result in faster CI builds, and an improvement in the - # local development experience when running specs through the UI (which - # now won't pause when e.g. minimizing the window). - backgroundThrottling: not @isSpec - # Disable the `auxclick` feature so that `click` events are triggered in - # response to a middle-click. - # (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) - disableBlinkFeatures: 'Auxclick' - - # Don't set icon on Windows so the exe's ico will be used as window and - # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. - if process.platform is 'linux' - options.icon = @constructor.iconPath - - if @shouldAddCustomTitleBar() - options.titleBarStyle = 'hidden' - - if @shouldAddCustomInsetTitleBar() - options.titleBarStyle = 'hidden-inset' - - if @shouldHideTitleBar() - options.frame = false - - @browserWindow = new BrowserWindow(options) - @handleEvents() - - @loadSettings = Object.assign({}, settings) - @loadSettings.appVersion = app.getVersion() - @loadSettings.resourcePath = @resourcePath - @loadSettings.devMode ?= false - @loadSettings.safeMode ?= false - @loadSettings.atomHome = process.env.ATOM_HOME - @loadSettings.clearWindowState ?= false - @loadSettings.initialPaths ?= - for {pathToOpen} in locationsToOpen when pathToOpen - stat = fs.statSyncNoException(pathToOpen) or null - if stat?.isDirectory() - pathToOpen - else - parentDirectory = path.dirname(pathToOpen) - if stat?.isFile() or fs.existsSync(parentDirectory) - parentDirectory - else - pathToOpen - @loadSettings.initialPaths.sort() - - # Only send to the first non-spec window created - if @constructor.includeShellLoadTime and not @isSpec - @constructor.includeShellLoadTime = false - @loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime - - @representedDirectoryPaths = @loadSettings.initialPaths - @env = @loadSettings.env if @loadSettings.env? - - @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) - - @browserWindow.on 'window:loaded', => - @disableZoom() - @emit 'window:loaded' - @resolveLoadedPromise() - - @browserWindow.on 'window:locations-opened', => - @emit 'window:locations-opened' - - @browserWindow.on 'enter-full-screen', => - @browserWindow.webContents.send('did-enter-full-screen') - - @browserWindow.on 'leave-full-screen', => - @browserWindow.webContents.send('did-leave-full-screen') - - @browserWindow.loadURL url.format - protocol: 'file' - pathname: "#{@resourcePath}/static/index.html" - slashes: true - - @browserWindow.showSaveDialog = @showSaveDialog.bind(this) - - @browserWindow.focusOnWebView() if @isSpec - @browserWindow.temporaryState = {windowDimensions} if windowDimensions? - - hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) - @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() - - @atomApplication.addWindow(this) - - hasProjectPath: -> @representedDirectoryPaths.length > 0 - - setupContextMenu: -> - ContextMenu = require './context-menu' - - @browserWindow.on 'context-menu', (menuTemplate) => - new ContextMenu(menuTemplate, this) - - containsPaths: (paths) -> - for pathToCheck in paths - return false unless @containsPath(pathToCheck) - true - - containsPath: (pathToCheck) -> - @representedDirectoryPaths.some (projectPath) -> - if not projectPath - false - else if not pathToCheck - false - else if pathToCheck is projectPath - true - else if fs.statSyncNoException(pathToCheck).isDirectory?() - false - else if pathToCheck.indexOf(path.join(projectPath, path.sep)) is 0 - true - else - false - - handleEvents: -> - @browserWindow.on 'close', (event) => - unless @atomApplication.quitting or @unloading - event.preventDefault() - @unloading = true - @atomApplication.saveState(false) - @prepareToUnload().then (result) => - @close() if result - - @browserWindow.on 'closed', => - @fileRecoveryService.didCloseWindow(this) - @atomApplication.removeWindow(this) - @resolveClosedPromise() - - @browserWindow.on 'unresponsive', => - return if @isSpec - - chosen = dialog.showMessageBox @browserWindow, - type: 'warning' - buttons: ['Force Close', 'Keep Waiting'] - message: 'Editor is not responding' - detail: 'The editor is not responding. Would you like to force close it or just keep waiting?' - @browserWindow.destroy() if chosen is 0 - - @browserWindow.webContents.on 'crashed', => - if @headless - console.log "Renderer process crashed, exiting" - @atomApplication.exit(100) - return - - @fileRecoveryService.didCrashWindow(this) - chosen = dialog.showMessageBox @browserWindow, - type: 'warning' - buttons: ['Close Window', 'Reload', 'Keep It Open'] - message: 'The editor has crashed' - detail: 'Please report this issue to https://github.com/atom/atom' - switch chosen - when 0 then @browserWindow.destroy() - when 1 then @browserWindow.reload() - - @browserWindow.webContents.on 'will-navigate', (event, url) => - unless url is @browserWindow.webContents.getURL() - event.preventDefault() - - @setupContextMenu() - - if @isSpec - # Spec window's web view should always have focus - @browserWindow.on 'blur', => - @browserWindow.focusOnWebView() - - prepareToUnload: -> - if @isSpecWindow() - return Promise.resolve(true) - @lastPrepareToUnloadPromise = new Promise (resolve) => - callback = (event, result) => - if BrowserWindow.fromWebContents(event.sender) is @browserWindow - ipcMain.removeListener('did-prepare-to-unload', callback) - unless result - @unloading = false - @atomApplication.quitting = false - resolve(result) - ipcMain.on('did-prepare-to-unload', callback) - @browserWindow.webContents.send('prepare-to-unload') - - openPath: (pathToOpen, initialLine, initialColumn) -> - @openLocations([{pathToOpen, initialLine, initialColumn}]) - - openLocations: (locationsToOpen) -> - @loadedPromise.then => @sendMessage 'open-locations', locationsToOpen - - replaceEnvironment: (env) -> - @browserWindow.webContents.send 'environment', env - - sendMessage: (message, detail) -> - @browserWindow.webContents.send 'message', message, detail - - sendCommand: (command, args...) -> - if @isSpecWindow() - unless @atomApplication.sendCommandToFirstResponder(command) - switch command - when 'window:reload' then @reload() - when 'window:toggle-dev-tools' then @toggleDevTools() - when 'window:close' then @close() - else if @isWebViewFocused() - @sendCommandToBrowserWindow(command, args...) - else - unless @atomApplication.sendCommandToFirstResponder(command) - @sendCommandToBrowserWindow(command, args...) - - sendURIMessage: (uri) -> - @browserWindow.webContents.send 'uri-message', uri - - sendCommandToBrowserWindow: (command, args...) -> - action = if args[0]?.contextCommand then 'context-command' else 'command' - @browserWindow.webContents.send action, command, args... - - getDimensions: -> - [x, y] = @browserWindow.getPosition() - [width, height] = @browserWindow.getSize() - {x, y, width, height} - - shouldAddCustomTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'custom' - - shouldAddCustomInsetTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'custom-inset' - - shouldHideTitleBar: -> - not @isSpec and - process.platform is 'darwin' and - @atomApplication.config.get('core.titleBar') is 'hidden' - - close: -> @browserWindow.close() - - focus: -> @browserWindow.focus() - - minimize: -> @browserWindow.minimize() - - maximize: -> @browserWindow.maximize() - - unmaximize: -> @browserWindow.unmaximize() - - restore: -> @browserWindow.restore() - - setFullScreen: (fullScreen) -> @browserWindow.setFullScreen(fullScreen) - - setAutoHideMenuBar: (autoHideMenuBar) -> @browserWindow.setAutoHideMenuBar(autoHideMenuBar) - - handlesAtomCommands: -> - not @isSpecWindow() and @isWebViewFocused() - - isFocused: -> @browserWindow.isFocused() - - isMaximized: -> @browserWindow.isMaximized() - - isMinimized: -> @browserWindow.isMinimized() - - isWebViewFocused: -> @browserWindow.isWebViewFocused() - - isSpecWindow: -> @isSpec - - reload: -> - @loadedPromise = new Promise((@resolveLoadedPromise) =>) - @prepareToUnload().then (result) => - @browserWindow.reload() if result - @loadedPromise - - showSaveDialog: (params) -> - params = Object.assign({ - title: 'Save File', - defaultPath: @representedDirectoryPaths[0] - }, params) - dialog.showSaveDialog(@browserWindow, params) - - toggleDevTools: -> @browserWindow.toggleDevTools() - - openDevTools: -> @browserWindow.openDevTools() - - closeDevTools: -> @browserWindow.closeDevTools() - - setDocumentEdited: (documentEdited) -> @browserWindow.setDocumentEdited(documentEdited) - - setRepresentedFilename: (representedFilename) -> @browserWindow.setRepresentedFilename(representedFilename) - - setRepresentedDirectoryPaths: (@representedDirectoryPaths) -> - @representedDirectoryPaths.sort() - @loadSettings.initialPaths = @representedDirectoryPaths - @browserWindow.loadSettingsJSON = JSON.stringify(@loadSettings) - @atomApplication.saveState() - - copy: -> @browserWindow.copy() - - disableZoom: -> - @browserWindow.webContents.setVisualZoomLevelLimits(1, 1) diff --git a/src/main-process/atom-window.js b/src/main-process/atom-window.js new file mode 100644 index 000000000..0ed4085fb --- /dev/null +++ b/src/main-process/atom-window.js @@ -0,0 +1,422 @@ +const {BrowserWindow, app, dialog, ipcMain} = require('electron') +const path = require('path') +const fs = require('fs') +const url = require('url') +const {EventEmitter} = require('events') + +const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png') + +let includeShellLoadTime = true +let nextId = 0 + +module.exports = +class AtomWindow extends EventEmitter { + constructor (atomApplication, fileRecoveryService, settings = {}) { + super() + + this.id = nextId++ + this.atomApplication = atomApplication + this.fileRecoveryService = fileRecoveryService + this.isSpec = settings.isSpec + this.headless = settings.headless + this.safeMode = settings.safeMode + this.devMode = settings.devMode + this.resourcePath = settings.resourcePath + + let {pathToOpen, locationsToOpen} = settings + if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}] + if (!locationsToOpen) locationsToOpen = [] + + this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve }) + this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve }) + + const options = { + show: false, + title: 'Atom', + tabbingIdentifier: 'atom', + webPreferences: { + // Prevent specs from throttling when the window is in the background: + // this should result in faster CI builds, and an improvement in the + // local development experience when running specs through the UI (which + // now won't pause when e.g. minimizing the window). + backgroundThrottling: !this.isSpec, + // Disable the `auxclick` feature so that `click` events are triggered in + // response to a middle-click. + // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960) + disableBlinkFeatures: 'Auxclick' + } + } + + // Don't set icon on Windows so the exe's ico will be used as window and + // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more. + if (process.platform === 'linux') options.icon = ICON_PATH + if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden' + if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hidden-inset' + if (this.shouldHideTitleBar()) options.frame = false + this.browserWindow = new BrowserWindow(options) + + this.handleEvents() + + this.loadSettings = Object.assign({}, settings) + this.loadSettings.appVersion = app.getVersion() + this.loadSettings.resourcePath = this.resourcePath + this.loadSettings.atomHome = process.env.ATOM_HOME + if (this.loadSettings.devMode == null) this.loadSettings.devMode = false + if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false + if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false + + if (!this.loadSettings.initialPaths) { + this.loadSettings.initialPaths = [] + for (const {pathToOpen} of locationsToOpen) { + if (!pathToOpen) continue + const stat = fs.statSyncNoException(pathToOpen) || null + if (stat && stat.isDirectory()) { + this.loadSettings.initialPaths.push(pathToOpen) + } else { + const parentDirectory = path.dirname(pathToOpen) + if ((stat && stat.isFile()) || fs.existsSync(parentDirectory)) { + this.loadSettings.initialPaths.push(parentDirectory) + } else { + this.loadSettings.initialPaths.push(pathToOpen) + } + } + } + } + + this.loadSettings.initialPaths.sort() + + // Only send to the first non-spec window created + if (includeShellLoadTime && !this.isSpec) { + includeShellLoadTime = false + if (!this.loadSettings.shellLoadTime) { + this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime + } + } + + this.representedDirectoryPaths = this.loadSettings.initialPaths + if (!this.loadSettings.env) this.env = this.loadSettings.env + + this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings) + + this.browserWindow.on('window:loaded', () => { + this.disableZoom() + this.emit('window:loaded') + this.resolveLoadedPromise() + }) + + this.browserWindow.on('window:locations-opened', () => { + this.emit('window:locations-opened') + }) + + this.browserWindow.on('enter-full-screen', () => { + this.browserWindow.webContents.send('did-enter-full-screen') + }) + + this.browserWindow.on('leave-full-screen', () => { + this.browserWindow.webContents.send('did-leave-full-screen') + }) + + this.browserWindow.loadURL( + url.format({ + protocol: 'file', + pathname: `${this.resourcePath}/static/index.html`, + slashes: true + }) + ) + + this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this) + + if (this.isSpec) this.browserWindow.focusOnWebView() + + const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null) + if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen) + this.atomApplication.addWindow(this) + } + + hasProjectPath () { + return this.representedDirectoryPaths.length > 0 + } + + setupContextMenu () { + const ContextMenu = require('./context-menu') + + this.browserWindow.on('context-menu', menuTemplate => { + return new ContextMenu(menuTemplate, this) + }) + } + + containsPaths (paths) { + return paths.every(p => this.containsPath(p)) + } + + containsPath (pathToCheck) { + if (!pathToCheck) return false + const stat = fs.statSyncNoException(pathToCheck) + if (stat && stat.isDirectory()) return false + + return this.representedDirectoryPaths.some(projectPath => + pathToCheck === projectPath || pathToCheck.startsWith(path.join(projectPath, path.sep)) + ) + } + + handleEvents () { + this.browserWindow.on('close', async event => { + if (!this.atomApplication.quitting && !this.unloading) { + event.preventDefault() + this.unloading = true + this.atomApplication.saveState(false) + if (await this.prepareToUnload()) this.close() + } + }) + + this.browserWindow.on('closed', () => { + this.fileRecoveryService.didCloseWindow(this) + this.atomApplication.removeWindow(this) + this.resolveClosedPromise() + }) + + this.browserWindow.on('unresponsive', () => { + if (this.isSpec) return + const chosen = dialog.showMessageBox(this.browserWindow, { + type: 'warning', + buttons: ['Force Close', 'Keep Waiting'], + message: 'Editor is not responding', + detail: + 'The editor is not responding. Would you like to force close it or just keep waiting?' + }) + if (chosen === 0) this.browserWindow.destroy() + }) + + this.browserWindow.webContents.on('crashed', () => { + if (this.headless) { + console.log('Renderer process crashed, exiting') + this.atomApplication.exit(100) + return + } + + this.fileRecoveryService.didCrashWindow(this) + const chosen = dialog.showMessageBox(this.browserWindow, { + type: 'warning', + buttons: ['Close Window', 'Reload', 'Keep It Open'], + message: 'The editor has crashed', + detail: 'Please report this issue to https://github.com/atom/atom' + }) + switch (chosen) { + case 0: return this.browserWindow.destroy() + case 1: return this.browserWindow.reload() + } + }) + + this.browserWindow.webContents.on('will-navigate', (event, url) => { + if (url !== this.browserWindow.webContents.getURL()) event.preventDefault() + }) + + this.setupContextMenu() + + // Spec window's web view should always have focus + if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView()) + } + + async prepareToUnload () { + if (this.isSpecWindow()) return true + + this.lastPrepareToUnloadPromise = new Promise(resolve => { + const callback = (event, result) => { + if (BrowserWindow.fromWebContents(event.sender) === this.browserWindow) { + ipcMain.removeListener('did-prepare-to-unload', callback) + if (!result) { + this.unloading = false + this.atomApplication.quitting = false + } + resolve(result) + } + } + ipcMain.on('did-prepare-to-unload', callback) + this.browserWindow.webContents.send('prepare-to-unload') + }) + + return this.lastPrepareToUnloadPromise + } + + openPath (pathToOpen, initialLine, initialColumn) { + return this.openLocations([{pathToOpen, initialLine, initialColumn}]) + } + + async openLocations (locationsToOpen) { + await this.loadedPromise + this.sendMessage('open-locations', locationsToOpen) + } + + replaceEnvironment (env) { + this.browserWindow.webContents.send('environment', env) + } + + sendMessage (message, detail) { + this.browserWindow.webContents.send('message', message, detail) + } + + sendCommand (command, ...args) { + if (this.isSpecWindow()) { + if (!this.atomApplication.sendCommandToFirstResponder(command)) { + switch (command) { + case 'window:reload': return this.reload() + case 'window:toggle-dev-tools': return this.toggleDevTools() + case 'window:close': return this.close() + } + } + } else if (this.isWebViewFocused()) { + this.sendCommandToBrowserWindow(command, ...args) + } else if (!this.atomApplication.sendCommandToFirstResponder(command)) { + this.sendCommandToBrowserWindow(command, ...args) + } + } + + sendURIMessage (uri) { + this.browserWindow.webContents.send('uri-message', uri) + } + + sendCommandToBrowserWindow (command, ...args) { + const action = args[0] && args[0].contextCommand + ? 'context-command' + : 'command' + this.browserWindow.webContents.send(action, command, ...args) + } + + getDimensions () { + const [x, y] = Array.from(this.browserWindow.getPosition()) + const [width, height] = Array.from(this.browserWindow.getSize()) + return {x, y, width, height} + } + + shouldAddCustomTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'custom' + ) + } + + shouldAddCustomInsetTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'custom-inset' + ) + } + + shouldHideTitleBar () { + return ( + !this.isSpec && + process.platform === 'darwin' && + this.atomApplication.config.get('core.titleBar') === 'hidden' + ) + } + + close () { + return this.browserWindow.close() + } + + focus () { + return this.browserWindow.focus() + } + + minimize () { + return this.browserWindow.minimize() + } + + maximize () { + return this.browserWindow.maximize() + } + + unmaximize () { + return this.browserWindow.unmaximize() + } + + restore () { + return this.browserWindow.restore() + } + + setFullScreen (fullScreen) { + return this.browserWindow.setFullScreen(fullScreen) + } + + setAutoHideMenuBar (autoHideMenuBar) { + return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar) + } + + handlesAtomCommands () { + return !this.isSpecWindow() && this.isWebViewFocused() + } + + isFocused () { + return this.browserWindow.isFocused() + } + + isMaximized () { + return this.browserWindow.isMaximized() + } + + isMinimized () { + return this.browserWindow.isMinimized() + } + + isWebViewFocused () { + return this.browserWindow.isWebViewFocused() + } + + isSpecWindow () { + return this.isSpec + } + + reload () { + this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve }) + this.prepareToUnload().then(canUnload => { + if (canUnload) this.browserWindow.reload() + }) + return this.loadedPromise + } + + showSaveDialog (params) { + params = Object.assign({ + title: 'Save File', + defaultPath: this.representedDirectoryPaths[0] + }, params) + return dialog.showSaveDialog(this.browserWindow, params) + } + + toggleDevTools () { + return this.browserWindow.toggleDevTools() + } + + openDevTools () { + return this.browserWindow.openDevTools() + } + + closeDevTools () { + return this.browserWindow.closeDevTools() + } + + setDocumentEdited (documentEdited) { + return this.browserWindow.setDocumentEdited(documentEdited) + } + + setRepresentedFilename (representedFilename) { + return this.browserWindow.setRepresentedFilename(representedFilename) + } + + setRepresentedDirectoryPaths (representedDirectoryPaths) { + this.representedDirectoryPaths = representedDirectoryPaths + this.representedDirectoryPaths.sort() + this.loadSettings.initialPaths = this.representedDirectoryPaths + this.browserWindow.loadSettingsJSON = JSON.stringify(this.loadSettings) + return this.atomApplication.saveState() + } + + copy () { + return this.browserWindow.copy() + } + + disableZoom () { + return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1) + } +}