Decaffeinate AtomWindow

This commit is contained in:
Max Brunsfeld
2018-01-05 10:01:44 -08:00
parent 043f183b1a
commit 822900f40e
2 changed files with 422 additions and 323 deletions

View File

@@ -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)

View File

@@ -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)
}
}