Merge branch 'master' into wl-async-save-dialog

This commit is contained in:
Wliu
2018-01-07 20:55:16 -05:00
60 changed files with 5152 additions and 2842 deletions

View File

@@ -1,161 +0,0 @@
{app, Menu} = require 'electron'
_ = require 'underscore-plus'
MenuHelpers = require '../menu-helpers'
# Used to manage the global application menu.
#
# It's created by {AtomApplication} upon instantiation and used to add, remove
# and maintain the state of all menu items.
module.exports =
class ApplicationMenu
constructor: (@version, @autoUpdateManager) ->
@windowTemplates = new WeakMap()
@setActiveTemplate(@getDefaultTemplate())
@autoUpdateManager.on 'state-changed', (state) => @showUpdateMenuItem(state)
# Public: Updates the entire menu with the given keybindings.
#
# window - The BrowserWindow this menu template is associated with.
# template - The Object which describes the menu to display.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
update: (window, template, keystrokesByCommand) ->
@translateTemplate(template, keystrokesByCommand)
@substituteVersion(template)
@windowTemplates.set(window, template)
@setActiveTemplate(template) if window is @lastFocusedWindow
setActiveTemplate: (template) ->
unless _.isEqual(template, @activeTemplate)
@activeTemplate = template
@menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(@menu)
@showUpdateMenuItem(@autoUpdateManager.getState())
# Register a BrowserWindow with this application menu.
addWindow: (window) ->
@lastFocusedWindow ?= window
focusHandler = =>
@lastFocusedWindow = window
if template = @windowTemplates.get(window)
@setActiveTemplate(template)
window.on 'focus', focusHandler
window.once 'closed', =>
@lastFocusedWindow = null if window is @lastFocusedWindow
@windowTemplates.delete(window)
window.removeListener 'focus', focusHandler
@enableWindowSpecificItems(true)
# Flattens the given menu and submenu items into an single Array.
#
# menu - A complete menu configuration object for atom-shell's menu API.
#
# Returns an Array of native menu items.
flattenMenuItems: (menu) ->
items = []
for index, item of menu.items or {}
items.push(item)
items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu
items
# Flattens the given menu template into an single Array.
#
# template - An object describing the menu item.
#
# Returns an Array of native menu items.
flattenMenuTemplate: (template) ->
items = []
for item in template
items.push(item)
items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu
items
# Public: Used to make all window related menu items are active.
#
# enable - If true enables all window specific items, if false disables all
# window specific items.
enableWindowSpecificItems: (enable) ->
for item in @flattenMenuItems(@menu)
item.enabled = enable if item.metadata?.windowSpecific
return
# Replaces VERSION with the current version.
substituteVersion: (template) ->
if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label is 'VERSION'))
item.label = "Version #{@version}"
# Sets the proper visible state the update menu items
showUpdateMenuItem: (state) ->
checkForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Check for Update')
checkingForUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Checking for Update')
downloadingUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Downloading Update')
installUpdateItem = _.find(@flattenMenuItems(@menu), ({label}) -> label is 'Restart and Install Update')
return unless checkForUpdateItem? and checkingForUpdateItem? and downloadingUpdateItem? and installUpdateItem?
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch state
when 'idle', 'error', 'no-update-available'
checkForUpdateItem.visible = true
when 'checking'
checkingForUpdateItem.visible = true
when 'downloading'
downloadingUpdateItem.visible = true
when 'update-available'
installUpdateItem.visible = true
# Default list of menu items.
#
# Returns an Array of menu item Objects.
getDefaultTemplate: ->
[
label: "Atom"
submenu: [
{label: "Check for Update", metadata: {autoUpdate: true}}
{label: 'Reload', accelerator: 'Command+R', click: => @focusedWindow()?.reload()}
{label: 'Close Window', accelerator: 'Command+Shift+W', click: => @focusedWindow()?.close()}
{label: 'Toggle Dev Tools', accelerator: 'Command+Alt+I', click: => @focusedWindow()?.toggleDevTools()}
{label: 'Quit', accelerator: 'Command+Q', click: -> app.quit()}
]
]
focusedWindow: ->
_.find global.atomApplication.getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Combines a menu template with the appropriate keystroke.
#
# template - An Object conforming to atom-shell's menu api but lacking
# accelerator and click properties.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
#
# Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate: (template, keystrokesByCommand) ->
template.forEach (item) =>
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template
# Determine the accelerator for a given command.
#
# command - The name of the command.
# keystrokesByCommand - An Object where the keys are commands and the values
# are Arrays containing the keystroke.
#
# Returns a String containing the keystroke in a format that can be interpreted
# by Electron to provide nice icons where available.
acceleratorForCommand: (command, keystrokesByCommand) ->
firstKeystroke = keystrokesByCommand[command]?[0]
MenuHelpers.acceleratorForKeystroke(firstKeystroke)

View File

