Mailspring/src/nylas-env.coffee
Ben Gotow 528c8850ce bump(electron): 0.34.3 => 0.35.1
Electron 0.35.1 includes the tray fixes we contributed last week but also includes API restructuring and improvements. Most importantly, modules from electron are now imported via `require('electron')`
2015-11-23 22:09:17 -08:00

944 lines
30 KiB
CoffeeScript

crypto = require 'crypto'
os = require 'os'
path = require 'path'
{ipcRenderer, remote, shell} = require 'electron'
_ = require 'underscore'
{deprecate} = require 'grim'
{Emitter} = require 'event-kit'
{Model} = require 'theorist'
fs = require 'fs-plus'
{convertStackTrace, convertLine} = require 'coffeestack'
WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'
Utils = require './flux/models/utils'
{APIError} = require './flux/errors'
ensureInteger = (f, fallback) ->
if f is NaN or f is undefined or f is null
f = fallback
return Math.round(f)
# Essential: NylasEnv global for dealing with packages, themes, menus, and the window.
#
# The singleton of this class is always available as the `NylasEnv` global.
module.exports =
class NylasEnvConstructor extends Model
@version: 1 # Increment this when the serialization format changes
assert: (bool, msg) ->
throw new Error("Assertion error: #{msg}") if not bool
# Load or create the application environment
# Returns an NylasEnv instance, fully initialized
@loadOrCreate: ->
startTime = Date.now()
savedState = @_loadSavedState()
if savedState and savedState?.version is @version
app = new this(savedState)
else
app = new this({@version})
return app
# Loads and returns the serialized state corresponding to this window
# if it exists; otherwise returns undefined.
@_loadSavedState: ->
statePath = @getStatePath()
if fs.existsSync(statePath)
try
stateString = fs.readFileSync(statePath, 'utf8')
catch error
console.warn "Error reading window state: #{statePath}", error.stack, error
else
stateString = @getLoadSettings().windowState
try
JSON.parse(stateString) if stateString?
catch error
console.warn "Error parsing window state: #{statePath} #{error.stack}", error
# Returns the path where the state for the current window will be
# located if it exists.
@getStatePath: ->
{isSpec, mainWindow} = @getLoadSettings()
if isSpec
filename = 'spec-saved-state.json'
else if mainWindow
path.join(@getConfigDirPath(), 'main-window-state.json')
else
null
# Get the directory path to NylasEnv's configuration area.
#
# Returns the absolute path to ~/.nylas
@getConfigDirPath: ->
@configDirPath ?= fs.absolute('~/.nylas')
# Returns the load settings hash associated with the current window.
@getLoadSettings: ->
@loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
cloned = Utils.deepClone(@loadSettings)
# The loadSettings.windowState could be large, request it only when needed.
cloned.__defineGetter__ 'windowState', =>
@getCurrentWindow().loadSettings.windowState
cloned.__defineSetter__ 'windowState', (value) =>
@getCurrentWindow().loadSettings.windowState = value
cloned
@getCurrentWindow: ->
remote.getCurrentWindow()
workspaceViewParentSelector: 'body'
lastUncaughtError: null
###
Section: Properties
###
# Public: A {CommandRegistry} instance
commands: null
# Public: A {Config} instance
config: null
# Public: A {Clipboard} instance
clipboard: null
# Public: A {MenuManager} instance
menu: null
# Public: A {KeymapManager} instance
keymaps: null
# Public: A {PackageManager} instance
packages: null
# Public: A {ThemeManager} instance
themes: null
# Public: A {StyleManager} instance
styles: null
###
Section: Construction and Destruction
###
# Call .loadOrCreate instead
constructor: (@savedState={}) ->
{@version} = @savedState
@emitter = new Emitter
# Sets up the basic services that should be available in all modes
# (both spec and application).
#
# Call after this instance has been assigned to the `NylasEnv` global.
initialize: ->
# Disable deprecations unless in dev mode or spec mode so that regular
# editor performance isn't impacted by generating stack traces for
# deprecated calls.
unless @inDevMode() or @inSpecMode()
require('grim').deprecate = ->
@enhanceEventObject()
@setupErrorLogger()
@unsubscribe()
@loadTime = null
Config = require './config'
KeymapManager = require './keymap-manager'
CommandRegistry = require './command-registry'
PackageManager = require './package-manager'
Clipboard = require './clipboard'
ThemeManager = require './theme-manager'
StyleManager = require './style-manager'
ActionBridge = require './flux/action-bridge'
MenuManager = require './menu-manager'
configDirPath = @getConfigDirPath()
{devMode, safeMode, resourcePath, windowType} = @getLoadSettings()
document.body.classList.add("platform-#{process.platform}")
document.body.classList.add("window-type-#{windowType}")
# Add 'src/global' to module search path.
globalPath = path.join(resourcePath, 'src', 'global')
require('module').globalPaths.push(globalPath)
# Still set NODE_PATH since tasks may need it.
process.env.NODE_PATH = globalPath
# Make react.js faster
process.env.NODE_ENV ?= 'production' unless devMode
# Set NylasEnv's home so packages don't have to guess it
process.env.NYLAS_HOME = configDirPath
# Setup config and load it immediately so it's available to our singletons
@config = new Config({configDirPath, resourcePath})
@keymaps = new KeymapManager({configDirPath, resourcePath})
@keymaps.onDidMatchBinding (event) ->
# If the user fired a command with the application: prefix bound to
# the body, re-fire it up into the browser process. This prevents us
# from needing this crap, which has to be updated every time a new
# application: command is added:
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
ipcRenderer.send('command', event.binding.command)
unless @inSpecMode()
@actionBridge = new ActionBridge(ipcRenderer)
@commands = new CommandRegistry
specMode = @inSpecMode()
@packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode, specMode})
@styles = new StyleManager
document.head.appendChild(new StylesElement)
@themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode})
@clipboard = new Clipboard()
@menu = new MenuManager({resourcePath})
if process.platform is 'win32'
@getCurrentWindow().setMenuBarVisibility(false)
# initialize spell checking
require('web-frame').setSpellCheckProvider("en-US", false, {
spellCheck: (text) ->
!(require('spellchecker').isMisspelled(text))
})
@subscribe @packages.onDidActivateInitialPackages => @watchThemes()
@windowEventHandler = new WindowEventHandler
window.onbeforeunload = => @_unloading()
@_unloadCallbacks = []
# Start our error reporting to the backend and attach error handlers
# to the window and the Bluebird Promise library, converting things
# back through the sourcemap as necessary.
setupErrorLogger: ->
ErrorLogger = require './error-logger'
@errorLogger = new ErrorLogger
inSpecMode: @inSpecMode()
inDevMode: @inDevMode()
resourcePath: @getLoadSettings().resourcePath
sourceMapCache = {}
window.onerror = =>
@lastUncaughtError = Array::slice.call(arguments)
[message, url, line, column, originalError] = @lastUncaughtError
{line, column} = mapSourcePosition({source: url, line, column})
eventObject = {message, url, line, column, originalError}
openDevTools = true
eventObject.preventDefault = -> openDevTools = false
@emitter.emit 'will-throw-error', eventObject
if openDevTools and @inDevMode()
@openDevTools()
@executeJavaScriptInDevTools('DevToolsAPI.showConsole()')
@emitter.emit 'did-throw-error', {message, url, line, column, originalError}
# Since Bluebird is the promise library, we can properly report
# unhandled errors from business logic inside promises.
Promise.longStackTraces() unless @inSpecMode()
Promise.onPossiblyUnhandledRejection (error) =>
error.stack = convertStackTrace(error.stack, sourceMapCache)
# API Errors are logged to Sentry only under certain circumstances,
# and are logged directly from the NylasAPI class.
if error instanceof APIError
return
if @inSpecMode()
console.error(error.stack)
else if @inDevMode()
console.error(error.message, error.stack, error)
@openDevTools()
@executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
else
console.warn(error)
console.warn(error.stack)
@emitError(error)
emitError: (error) ->
console.error(error) unless @inSpecMode()
eventObject = {message: error.message, originalError: error}
@emitter.emit('will-throw-error', eventObject)
@emit('uncaught-error', error.message, null, null, null, error)
@emitter.emit('did-throw-error', eventObject)
###
Section: Event Subscription
###
# Extended: Invoke the given callback whenever {::beep} is called.
#
# * `callback` {Function} to be called whenever {::beep} is called.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidBeep: (callback) ->
@emitter.on 'did-beep', callback
# Extended: Invoke the given callback when there is an unhandled error, but
# before the devtools pop open
#
# * `callback` {Function} to be called whenever there is an unhandled error
# * `event` {Object}
# * `originalError` {Object} the original error object
# * `message` {String} the original error object
# * `url` {String} Url to the file where the error originated.
# * `line` {Number}
# * `column` {Number}
# * `preventDefault` {Function} call this to avoid popping up the dev tools.
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onWillThrowError: (callback) ->
@emitter.on 'will-throw-error', callback
# Extended: Invoke the given callback whenever there is an unhandled error.
#
# * `callback` {Function} to be called whenever there is an unhandled error
# * `event` {Object}
# * `originalError` {Object} the original error object
# * `message` {String} the original error object
# * `url` {String} Url to the file where the error originated.
# * `line` {Number}
# * `column` {Number}
#
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidThrowError: (callback) ->
@emitter.on 'did-throw-error', callback
# Extended: Run the Chromium content-tracing module for five seconds, and save
# the output to a file which is printed to the command-line output of the app.
# You can take the file exported by this function and load it into Chrome's
# content trace visualizer (chrome://tracing). It's like Chromium Developer
# Tools Profiler, but for all processes and threads.
trace: ->
tracing = remote.require('content-tracing')
tracing.startRecording '*', 'record-until-full,enable-sampling,enable-systrace', ->
console.log('Tracing started')
setTimeout ->
tracing.stopRecording '', (path) ->
console.log('Tracing data recorded to ' + path)
, 5000
isMainWindow: ->
!!@getLoadSettings().mainWindow
isWorkWindow: ->
@getWindowType() is 'work'
getWindowType: ->
@getLoadSettings().windowType
# Public: Is the current window in development mode?
inDevMode: ->
@getLoadSettings().devMode
# Public: Is the current window in safe mode?
inSafeMode: ->
@getLoadSettings().safeMode
# Public: Is the current window running specs?
inSpecMode: ->
@getLoadSettings().isSpec
# Public: Get the version of N1.
#
# Returns the version text {String}.
getVersion: ->
@appVersion ?= @getLoadSettings().appVersion
# Public: Determine whether the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
# Public: Get the directory path to N1's configuration area.
#
# Returns the absolute path to `~/.nylas`.
getConfigDirPath: ->
@constructor.getConfigDirPath()
# Public: Get the time taken to completely load the current window.
#
# This time include things like loading and activating packages, creating
# DOM elements for the editor, and reading the config.
#
# Returns the {Number} of milliseconds taken to load the window or null
# if the window hasn't finished loading yet.
getWindowLoadTime: ->
@loadTime
# Public: Get the load settings for the current window.
#
# Returns an {Object} containing all the load setting key/value pairs.
getLoadSettings: ->
@constructor.getLoadSettings()
###
Section: Managing The Nylas Window
###
# Essential: Close the current window.
close: ->
@getCurrentWindow().close()
quit: ->
remote.require('app').quit()
# Essential: Get the size of current window.
#
# Returns an {Object} in the format `{width: 1000, height: 700}`
getSize: ->
[width, height] = @getCurrentWindow().getSize()
{width, height}
# Essential: Set the size of current window.
#
# * `width` The {Number} of pixels.
# * `height` The {Number} of pixels.
setSize: (width, height) ->
width = ensureInteger(width, 100)
height = ensureInteger(height, 100)
@getCurrentWindow().setSize(width, height)
# Essential: Transition and set the size of the current window.
#
# * `width` The {Number} of pixels.
# * `height` The {Number} of pixels.
# * `duration` The {Number} of pixels.
setSizeAnimated: (width, height, duration=400) ->
# On Windows, the native window resizing code isn't fast enough to "animate"
# by resizing over and over again. Just turn off animation for now.
if process.platform is 'win32'
duration = 1
# Avoid divide by zero errors below
duration = Math.max(1, duration)
# Keep track of the number of times this method has been invoked, and ensure
# that we only `tick` for the last invocation. This prevents two resizes from
# running at the same time.
@_setSizeAnimatedCallCount ?= 0
@_setSizeAnimatedCallCount += 1
call = @_setSizeAnimatedCallCount
cubicInOut = (t) -> if t<.5 then 4*t**3 else (t-1)*(2*t-2)**2+1
win = @getCurrentWindow()
width = Math.round(width)
height = Math.round(height)
startBounds = win.getBounds()
startTime = Date.now() - 1 # - 1 so that if duration is 1, t = 1 on the first frame
boundsForI = (i) ->
# It's very important this function never return undefined for any of the
# keys which blows up setBounds.
x: ensureInteger(startBounds.x + (width-startBounds.width) * -0.5 * i, 0)
y: ensureInteger(startBounds.y + (height-startBounds.height) * -0.5 * i, 0)
width: ensureInteger(startBounds.width + (width-startBounds.width) * i, width)
height: ensureInteger(startBounds.height + (height-startBounds.height) * i, height)
tick = =>
return unless call is @_setSizeAnimatedCallCount
t = Math.min(1, (Date.now() - startTime) / (duration))
i = cubicInOut(t)
win.setBounds(boundsForI(i))
unless t is 1
_.defer(tick)
tick()
setMinimumWidth: (minWidth) ->
win = @getCurrentWindow()
minWidth = ensureInteger(minWidth, 0)
minHeight = win.getMinimumSize()[1]
win.setMinimumSize(minWidth, minHeight)
[currWidth, currHeight] = win.getSize()
win.setSize(minWidth, currHeight) if minWidth > currWidth
# Essential: Get the position of current window.
#
# Returns an {Object} in the format `{x: 10, y: 20}`
getPosition: ->
[x, y] = @getCurrentWindow().getPosition()
{x, y}
# Essential: Set the position of current window.
#
# * `x` The {Number} of pixels.
# * `y` The {Number} of pixels.
setPosition: (x, y) ->
x = ensureInteger(x, 0)
y = ensureInteger(y, 0)
ipcRenderer.send('call-window-method', 'setPosition', x, y)
# Extended: Get the current window
getCurrentWindow: ->
@constructor.getCurrentWindow()
# Extended: Move current window to the center of the screen.
center: ->
ipcRenderer.send('call-window-method', 'center')
# Extended: Focus the current window.
focus: ->
ipcRenderer.send('call-window-method', 'focus')
window.focus()
# Extended: Show the current window.
show: ->
ipcRenderer.send('call-window-method', 'show')
isVisible: ->
@getCurrentWindow().isVisible()
# Extended: Hide the current window.
hide: ->
ipcRenderer.send('call-window-method', 'hide')
# Extended: Reload the current window.
reload: ->
ipcRenderer.send('call-window-method', 'restart')
# Updates the window load settings - called when the app is ready to display
# a hot-loaded window. Causes listeners registered with `onWindowPropsReceived`
# to receive new window props.
loadSettingsChanged: (event, loadSettings) =>
@loadSettings = loadSettings
@constructor.loadSettings = loadSettings
{width, height, windowProps} = loadSettings
@emitter.emit('window-props-received', windowProps ? {})
if width and height
@setWindowDimensions({width, height})
# Public: The windowProps passed when creating the window via `newWindow`.
#
getWindowProps: ->
@getLoadSettings().windowProps ? {}
# Public: If your package declares hot-loaded window types, `onWindowPropsReceived`
# fires when your hot-loaded window is about to be shown so you can update
# components to reflect the new window props.
#
# - callback: A function to call when window props are received, just before
# the hot window is shown. The first parameter is the new windowProps.
#
onWindowPropsReceived: (callback) ->
@emitter.on('window-props-received', callback)
# Extended: Is the current window maximized?
isMaximixed: ->
@getCurrentWindow().isMaximized()
maximize: ->
ipcRenderer.send('call-window-method', 'maximize')
minimize: ->
ipcRenderer.send('call-window-method', 'minimize')
# Extended: Is the current window in full screen mode?
isFullScreen: ->
@getCurrentWindow().isFullScreen()
# Extended: Set the full screen state of the current window.
setFullScreen: (fullScreen=false) ->
ipcRenderer.send('call-window-method', 'setFullScreen', fullScreen)
if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen")
# Extended: Toggle the full screen state of the current window.
toggleFullScreen: ->
@setFullScreen(!@isFullScreen())
# Get the dimensions of this window.
#
# Returns an {Object} with the following keys:
# * `x` The window's x-position {Number}.
# * `y` The window's y-position {Number}.
# * `width` The window's width {Number}.
# * `height` The window's height {Number}.
getWindowDimensions: ->
browserWindow = @getCurrentWindow()
{x, y, width, height} = browserWindow.getBounds()
maximized = browserWindow.isMaximized()
{x, y, width, height, maximized}
# Set the dimensions of the window.
#
# The window will be centered if either the x or y coordinate is not set
# in the dimensions parameter. If x or y are omitted the window will be
# centered. If height or width are omitted only the position will be changed.
#
# * `dimensions` An {Object} with the following keys:
# * `x` The new x coordinate.
# * `y` The new y coordinate.
# * `width` The new width.
# * `height` The new height.
setWindowDimensions: ({x, y, width, height}) ->
if x? and y? and width? and height?
@getCurrentWindow().setBounds({x, y, width, height})
else if width? and height?
@setSize(width, height)
else if x? and y?
@setPosition(x, y)
else
@center()
# Returns true if the dimensions are useable, false if they should be ignored.
# Work around for https://github.com/atom/electron/issues/473
isValidDimensions: ({x, y, width, height}={}) ->
width > 0 and height > 0 and x + width > 0 and y + height > 0
getDefaultWindowDimensions: ->
screen = remote.require('screen')
{width, height} = screen.getPrimaryDisplay().workAreaSize
x = 0
y = 0
MAX_WIDTH = 1440
if width > MAX_WIDTH
x = Math.floor((width - MAX_WIDTH) / 2)
width = MAX_WIDTH
MAX_HEIGHT = 900
if height > MAX_HEIGHT
y = Math.floor((height - MAX_HEIGHT) / 2)
height = MAX_HEIGHT
{x, y, width, height}
restoreWindowDimensions: ->
dimensions = @savedState.windowDimensions
unless @isValidDimensions(dimensions)
dimensions = @getDefaultWindowDimensions()
@setWindowDimensions(dimensions)
@maximize() if dimensions.maximized and process.platform isnt 'darwin'
storeWindowDimensions: ->
dimensions = @getWindowDimensions()
@savedState.windowDimensions = dimensions if @isValidDimensions(dimensions)
# Call this method when establishing a real application window.
startRootWindow: ->
@displayWindow()
{safeMode, windowType} = @getLoadSettings()
@registerCommands()
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages(windowType)
@deserializePackageStates()
@deserializeSheetContainer()
@packages.activate()
@keymaps.loadUserKeymap()
@requireUserInitScript() unless safeMode
@menu.update()
@showRootWindow()
ipcRenderer.send('window-command', 'window:loaded')
showRootWindow: ->
cover = document.getElementById("application-loading-cover")
cover.classList.add('visible')
@restoreWindowDimensions()
@getCurrentWindow().setMinimumSize(875, 500)
registerCommands: ->
{resourcePath} = @getLoadSettings()
CommandInstaller = require './command-installer'
CommandInstaller.installN1Command resourcePath, false, (error) ->
console.warn error.message if error?
CommandInstaller.installApmCommand resourcePath, false, (error) ->
console.warn error.message if error?
# Call this method when establishing a secondary application window
# displaying a specific set of packages.
#
startSecondaryWindow: ->
{width,
height,
windowType,
windowPackages} = @getLoadSettings()
cover = document.getElementById("application-loading-cover")
cover.remove()
@loadConfig()
@keymaps.loadBundledKeymaps()
@themes.loadBaseStylesheets()
@packages.loadPackages(windowType)
@packages.loadPackage(pack) for pack in (windowPackages ? [])
@deserializeSheetContainer()
@packages.activate()
@keymaps.loadUserKeymap()
ipcRenderer.on("load-settings-changed", @loadSettingsChanged)
@setWindowDimensions({width, height}) if width and height
@menu.update()
ipcRenderer.send('window-command', 'window:loaded')
# Requests that the backend browser bootup a new window with the given
# options.
# See the valid option types in Application::newWindow in
# src/browser/application.coffee
newWindow: (options={}) -> ipcRenderer.send('new-window', options)
# Registers a hot window for certain packages
# See the valid option types in Application::registerHotWindow in
# src/browser/application.coffee
registerHotWindow: (options={}) -> ipcRenderer.send('register-hot-window', options)
# Unregisters a hot window with the given windowType
unregisterHotWindow: (windowType) -> ipcRenderer.send('unregister-hot-window', windowType)
saveStateAndUnloadWindow: ->
@packages.deactivatePackages()
@savedState.packageStates = @packages.packageStates
@saveSync()
@windowState = null
###
Section: Messaging the User
###
displayWindow: ({maximize} = {}) ->
@show()
@focus()
@maximize() if maximize
# Essential: Visually and audibly trigger a beep.
beep: ->
shell.beep() if @config.get('core.audioBeep')
@emitter.emit 'did-beep'
# Essential: A flexible way to open a dialog akin to an alert dialog.
#
# ## Examples
#
# ```coffee
# NylasEnv.confirm
# message: 'How you feeling?'
# detailedMessage: 'Be honest.'
# buttons:
# Good: -> window.alert('good to hear')
# Bad: -> window.alert('bummer')
# ```
#
# * `options` An {Object} with the following keys:
# * `message` The {String} message to display.
# * `detailedMessage` (optional) The {String} detailed message to display.
# * `buttons` (optional) Either an array of strings or an object where keys are
# button names and the values are callbacks to invoke when clicked.
#
# Returns the chosen button index {Number} if the buttons option was an array.
confirm: ({message, detailedMessage, buttons}={}) ->
buttons ?= {}
if _.isArray(buttons)
buttonLabels = buttons
else
buttonLabels = Object.keys(buttons)
dialog = remote.require('dialog')
chosen = dialog.showMessageBox @getCurrentWindow(),
type: 'info'
message: message
detail: detailedMessage
buttons: buttonLabels
if _.isArray(buttons)
chosen
else
callback = buttons[buttonLabels[chosen]]
callback?()
###
Section: Managing the Dev Tools
###
# Extended: Open the dev tools for the current window.
openDevTools: ->
ipcRenderer.send('call-window-method', 'openDevTools')
# Extended: Toggle the visibility of the dev tools for the current window.
toggleDevTools: ->
ipcRenderer.send('call-window-method', 'toggleDevTools')
# Extended: Execute code in dev tools.
executeJavaScriptInDevTools: (code) ->
ipcRenderer.send('call-webcontents-method', 'executeJavaScriptInDevTools', code)
###
Section: Private
###
deserializeSheetContainer: ->
startTime = Date.now()
# Put state back into sheet-container? Restore app state here
@item = document.createElement("nylas-workspace")
@item.setAttribute("id", "sheet-container")
@item.setAttribute("class", "sheet-container")
@item.setAttribute("tabIndex", "-1")
React = require "react"
SheetContainer = require './sheet-container'
React.render(React.createElement(SheetContainer), @item)
document.querySelector(@workspaceViewParentSelector).appendChild(@item)
deserializePackageStates: ->
@packages.packageStates = @savedState.packageStates ? {}
delete @savedState.packageStates
loadThemes: ->
@themes.load()
loadConfig: ->
@config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
@config.load()
watchThemes: ->
@themes.onDidChangeActiveThemes =>
# Only reload stylesheets from non-theme packages
for pack in @packages.getActivePackages() when pack.getType() isnt 'theme'
pack.reloadStylesheets?()
null
exit: (status) ->
app = remote.require('app')
app.emit('will-exit')
remote.process.exit(status)
showOpenDialog: (options, callback) ->
dialog = remote.require('dialog')
callback(dialog.showOpenDialog(@getCurrentWindow(), options))
showSaveDialog: (options, callback) ->
options.title ?= 'Save File'
dialog = remote.require('dialog')
callback(dialog.showSaveDialog(@getCurrentWindow(), options))
showErrorDialog: (message) ->
dialog = remote.require('dialog')
dialog.showMessageBox null, {
type: 'warning'
buttons: ['Okay'],
message: "Error"
detail: message
}
saveSync: ->
stateString = JSON.stringify(@savedState)
if statePath = @constructor.getStatePath()
fs.writeFileSync(statePath, stateString, 'utf8')
else
@getCurrentWindow().loadSettings.windowState = stateString
crashMainProcess: ->
remote.process.crash()
crashRenderProcess: ->
process.crash()
getUserInitScriptPath: ->
initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
requireUserInitScript: ->
if userInitScriptPath = @getUserInitScriptPath()
try
require(userInitScriptPath) if fs.isFileSync(userInitScriptPath)
catch error
console.log(error)
# Require the module with the given globals.
#
# The globals will be set on the `window` object and removed after the
# require completes.
#
# * `id` The {String} module name or path.
# * `globals` An optinal {Object} to set as globals during require.
requireWithGlobals: (id, globals={}) ->
existingGlobals = {}
for key, value of globals
existingGlobals[key] = window[key]
window[key] = value
require(id)
for key, value of existingGlobals
if value is undefined
delete window[key]
else
window[key] = value
onUpdateAvailable: (callback) ->
@emitter.on 'update-available', callback
updateAvailable: (details) ->
@emitter.emit 'update-available', details
# Lets multiple components register beforeUnload callbacks.
# The callbacks are expected to return either true or false.
#
# Note: If you return false to cancel the window close, you /must/ perform
# work and then call finishUnload. We do not support cancelling quit!
# https://phab.nylas.com/D1932#inline-11722
#
onBeforeUnload: (callback) -> @_unloadCallbacks.push(callback)
_unloading: ->
continueUnload = true
for callback in @_unloadCallbacks
returnValue = callback()
if returnValue is true
continue
else if returnValue is false
continueUnload = false
else
console.warn "You registered an `onBeforeUnload` callback that does not return either exactly `true` or `false`. It returned #{returnValue}", callback
return continueUnload
# Call this method to resume the close / quit process if you returned
# false from a onBeforeUnload handler.
#
finishUnload: ->
_.defer =>
if remote.getGlobal('application').quitting
remote.require('app').quit()
else
@close()
enhanceEventObject: ->
overriddenStop = Event::stopPropagation
Event::stopPropagation = ->
@propagationStopped = true
overriddenStop.apply(@, arguments)
Event::isPropagationStopped = ->
@propagationStopped