Files
atom/src/menu-manager.coffee
Nathan Sobo 61e9befbe7 Favor the most recent matching binding when selecting app menu shortcuts
We sending every binding matching the `body` selector to the browser
process, but it only selects the first. Since we scan the bindings in
chronological order, we need to unshift bindings to the front of the
array so the most recently defined bindings are loaded first.
2014-03-21 10:42:04 -06:00

127 lines
4.3 KiB
CoffeeScript

path = require 'path'
_ = require 'underscore-plus'
ipc = require 'ipc'
CSON = require 'season'
fs = require 'fs-plus'
# Public: 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 `atom.menu` global.
module.exports =
class MenuManager
constructor: ({@resourcePath}) ->
@pendingUpdateOperation = null
@template = []
atom.keymap.on 'bundled-keymaps-loaded', => @loadPlatformItems()
# Public: Adds the given item definition to the existing template.
#
# ## Example
# ```coffee
# atom.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 nothing.
add: (items) ->
@merge(@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 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 .editor element attached to a .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('body')
workspaceClasses = @classesForElement(document.body.querySelector('.workspace')) ? ['.workspace']
testWorkspace.classList.add(workspaceClasses...)
testBody.appendChild(testWorkspace)
@testEditor = document.createElement('div')
@testEditor.classList.add('editor')
testWorkspace.appendChild(@testEditor)
@testEditor.webkitMatchesSelector(selector)
# Public: Refreshes the currently visible menu.
update: ->
clearImmediate(@pendingUpdateOperation) if @pendingUpdateOperation?
@pendingUpdateOperation = setImmediate =>
keystrokesByCommand = {}
for binding in atom.keymap.getKeyBindings() when @includeSelector(binding.selector)
keystrokesByCommand[binding.command] ?= []
keystrokesByCommand[binding.command].unshift binding.keystroke
@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) ->
item = _.deepClone(item)
if item.submenu? and match = _.find(menu, (i) => i.submenu? and @normalizeLabel(i.label) == @normalizeLabel(item.label))
@merge(match.submenu, i) for i in item.submenu
else
menu.push(item) unless _.find(menu, (i) => @normalizeLabel(i.label) == @normalizeLabel(item.label))
# 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
filtered[key] ?= []
filtered[key].push(binding)
filtered
sendToBrowserProcess: (template, keystrokesByCommand) ->
keystrokesByCommand = @filterMultipleKeystroke(keystrokesByCommand)
ipc.sendChannel 'update-application-menu', template, keystrokesByCommand
normalizeLabel: (label) ->
return undefined unless label?
if process.platform is 'win32'
label.replace(/\&/g, '')
else
label
# Get an {Array} of {String} classes for the given element.
classesForElement: (element) ->
element?.classList.toString().split(' ') ? []