@@ -0,0 +1,225 @@
const {app, Menu} = require('electron')
const _ = require('underscore-plus')
const MenuHelpers = require('../menu-helpers')
// Used to manage the global application menu.
//
// It's created by {AtomApplication} upon instantiation and used to add, remove
// and maintain the state of all menu items.
module.exports =
class ApplicationMenu {
constructor (version, autoUpdateManager) {
this.version = version
this.autoUpdateManager = autoUpdateManager
this.windowTemplates = new WeakMap()
this.setActiveTemplate(this.getDefaultTemplate())
this.autoUpdateManager.on('state-changed', state => this.showUpdateMenuItem(state))
}
// Public: Updates the entire menu with the given keybindings.
//
// window - The BrowserWindow this menu template is associated with.
// template - The Object which describes the menu to display.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
update (window, template, keystrokesByCommand) {
this.translateTemplate(template, keystrokesByCommand)
this.substituteVersion(template)
this.windowTemplates.set(window, template)
if (window === this.lastFocusedWindow) return this.setActiveTemplate(template)
}
setActiveTemplate (template) {
if (!_.isEqual(template, this.activeTemplate)) {
this.activeTemplate = template
this.menu = Menu.buildFromTemplate(_.deepClone(template))
Menu.setApplicationMenu(this.menu)
}
return this.showUpdateMenuItem(this.autoUpdateManager.getState())
}
// Register a BrowserWindow with this application menu.
addWindow (window) {
if (this.lastFocusedWindow == null) this.lastFocusedWindow = window
const focusHandler = () => {
this.lastFocusedWindow = window
const template = this.windowTemplates.get(window)
if (template) this.setActiveTemplate(template)
}
window.on('focus', focusHandler)
window.once('closed', () => {
if (window === this.lastFocusedWindow) this.lastFocusedWindow = null
this.windowTemplates.delete(window)
window.removeListener('focus', focusHandler)
})
this.enableWindowSpecificItems(true)
}
// Flattens the given menu and submenu items into an single Array.
//
// menu - A complete menu configuration object for atom-shell's menu API.
//
// Returns an Array of native menu items.
flattenMenuItems (menu) {
const object = menu.items || {}
let items = []
for (let index in object) {
const item = object[index]
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuItems(item.submenu))
}
return items
}
// Flattens the given menu template into an single Array.
//
// template - An object describing the menu item.
//
// Returns an Array of native menu items.
flattenMenuTemplate (template) {
let items = []
for (let item of template) {
items.push(item)
if (item.submenu) items = items.concat(this.flattenMenuTemplate(item.submenu))
}
return items
}
// Public: Used to make all window related menu items are active.
//
// enable - If true enables all window specific items, if false disables all
// window specific items.
enableWindowSpecificItems (enable) {
for (let item of this.flattenMenuItems(this.menu)) {
if (item.metadata && item.metadata.windowSpecific) item.enabled = enable
}
}
// Replaces VERSION with the current version.
substituteVersion (template) {
let item = this.flattenMenuTemplate(template).find(({label}) => label === 'VERSION')
if (item) item.label = `Version ${this.version}`
}
// Sets the proper visible state the update menu items
showUpdateMenuItem (state) {
const items = this.flattenMenuItems(this.menu)
const checkForUpdateItem = items.find(({label}) => label === 'Check for Update')
const checkingForUpdateItem = items.find(({label}) => label === 'Checking for Update')
const downloadingUpdateItem = items.find(({label}) => label === 'Downloading Update')
const installUpdateItem = items.find(({label}) => label === 'Restart and Install Update')
if (!checkForUpdateItem || !checkingForUpdateItem ||
!downloadingUpdateItem || !installUpdateItem) return
checkForUpdateItem.visible = false
checkingForUpdateItem.visible = false
downloadingUpdateItem.visible = false
installUpdateItem.visible = false
switch (state) {
case 'idle':
case 'error':
case 'no-update-available':
checkForUpdateItem.visible = true
break
case 'checking':
checkingForUpdateItem.visible = true
break
case 'downloading':
downloadingUpdateItem.visible = true
break
case 'update-available':
installUpdateItem.visible = true
break
}
}
// Default list of menu items.
//
// Returns an Array of menu item Objects.
getDefaultTemplate () {
return [{
label: 'Atom',
submenu: [
{
label: 'Check for Update',
metadata: {autoUpdate: true}
},
{
label: 'Reload',
accelerator: 'Command+R',
click: () => {
const window = this.focusedWindow()
if (window) window.reload()
}
},
{
label: 'Close Window',
accelerator: 'Command+Shift+W',
click: () => {
const window = this.focusedWindow()
if (window) window.close()
}
},
{
label: 'Toggle Dev Tools',
accelerator: 'Command+Alt+I',
click: () => {
const window = this.focusedWindow()
if (window) window.toggleDevTools()
}
},
{
label: 'Quit',
accelerator: 'Command+Q',
click: () => app.quit()
}
]
}]
}
focusedWindow () {
return global.atomApplication.getAllWindows().find(window => window.isFocused())
}
// Combines a menu template with the appropriate keystroke.
//
// template - An Object conforming to atom-shell's menu api but lacking
// accelerator and click properties.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a complete menu configuration object for atom-shell's menu API.
translateTemplate (template, keystrokesByCommand) {
template.forEach(item => {
if (item.metadata == null) item.metadata = {}
if (item.command) {
item.accelerator = this.acceleratorForCommand(item.command, keystrokesByCommand)
item.click = () => global.atomApplication.sendCommand(item.command, item.commandDetail)
if (!/^application:/.test(item.command, item.commandDetail)) {
item.metadata.windowSpecific = true
}
}
if (item.submenu) this.translateTemplate(item.submenu, keystrokesByCommand)
})
return template
}
// Determine the accelerator for a given command.
//
// command - The name of the command.
// keystrokesByCommand - An Object where the keys are commands and the values
// are Arrays containing the keystroke.
//
// Returns a String containing the keystroke in a format that can be interpreted
// by Electron to provide nice icons where available.
acceleratorForCommand (command, keystrokesByCommand) {
const firstKeystroke = keystrokesByCommand[command] && keystrokesByCommand[command][0]
return MenuHelpers.acceleratorForKeystroke(firstKeystroke)
}
}

View File

