Mailspring/src/menu-manager.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

189 lines
5.9 KiB
CoffeeScript

path = require 'path'
_ = require 'underscore'
CSON = require 'season'
fs = require 'fs-plus'
{ipcRenderer} = require 'electron'
{Disposable} = require 'event-kit'
Utils = require './flux/models/utils'
MenuHelpers = require './menu-helpers'
# Extended: Provides a registry for menu items that you'd like to appear in the
# application menu.
#
# An instance of this class is always available as the `NylasEnv.menu` global.
#
# ## Menu CSON Format
#
# ```coffee
# [
# {
# 'label': 'View'
# 'submenu': [
# { 'label': 'Toggle Tree View', 'command': 'tree-view:toggle' }
# ]
# }
# {
# 'label': 'Packages'
# 'submenu': [
# 'label': 'Tree View'
# 'submenu': [
# { 'label': 'Focus', 'command': 'tree-view:toggle-focus' }
# { 'label': 'Toggle', 'command': 'tree-view:toggle' }
# { 'label': 'Reveal Active File', 'command': 'tree-view:reveal-active-file' }
# { 'label': 'Toggle Tree Side', 'command': 'tree-view:toggle-side' }
# ]
# ]
# }
# ]
# ```
#
# Use in your package's menu `.cson` file requires that you place your menu
# structure under a `menu` key.
#
# ```coffee
# 'menu': [
# {
# 'label': 'View'
# 'submenu': [
# { 'label': 'Toggle Tree View', 'command': 'tree-view:toggle' }
# ]
# }
# ]
# ```
#
# See {::add} for more info about adding menu's directly.
module.exports =
class MenuManager
constructor: ({@resourcePath}) ->
@pendingUpdateOperation = null
@template = []
NylasEnv.keymaps.onDidLoadBundledKeymaps => @loadPlatformItems()
NylasEnv.keymaps.onDidReloadKeymap => @update()
NylasEnv.packages.onDidActivateInitialPackages => @sortPackagesMenu()
# Public: Adds the given items to the application menu.
#
# ## Examples
#
# ```coffee
# NylasEnv.menu.add [
# {
# label: 'Hello'
# submenu : [{label: 'World!', command: 'hello:world'}]
# }
# ]
# ```
#
# * `items` An {Array} of menu item {Object}s containing the keys:
# * `label` The {String} menu label.
# * `submenu` An optional {Array} of sub menu items.
# * `command` An optional {String} command to trigger when the item is
# clicked.
#
# Returns a {Disposable} on which `.dispose()` can be called to remove the
# added menu items.
add: (items) ->
items = Utils.deepClone(items)
@merge(@template, item) for item in items
@update()
new Disposable => @remove(items)
remove: (items) ->
@unmerge(@template, item) for item in items
@update()
# Should the binding for the given selector be included in the menu
# commands.
#
# * `selector` A {String} selector to check.
#
# Returns a {Boolean}, true to include the selector, false otherwise.
includeSelector: (selector) ->
try
return true if document.body.webkitMatchesSelector(selector)
catch error
# Selector isn't valid
return false
# Simulate an nylas-theme-wrap element attached to a nylas-workspace element attached
# to a body element that has the same classes as the current body element.
unless @testEditor?
testBody = document.createElement('body')
testBody.classList.add(@classesForElement(document.body)...)
testWorkspace = document.createElement('div')
workspaceClasses = @classesForElement(document.body.querySelector('nylas-workspace'))
workspaceClasses = ['workspace'] if workspaceClasses.length is 0
testWorkspace.classList.add(workspaceClasses...)
testBody.appendChild(testWorkspace)
@testEditor = document.createElement('div')
@testEditor.classList.add('editor')
testWorkspace.appendChild(@testEditor)
element = @testEditor
while element
return true if element.webkitMatchesSelector(selector)
element = element.parentElement
false
# Public: Refreshes the currently visible menu.
update: ->
clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
@pendingUpdateOperation = setImmediate =>
keystrokesByCommand = {}
for binding in NylasEnv.keymaps.getKeyBindings() when @includeSelector(binding.selector)
keystrokesByCommand[binding.command] ?= []
keystrokesByCommand[binding.command].unshift binding.keystrokes
@sendToBrowserProcess(@template, keystrokesByCommand)
loadPlatformItems: ->
menusDirPath = path.join(@resourcePath, 'menus')
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
{menu} = CSON.readFileSync(platformMenuPath)
@add(menu)
# Merges an item in a submenu aware way such that new items are always
# appended to the bottom of existing menus where possible.
merge: (menu, item) ->
MenuHelpers.merge(menu, item)
unmerge: (menu, item) ->
MenuHelpers.unmerge(menu, item)
# OSX can't handle displaying accelerators for multiple keystrokes.
# If they are sent across, it will stop processing accelerators for the rest
# of the menu items.
filterMultipleKeystroke: (keystrokesByCommand) ->
filtered = {}
for key, bindings of keystrokesByCommand
for binding in bindings
continue if binding.indexOf(' ') != -1
continue unless /(cmd|ctrl|shift|alt)/.test(binding)
filtered[key] ?= []
filtered[key].push(binding)
filtered
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipcRenderer.send 'update-application-menu', template, keystrokesByCommand
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
element?.classList.toString().split(' ') ? []
sortPackagesMenu: ->
packagesMenu = _.find @template, ({label}) -> MenuHelpers.normalizeLabel(label) is 'Packages'
return unless packagesMenu?.submenu?
packagesMenu.submenu.sort (item1, item2) ->
if item1.label and item2.label
MenuHelpers.normalizeLabel(item1.label).localeCompare(MenuHelpers.normalizeLabel(item2.label))
else
0
@update()