mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-24 01:06:07 +08:00
528c8850ce
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')`
944 lines
30 KiB
CoffeeScript
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
|