@@ -1,917 +0,0 @@
AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
StorageFolder = require '../storage-folder'
Config = require '../config'
FileRecoveryService = require './file-recovery-service'
ipcHelpers = require '../ipc-helpers'
{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron'
{CompositeDisposable, Disposable} = require 'event-kit'
fs = require 'fs-plus'
path = require 'path'
os = require 'os'
net = require 'net'
url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
FindParentDir = null
Resolve = null
ConfigSchema = require '../config-schema'
LocationSuffixRegExp = /(:\d+)(:\d+)?$/
# The application's singleton class.
#
# It's the entry point into the Atom application and maintains the global state
# of the application.
#
module.exports =
class AtomApplication
Object.assign @prototype, EventEmitter.prototype
# Public: The entry point into the Atom application.
@open: (options) ->
unless options.socketPath?
if process.platform is 'win32'
userNameSafe = new Buffer(process.env.USERNAME).toString('base64')
options.socketPath = "\\\\.\\pipe\\atom-#{options.version}-#{userNameSafe}-#{process.arch}-sock"
else
options.socketPath = path.join(os.tmpdir(), "atom-#{options.version}-#{process.env.USER}.sock")
# FIXME: Sometimes when socketPath doesn't exist, net.connect would strangely
# take a few seconds to trigger 'error' event, it could be a bug of node
# or atom-shell, before it's fixed we check the existence of socketPath to
# speedup startup.
if (process.platform isnt 'win32' and not fs.existsSync options.socketPath) or options.test or options.benchmark or options.benchmarkTest
new AtomApplication(options).initialize(options)
return
client = net.connect {path: options.socketPath}, ->
client.write JSON.stringify(options), ->
client.end()
app.quit()
client.on 'error', -> new AtomApplication(options).initialize(options)
windows: null
applicationMenu: null
atomProtocolHandler: null
resourcePath: null
version: null
quitting: false
exit: (status) -> app.exit(status)
constructor: (options) ->
{@resourcePath, @devResourcePath, @version, @devMode, @safeMode, @socketPath, @logFile, @userDataDir} = options
@socketPath = null if options.test or options.benchmark or options.benchmarkTest
@pidsToOpenWindows = {}
@windowStack = new WindowStack()
@config = new Config({enablePersistence: true})
@config.setSchema null, {type: 'object', properties: _.clone(ConfigSchema)}
ConfigSchema.projectHome = {
type: 'string',
default: path.join(fs.getHomeDirectory(), 'github'),
description: 'The directory where projects are assumed to be located. Packages created using the Package Generator will be stored here by default.'
}
@config.initialize({configDirPath: process.env.ATOM_HOME, @resourcePath, projectHomeSchema: ConfigSchema.projectHome})
@config.load()
@fileRecoveryService = new FileRecoveryService(path.join(process.env.ATOM_HOME, "recovery"))
@storageFolder = new StorageFolder(process.env.ATOM_HOME)
@autoUpdateManager = new AutoUpdateManager(
@version,
options.test or options.benchmark or options.benchmarkTest,
@config
)
@disposable = new CompositeDisposable
@handleEvents()
# This stuff was previously done in the constructor, but we want to be able to construct this object
# for testing purposes without booting up the world. As you add tests, feel free to move instantiation
# of these various sub-objects into the constructor, but you'll need to remove the side-effects they
# perform during their construction, adding an initialize method that you call here.
initialize: (options) ->
global.atomApplication = this
# DEPRECATED: This can be removed at some point (added in 1.13)
# It converts `useCustomTitleBar: true` to `titleBar: "custom"`
if process.platform is 'darwin' and @config.get('core.useCustomTitleBar')
@config.unset('core.useCustomTitleBar')
@config.set('core.titleBar', 'custom')
@config.onDidChange 'core.titleBar', @promptForRestart.bind(this)
process.nextTick => @autoUpdateManager.initialize()
@applicationMenu = new ApplicationMenu(@version, @autoUpdateManager)
@atomProtocolHandler = new AtomProtocolHandler(@resourcePath, @safeMode)
@listenForArgumentsFromNewProcess()
@setupDockMenu()
@launch(options)
destroy: ->
windowsClosePromises = @getAllWindows().map (window) ->
window.close()
window.closedPromise
Promise.all(windowsClosePromises).then(=> @disposable.dispose())
launch: (options) ->
if options.test or options.benchmark or options.benchmarkTest
@openWithOptions(options)
else if options.pathsToOpen?.length > 0 or options.urlsToOpen?.length > 0
if @config.get('core.restorePreviousWindowsOnStart') is 'always'
@loadState(_.deepClone(options))
@openWithOptions(options)
else
@loadState(options) or @openPath(options)
openWithOptions: (options) ->
{
initialPaths, pathsToOpen, executedFrom, urlsToOpen, benchmark,
benchmarkTest, test, pidToKillWhenClosed, devMode, safeMode, newWindow,
logFile, profileStartup, timeout, clearWindowState, addToLastWindow, env
} = options
app.focus()
if test
@runTests({
headless: true, devMode, @resourcePath, executedFrom, pathsToOpen,
logFile, timeout, env
})
else if benchmark or benchmarkTest
@runBenchmarks({headless: true, test: benchmarkTest, @resourcePath, executedFrom, pathsToOpen, timeout, env})
else if pathsToOpen.length > 0
@openPaths({
initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow,
devMode, safeMode, profileStartup, clearWindowState, addToLastWindow, env
})
else if urlsToOpen.length > 0
for urlToOpen in urlsToOpen
@openUrl({urlToOpen, devMode, safeMode, env})
else
# Always open a editor window if this is the first instance of Atom.
@openPath({
initialPaths, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup,
clearWindowState, addToLastWindow, env
})
# Public: Removes the {AtomWindow} from the global window list.
removeWindow: (window) ->
@windowStack.removeWindow(window)
if @getAllWindows().length is 0
@applicationMenu?.enableWindowSpecificItems(false)
if process.platform in ['win32', 'linux']
app.quit()
return
@saveState(true) unless window.isSpec
# Public: Adds the {AtomWindow} to the global window list.
addWindow: (window) ->
@windowStack.addWindow(window)
@applicationMenu?.addWindow(window.browserWindow)
window.once 'window:loaded', =>
@autoUpdateManager?.emitUpdateAvailableEvent(window)
unless window.isSpec
focusHandler = => @windowStack.touch(window)
blurHandler = => @saveState(false)
window.browserWindow.on 'focus', focusHandler
window.browserWindow.on 'blur', blurHandler
window.browserWindow.once 'closed', =>
@windowStack.removeWindow(window)
window.browserWindow.removeListener 'focus', focusHandler
window.browserWindow.removeListener 'blur', blurHandler
window.browserWindow.webContents.once 'did-finish-load', => @saveState(false)
getAllWindows: =>
@windowStack.all().slice()
getLastFocusedWindow: (predicate) =>
@windowStack.getLastFocusedWindow(predicate)
# Creates server to listen for additional atom application launches.
#
# You can run the atom command multiple times, but after the first launch
# the other launches will just pass their information to this server and then
# close immediately.
listenForArgumentsFromNewProcess: ->
return unless @socketPath?
@deleteSocketFile()
server = net.createServer (connection) =>
data = ''
connection.on 'data', (chunk) ->
data = data + chunk
connection.on 'end', =>
options = JSON.parse(data)
@openWithOptions(options)
server.listen @socketPath
server.on 'error', (error) -> console.error 'Application server failed', error
deleteSocketFile: ->
return if process.platform is 'win32' or not @socketPath?
if fs.existsSync(@socketPath)
try
fs.unlinkSync(@socketPath)
catch error
# Ignore ENOENT errors in case the file was deleted between the exists
# check and the call to unlink sync. This occurred occasionally on CI
# which is why this check is here.
throw error unless error.code is 'ENOENT'
# Registers basic application commands, non-idempotent.
handleEvents: ->
getLoadSettings = =>
devMode: @focusedWindow()?.devMode
safeMode: @focusedWindow()?.safeMode
@on 'application:quit', -> app.quit()
@on 'application:new-window', -> @openPath(getLoadSettings())
@on 'application:new-file', -> (@focusedWindow() ? this).openPath()
@on 'application:open-dev', -> @promptForPathToOpen('all', devMode: true)
@on 'application:open-safe', -> @promptForPathToOpen('all', safeMode: true)
@on 'application:inspect', ({x, y, atomWindow}) ->
atomWindow ?= @focusedWindow()
atomWindow?.browserWindow.inspectElement(x, y)
@on 'application:open-documentation', -> shell.openExternal('http://flight-manual.atom.io/')
@on 'application:open-discussions', -> shell.openExternal('https://discuss.atom.io')
@on 'application:open-faq', -> shell.openExternal('https://atom.io/faq')
@on 'application:open-terms-of-use', -> shell.openExternal('https://atom.io/terms')
@on 'application:report-issue', -> shell.openExternal('https://github.com/atom/atom/blob/master/CONTRIBUTING.md#reporting-bugs')
@on 'application:search-issues', -> shell.openExternal('https://github.com/search?q=+is%3Aissue+user%3Aatom')
@on 'application:install-update', =>
@quitting = true
@autoUpdateManager.install()
@on 'application:check-for-update', => @autoUpdateManager.check()
if process.platform is 'darwin'
@on 'application:bring-all-windows-to-front', -> Menu.sendActionToFirstResponder('arrangeInFront:')
@on 'application:hide', -> Menu.sendActionToFirstResponder('hide:')
@on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
@on 'application:minimize', -> Menu.sendActionToFirstResponder('performMiniaturize:')
@on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
@on 'application:zoom', -> Menu.sendActionToFirstResponder('zoom:')
else
@on 'application:minimize', -> @focusedWindow()?.minimize()
@on 'application:zoom', -> @focusedWindow()?.maximize()
@openPathOnEvent('application:about', 'atom://about')
@openPathOnEvent('application:show-settings', 'atom://config')
@openPathOnEvent('application:open-your-config', 'atom://.atom/config')
@openPathOnEvent('application:open-your-init-script', 'atom://.atom/init-script')
@openPathOnEvent('application:open-your-keymap', 'atom://.atom/keymap')
@openPathOnEvent('application:open-your-snippets', 'atom://.atom/snippets')
@openPathOnEvent('application:open-your-stylesheet', 'atom://.atom/stylesheet')
@openPathOnEvent('application:open-license', path.join(process.resourcesPath, 'LICENSE.md'))
@disposable.add ipcHelpers.on app, 'before-quit', (event) =>
resolveBeforeQuitPromise = null
@lastBeforeQuitPromise = new Promise((resolve) -> resolveBeforeQuitPromise = resolve)
if @quitting
resolveBeforeQuitPromise()
else
event.preventDefault()
@quitting = true
windowUnloadPromises = @getAllWindows().map((window) -> window.prepareToUnload())
Promise.all(windowUnloadPromises).then((windowUnloadedResults) ->
didUnloadAllWindows = windowUnloadedResults.every((didUnloadWindow) -> didUnloadWindow)
app.quit() if didUnloadAllWindows
resolveBeforeQuitPromise()
)
@disposable.add ipcHelpers.on app, 'will-quit', =>
@killAllProcesses()
@deleteSocketFile()
@disposable.add ipcHelpers.on app, 'open-file', (event, pathToOpen) =>
event.preventDefault()
@openPath({pathToOpen})
@disposable.add ipcHelpers.on app, 'open-url', (event, urlToOpen) =>
event.preventDefault()
@openUrl({urlToOpen, @devMode, @safeMode})
@disposable.add ipcHelpers.on app, 'activate', (event, hasVisibleWindows) =>
unless hasVisibleWindows
event?.preventDefault()
@emit('application:new-window')
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'resolve-proxy', (event, requestId, url) ->
event.sender.session.resolveProxy url, (proxy) ->
unless event.sender.isDestroyed()
event.sender.send('did-resolve-proxy', requestId, proxy)
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @getAllWindows()
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)
if options?
if typeof options.pathsToOpen is 'string'
options.pathsToOpen = [options.pathsToOpen]
if options.pathsToOpen?.length > 0
options.window = window
@openPaths(options)
else
new AtomWindow(this, @fileRecoveryService, options)
else
@promptForPathToOpen('all', {window})
@disposable.add ipcHelpers.on ipcMain, 'update-application-menu', (event, template, keystrokesByCommand) =>
win = BrowserWindow.fromWebContents(event.sender)
@applicationMenu?.update(win, template, keystrokesByCommand)
@disposable.add ipcHelpers.on ipcMain, 'run-package-specs', (event, packageSpecPath) =>
@runTests({resourcePath: @devResourcePath, pathsToOpen: [packageSpecPath], headless: false})
@disposable.add ipcHelpers.on ipcMain, 'run-benchmarks', (event, benchmarksPath) =>
@runBenchmarks({resourcePath: @devResourcePath, pathsToOpen: [benchmarksPath], headless: false, test: false})
@disposable.add ipcHelpers.on ipcMain, 'command', (event, command) =>
@emit(command)
@disposable.add ipcHelpers.on ipcMain, 'open-command', (event, command, args...) =>
defaultPath = args[0] if args.length > 0
switch command
when 'application:open' then @promptForPathToOpen('all', getLoadSettings(), defaultPath)
when 'application:open-file' then @promptForPathToOpen('file', getLoadSettings(), defaultPath)
when 'application:open-folder' then @promptForPathToOpen('folder', getLoadSettings(), defaultPath)
else console.log "Invalid open-command received: " + command
@disposable.add ipcHelpers.on ipcMain, 'window-command', (event, command, args...) ->
win = BrowserWindow.fromWebContents(event.sender)
win.emit(command, args...)
@disposable.add ipcHelpers.respondTo 'window-method', (browserWindow, method, args...) =>
@atomWindowForBrowserWindow(browserWindow)?[method](args...)
@disposable.add ipcHelpers.on ipcMain, 'pick-folder', (event, responseChannel) =>
@promptForPath "folder", (selectedPaths) ->
event.sender.send(responseChannel, selectedPaths)
@disposable.add ipcHelpers.respondTo 'set-window-size', (win, width, height) ->
win.setSize(width, height)
@disposable.add ipcHelpers.respondTo 'set-window-position', (win, x, y) ->
win.setPosition(x, y)
@disposable.add ipcHelpers.respondTo 'center-window', (win) ->
win.center()
@disposable.add ipcHelpers.respondTo 'focus-window', (win) ->
win.focus()
@disposable.add ipcHelpers.respondTo 'show-window', (win) ->
win.show()
@disposable.add ipcHelpers.respondTo 'hide-window', (win) ->
win.hide()
@disposable.add ipcHelpers.respondTo 'get-temporary-window-state', (win) ->
win.temporaryState
@disposable.add ipcHelpers.respondTo 'set-temporary-window-state', (win, state) ->
win.temporaryState = state
clipboard = require '../safe-clipboard'
@disposable.add ipcHelpers.on ipcMain, 'write-text-to-selection-clipboard', (event, selectedText) ->
clipboard.writeText(selectedText, 'selection')
@disposable.add ipcHelpers.on ipcMain, 'write-to-stdout', (event, output) ->
process.stdout.write(output)
@disposable.add ipcHelpers.on ipcMain, 'write-to-stderr', (event, output) ->
process.stderr.write(output)
@disposable.add ipcHelpers.on ipcMain, 'add-recent-document', (event, filename) ->
app.addRecentDocument(filename)
@disposable.add ipcHelpers.on ipcMain, 'execute-javascript-in-dev-tools', (event, code) ->
event.sender.devToolsWebContents?.executeJavaScript(code)
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-state', (event) =>
event.returnValue = @autoUpdateManager.getState()
@disposable.add ipcHelpers.on ipcMain, 'get-auto-update-manager-error', (event) =>
event.returnValue = @autoUpdateManager.getErrorMessage()
@disposable.add ipcHelpers.on ipcMain, 'will-save-path', (event, path) =>
@fileRecoveryService.willSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-save-path', (event, path) =>
@fileRecoveryService.didSavePath(@atomWindowForEvent(event), path)
event.returnValue = true
@disposable.add ipcHelpers.on ipcMain, 'did-change-paths', =>
@saveState(false)
@disposable.add(@disableZoomOnDisplayChange())
setupDockMenu: ->
if process.platform is 'darwin'
dockMenu = Menu.buildFromTemplate [
{label: 'New Window', click: => @emit('application:new-window')}
]
app.dock.setMenu dockMenu
# Public: Executes the given command.
#
# If it isn't handled globally, delegate to the currently focused window.
#
# command - The string representing the command.
# args - The optional arguments to pass along.
sendCommand: (command, args...) ->
unless @emit(command, args...)
focusedWindow = @focusedWindow()
if focusedWindow?
focusedWindow.sendCommand(command, args...)
else
@sendCommandToFirstResponder(command)
# Public: Executes the given command on the given window.
#
# command - The string representing the command.
# atomWindow - The {AtomWindow} to send the command to.
# args - The optional arguments to pass along.
sendCommandToWindow: (command, atomWindow, args...) ->
unless @emit(command, args...)
if atomWindow?
atomWindow.sendCommand(command, args...)
else
@sendCommandToFirstResponder(command)
# Translates the command into macOS action and sends it to application's first
# responder.
sendCommandToFirstResponder: (command) ->
return false unless process.platform is 'darwin'
switch command
when 'core:undo' then Menu.sendActionToFirstResponder('undo:')
when 'core:redo' then Menu.sendActionToFirstResponder('redo:')
when 'core:copy' then Menu.sendActionToFirstResponder('copy:')
when 'core:cut' then Menu.sendActionToFirstResponder('cut:')
when 'core:paste' then Menu.sendActionToFirstResponder('paste:')
when 'core:select-all' then Menu.sendActionToFirstResponder('selectAll:')
else return false
true
# Public: Open the given path in the focused window when the event is
# triggered.
#
# A new window will be created if there is no currently focused window.
#
# eventName - The event to listen for.
# pathToOpen - The path to open when the event is triggered.
openPathOnEvent: (eventName, pathToOpen) ->
@on eventName, ->
if window = @focusedWindow()
window.openPath(pathToOpen)
else
@openPath({pathToOpen})
# Returns the {AtomWindow} for the given paths.
windowForPaths: (pathsToOpen, devMode) ->
_.find @getAllWindows(), (atomWindow) ->
atomWindow.devMode is devMode and atomWindow.containsPaths(pathsToOpen)
# Returns the {AtomWindow} for the given ipcMain event.
atomWindowForEvent: ({sender}) ->
@atomWindowForBrowserWindow(BrowserWindow.fromWebContents(sender))
atomWindowForBrowserWindow: (browserWindow) ->
@getAllWindows().find((atomWindow) -> atomWindow.browserWindow is browserWindow)
# Public: Returns the currently focused {AtomWindow} or undefined if none.
focusedWindow: ->
_.find @getAllWindows(), (atomWindow) -> atomWindow.isFocused()
# Get the platform-specific window offset for new windows.
getWindowOffsetForCurrentPlatform: ->
offsetByPlatform =
darwin: 22
win32: 26
offsetByPlatform[process.platform] ? 0
# Get the dimensions for opening a new window by cascading as appropriate to
# the platform.
getDimensionsForNewWindow: ->
return if (@focusedWindow() ? @getLastFocusedWindow())?.isMaximized()
dimensions = (@focusedWindow() ? @getLastFocusedWindow())?.getDimensions()
offset = @getWindowOffsetForCurrentPlatform()
if dimensions? and offset?
dimensions.x += offset
dimensions.y += offset
dimensions
# Public: Opens a single path, in an existing window if possible.
#
# options -
# :pathToOpen - The file path to open
# :pidToKillWhenClosed - The integer of the pid to kill
# :newWindow - Boolean of whether this should be opened in a new window.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :profileStartup - Boolean to control creating a profile of the startup time.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPath: ({initialPaths, pathToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env} = {}) ->
@openPaths({initialPaths, pathsToOpen: [pathToOpen], pidToKillWhenClosed, newWindow, devMode, safeMode, profileStartup, window, clearWindowState, addToLastWindow, env})
# Public: Opens multiple paths, in existing windows if possible.
#
# options -
# :pathsToOpen - The array of file paths to open
# :pidToKillWhenClosed - The integer of the pid to kill
# :newWindow - Boolean of whether this should be opened in a new window.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
# :windowDimensions - Object with height and width keys.
# :window - {AtomWindow} to open file paths in.
# :addToLastWindow - Boolean of whether this should be opened in last focused window.
openPaths: ({initialPaths, pathsToOpen, executedFrom, pidToKillWhenClosed, newWindow, devMode, safeMode, windowDimensions, profileStartup, window, clearWindowState, addToLastWindow, env}={}) ->
if not pathsToOpen? or pathsToOpen.length is 0
return
env = process.env unless env?
devMode = Boolean(devMode)
safeMode = Boolean(safeMode)
clearWindowState = Boolean(clearWindowState)
locationsToOpen = (@locationForPathToOpen(pathToOpen, executedFrom, addToLastWindow) for pathToOpen in pathsToOpen)
pathsToOpen = (locationToOpen.pathToOpen for locationToOpen in locationsToOpen)
unless pidToKillWhenClosed or newWindow
existingWindow = @windowForPaths(pathsToOpen, devMode)
stats = (fs.statSyncNoException(pathToOpen) for pathToOpen in pathsToOpen)
unless existingWindow?
if currentWindow = window ? @getLastFocusedWindow()
existingWindow = currentWindow if (
addToLastWindow or
currentWindow.devMode is devMode and
(
stats.every((stat) -> stat.isFile?()) or
stats.some((stat) -> stat.isDirectory?() and not currentWindow.hasProjectPath())
)
)
if existingWindow?
openedWindow = existingWindow
openedWindow.openLocations(locationsToOpen)
if openedWindow.isMinimized()
openedWindow.restore()
else
openedWindow.focus()
openedWindow.replaceEnvironment(env)
else
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
resourcePath ?= @resourcePath
windowDimensions ?= @getDimensionsForNewWindow()
openedWindow = new AtomWindow(this, @fileRecoveryService, {initialPaths, locationsToOpen, windowInitializationScript, resourcePath, devMode, safeMode, windowDimensions, profileStartup, clearWindowState, env})
openedWindow.focus()
@windowStack.addWindow(openedWindow)
if pidToKillWhenClosed?
@pidsToOpenWindows[pidToKillWhenClosed] = openedWindow
openedWindow.browserWindow.once 'closed', =>
@killProcessForWindow(openedWindow)
openedWindow
# Kill all processes associated with opened windows.
killAllProcesses: ->
@killProcess(pid) for pid of @pidsToOpenWindows
return
# Kill process associated with the given opened window.
killProcessForWindow: (openedWindow) ->
for pid, trackedWindow of @pidsToOpenWindows
@killProcess(pid) if trackedWindow is openedWindow
return
# Kill the process with the given pid.
killProcess: (pid) ->
try
parsedPid = parseInt(pid)
process.kill(parsedPid) if isFinite(parsedPid)
catch error
if error.code isnt 'ESRCH'
console.log("Killing process #{pid} failed: #{error.code ? error.message}")
delete @pidsToOpenWindows[pid]
saveState: (allowEmpty=false) ->
return if @quitting
states = []
for window in @getAllWindows()
unless window.isSpec
states.push({initialPaths: window.representedDirectoryPaths})
states.reverse()
if states.length > 0 or allowEmpty
@storageFolder.storeSync('application.json', states)
@emit('application:did-save-state')
loadState: (options) ->
if (@config.get('core.restorePreviousWindowsOnStart') in ['yes', 'always']) and (states = @storageFolder.load('application.json'))?.length > 0
for state in states
@openWithOptions(Object.assign(options, {
initialPaths: state.initialPaths
pathsToOpen: state.initialPaths.filter (directoryPath) -> fs.isDirectorySync(directoryPath)
urlsToOpen: []
devMode: @devMode
safeMode: @safeMode
}))
else
null
# Open an atom:// url.
#
# The host of the URL being opened is assumed to be the package name
# responsible for opening the URL. A new window will be created with
# that package's `urlMain` as the bootstrap script.
#
# options -
# :urlToOpen - The atom:// url to open.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
openUrl: ({urlToOpen, devMode, safeMode, env}) ->
parsedUrl = url.parse(urlToOpen, true)
return unless parsedUrl.protocol is "atom:"
pack = @findPackageWithName(parsedUrl.host, devMode)
if pack?.urlMain
@openPackageUrlMain(parsedUrl.host, pack.urlMain, urlToOpen, devMode, safeMode, env)
else
@openPackageUriHandler(urlToOpen, parsedUrl, devMode, safeMode, env)
openPackageUriHandler: (url, parsedUrl, devMode, safeMode, env) ->
bestWindow = null
if parsedUrl.host is 'core'
predicate = require('../core-uri-handlers').windowPredicate(parsedUrl)
bestWindow = @getLastFocusedWindow (win) ->
not win.isSpecWindow() and predicate(win)
bestWindow ?= @getLastFocusedWindow (win) -> not win.isSpecWindow()
if bestWindow?
bestWindow.sendURIMessage url
bestWindow.focus()
else
resourcePath = @resourcePath
if devMode
try
windowInitializationScript = require.resolve(path.join(@devResourcePath, 'src', 'initialize-application-window'))
resourcePath = @devResourcePath
windowInitializationScript ?= require.resolve('../initialize-application-window')
windowDimensions = @getDimensionsForNewWindow()
win = new AtomWindow(this, @fileRecoveryService, {resourcePath, windowInitializationScript, devMode, safeMode, windowDimensions, env})
@windowStack.addWindow(win)
win.on 'window:loaded', ->
win.sendURIMessage url
findPackageWithName: (packageName, devMode) ->
_.find @getPackageManager(devMode).getAvailablePackageMetadata(), ({name}) -> name is packageName
openPackageUrlMain: (packageName, packageUrlMain, urlToOpen, devMode, safeMode, env) ->
packagePath = @getPackageManager(devMode).resolvePackagePath(packageName)
windowInitializationScript = path.resolve(packagePath, packageUrlMain)
windowDimensions = @getDimensionsForNewWindow()
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, @resourcePath, devMode, safeMode, urlToOpen, windowDimensions, env})
getPackageManager: (devMode) ->
unless @packages?
PackageManager = require '../package-manager'
@packages = new PackageManager({})
@packages.initialize
configDirPath: process.env.ATOM_HOME
devMode: devMode
resourcePath: @resourcePath
@packages
# Opens up a new {AtomWindow} to run specs within.
#
# options -
# :headless - A Boolean that, if true, will close the window upon
# completion.
# :resourcePath - The path to include specs from.
# :specPath - The directory to load specs from.
# :safeMode - A Boolean that, if true, won't run specs from ~/.atom/packages
# and ~/.atom/dev/packages, defaults to false.
runTests: ({headless, resourcePath, executedFrom, pathsToOpen, logFile, safeMode, timeout, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
timeoutInSeconds = Number.parseFloat(timeout)
unless Number.isNaN(timeoutInSeconds)
timeoutHandler = ->
console.log "The test suite has timed out because it has been running for more than #{timeoutInSeconds} seconds."
process.exit(124) # Use the same exit code as the UNIX timeout util.
setTimeout(timeoutHandler, timeoutInSeconds * 1000)
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-test-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-test-window'))
testPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
testPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if testPaths.length is 0
process.stderr.write 'Error: Specify at least one test path\n\n'
process.exit(1)
legacyTestRunnerPath = @resolveLegacyTestRunnerPath()
testRunnerPath = @resolveTestRunnerPath(testPaths[0])
devMode = true
isSpec = true
safeMode ?= false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, isSpec, devMode, testRunnerPath, legacyTestRunnerPath, testPaths, logFile, safeMode, env})
runBenchmarks: ({headless, test, resourcePath, executedFrom, pathsToOpen, env}) ->
if resourcePath isnt @resourcePath and not fs.existsSync(resourcePath)
resourcePath = @resourcePath
try
windowInitializationScript = require.resolve(path.resolve(@devResourcePath, 'src', 'initialize-benchmark-window'))
catch error
windowInitializationScript = require.resolve(path.resolve(__dirname, '..', '..', 'src', 'initialize-benchmark-window'))
benchmarkPaths = []
if pathsToOpen?
for pathToOpen in pathsToOpen
benchmarkPaths.push(path.resolve(executedFrom, fs.normalize(pathToOpen)))
if benchmarkPaths.length is 0
process.stderr.write 'Error: Specify at least one benchmark path.\n\n'
process.exit(1)
devMode = true
isSpec = true
safeMode = false
new AtomWindow(this, @fileRecoveryService, {windowInitializationScript, resourcePath, headless, test, isSpec, devMode, benchmarkPaths, safeMode, env})
resolveTestRunnerPath: (testPath) ->
FindParentDir ?= require 'find-parent-dir'
if packageRoot = FindParentDir.sync(testPath, 'package.json')
packageMetadata = require(path.join(packageRoot, 'package.json'))
if packageMetadata.atomTestRunner
Resolve ?= require('resolve')
if testRunnerPath = Resolve.sync(packageMetadata.atomTestRunner, basedir: packageRoot, extensions: Object.keys(require.extensions))
return testRunnerPath
else
process.stderr.write "Error: Could not resolve test runner path '#{packageMetadata.atomTestRunner}'"
process.exit(1)
@resolveLegacyTestRunnerPath()
resolveLegacyTestRunnerPath: ->
try
require.resolve(path.resolve(@devResourcePath, 'spec', 'jasmine-test-runner'))
catch error
require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'jasmine-test-runner'))
locationForPathToOpen: (pathToOpen, executedFrom='', forceAddToWindow) ->
return {pathToOpen} unless pathToOpen
pathToOpen = pathToOpen.replace(/[:\s]+$/, '')
match = pathToOpen.match(LocationSuffixRegExp)
if match?
pathToOpen = pathToOpen.slice(0, -match[0].length)
initialLine = Math.max(0, parseInt(match[1].slice(1)) - 1) if match[1]
initialColumn = Math.max(0, parseInt(match[2].slice(1)) - 1) if match[2]
else
initialLine = initialColumn = null
unless url.parse(pathToOpen).protocol?
pathToOpen = path.resolve(executedFrom, fs.normalize(pathToOpen))
{pathToOpen, initialLine, initialColumn, forceAddToWindow}
# Opens a native dialog to prompt the user for a path.
#
# Once paths are selected, they're opened in a new or existing {AtomWindow}s.
#
# options -
# :type - A String which specifies the type of the dialog, could be 'file',
# 'folder' or 'all'. The 'all' is only available on macOS.
# :devMode - A Boolean which controls whether any newly opened windows
# should be in dev mode or not.
# :safeMode - A Boolean which controls whether any newly opened windows
# should be in safe mode or not.
# :window - An {AtomWindow} to use for opening a selected file path.
# :path - An optional String which controls the default path to which the
# file dialog opens.
promptForPathToOpen: (type, {devMode, safeMode, window}, path=null) ->
@promptForPath type, ((pathsToOpen) =>
@openPaths({pathsToOpen, devMode, safeMode, window})), path
promptForPath: (type, callback, path) ->
properties =
switch type
when 'file' then ['openFile']
when 'folder' then ['openDirectory']
when 'all' then ['openFile', 'openDirectory']
else throw new Error("#{type} is an invalid type for promptForPath")
# Show the open dialog as child window on Windows and Linux, and as
# independent dialog on macOS. This matches most native apps.
parentWindow =
if process.platform is 'darwin'
null
else
BrowserWindow.getFocusedWindow()
openOptions =
properties: properties.concat(['multiSelections', 'createDirectory'])
title: switch type
when 'file' then 'Open File'
when 'folder' then 'Open Folder'
else 'Open'
# File dialog defaults to project directory of currently active editor
if path?
openOptions.defaultPath = path
dialog.showOpenDialog(parentWindow, openOptions, callback)
promptForRestart: ->
chosen = dialog.showMessageBox BrowserWindow.getFocusedWindow(),
type: 'warning'
title: 'Restart required'
message: "You will need to restart Atom for this change to take effect."
buttons: ['Restart Atom', 'Cancel']
if chosen is 0
@restart()
restart: ->
args = []
args.push("--safe") if @safeMode
args.push("--log-file=#{@logFile}") if @logFile?
args.push("--socket-path=#{@socketPath}") if @socketPath?
args.push("--user-data-dir=#{@userDataDir}") if @userDataDir?
if @devMode
args.push('--dev')
args.push("--resource-path=#{@resourcePath}")
app.relaunch({args})
app.quit()
disableZoomOnDisplayChange: ->
outerCallback = =>
for window in @getAllWindows()
window.disableZoom()
# Set the limits every time a display is added or removed, otherwise the
# configuration gets reset to the default, which allows zooming the
# webframe.
screen.on('display-added', outerCallback)
screen.on('display-removed', outerCallback)
new Disposable ->
screen.removeListener('display-added', outerCallback)
screen.removeListener('display-removed', outerCallback)
class WindowStack
constructor: (@windows = []) ->
addWindow: (window) =>
@removeWindow(window)
@windows.unshift(window)
touch: (window) =>
@addWindow(window)
removeWindow: (window) =>
currentIndex = @windows.indexOf(window)
@windows.splice(currentIndex, 1) if currentIndex > -1
getLastFocusedWindow: (predicate) =>
predicate ?= (win) -> true
@windows.find(predicate)
all: =>
@windows

File diff suppressed because it is too large Load Diff

View File

@@ -1,43 +0,0 @@
{protocol} = require 'electron'
fs = require 'fs'
path = require 'path'
# Handles requests with 'atom' protocol.
#
# It's created by {AtomApplication} upon instantiation and is used to create a
# custom resource loader for 'atom://' URLs.
#
# The following directories are searched in order:
# * ~/.atom/assets
# * ~/.atom/dev/packages (unless in safe mode)
# * ~/.atom/packages
# * RESOURCE_PATH/node_modules
#
module.exports =
class AtomProtocolHandler
constructor: (resourcePath, safeMode) ->
@loadPaths = []
unless safeMode
@loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
@loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
@loadPaths.push(path.join(resourcePath, 'node_modules'))
@registerAtomProtocol()
# Creates the 'atom' custom protocol handler.
registerAtomProtocol: ->
protocol.registerFileProtocol 'atom', (request, callback) =>
relativePath = path.normalize(request.url.substr(7))
if relativePath.indexOf('assets/') is 0
assetsPath = path.join(process.env.ATOM_HOME, relativePath)
filePath = assetsPath if fs.statSyncNoException(assetsPath).isFile?()
unless filePath
for loadPath in @loadPaths
filePath = path.join(loadPath, relativePath)
break if fs.statSyncNoException(filePath).isFile?()
callback(filePath)

View File

@@ -0,0 +1,54 @@
const {protocol} = require('electron')
const fs = require('fs')
const path = require('path')
// Handles requests with 'atom' protocol.
//
// It's created by {AtomApplication} upon instantiation and is used to create a
// custom resource loader for 'atom://' URLs.
//
// The following directories are searched in order:
// * ~/.atom/assets
// * ~/.atom/dev/packages (unless in safe mode)
// * ~/.atom/packages
// * RESOURCE_PATH/node_modules
//
module.exports =
class AtomProtocolHandler {
constructor (resourcePath, safeMode) {
this.loadPaths = []
if (!safeMode) {
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'dev', 'packages'))
}
this.loadPaths.push(path.join(process.env.ATOM_HOME, 'packages'))
this.loadPaths.push(path.join(resourcePath, 'node_modules'))
this.registerAtomProtocol()
}
// Creates the 'atom' custom protocol handler.
registerAtomProtocol () {
protocol.registerFileProtocol('atom', (request, callback) => {
const relativePath = path.normalize(request.url.substr(7))
let filePath
if (relativePath.indexOf('assets/') === 0) {
const assetsPath = path.join(process.env.ATOM_HOME, relativePath)
const stat = fs.statSyncNoException(assetsPath)
if (stat && stat.isFile()) filePath = assetsPath
}
if (!filePath) {
for (let loadPath of this.loadPaths) {
filePath = path.join(loadPath, relativePath)
const stat = fs.statSyncNoException(filePath)
if (stat && stat.isFile()) break
}
}
callback(filePath)
})
}
}

View File

@@ -1,329 +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: (options, callback) ->
options = Object.assign({
title: 'Save File',
defaultPath: @representedDirectoryPaths[0]
}, options)
if callback?
# Async
dialog.showSaveDialog(@browserWindow, options, callback)
else
# Sync
dialog.showSaveDialog(@browserWindow, options)
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,432 @@
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)
}
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 (options, callback) {
options = Object.assign({
title: 'Save File',
defaultPath: this.representedDirectoryPaths[0]
}, options)
if (typeof callback === 'function') {
// Async
dialog.showSaveDialog(this.browserWindow, options, callback)
} else {
// Sync
return dialog.showSaveDialog(this.browserWindow, options)
}
}
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()
}
didClosePathWithWaitSession (path) {
this.atomApplication.windowDidClosePathWithWaitSession(this, path)
}
copy () {
return this.browserWindow.copy()
}
disableZoom () {
return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
}
}