mirror of https://github.com/atom/atom.git
233 lines
8.1 KiB
CoffeeScript
233 lines
8.1 KiB
CoffeeScript
path = require 'path'
|
|
CSON = require 'season'
|
|
fs = require 'fs-plus'
|
|
{calculateSpecificity, validateSelector} = require 'clear-cut'
|
|
{Disposable} = require 'event-kit'
|
|
{remote} = require 'electron'
|
|
MenuHelpers = require './menu-helpers'
|
|
{sortMenuItems} = require './menu-sort-helpers'
|
|
_ = require 'underscore-plus'
|
|
|
|
platformContextMenu = require('../package.json')?._atomMenu?['context-menu']
|
|
|
|
# Extended: Provides a registry for commands that you'd like to appear in the
|
|
# context menu.
|
|
#
|
|
# An instance of this class is always available as the `atom.contextMenu`
|
|
# global.
|
|
#
|
|
# ## Context Menu CSON Format
|
|
#
|
|
# ```coffee
|
|
# 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}]
|
|
# 'atom-text-editor': [{
|
|
# label: 'History',
|
|
# submenu: [
|
|
# {label: 'Undo', command:'core:undo'}
|
|
# {label: 'Redo', command:'core:redo'}
|
|
# ]
|
|
# }]
|
|
# ```
|
|
#
|
|
# In your package's menu `.cson` file you need to specify it under a
|
|
# `context-menu` key:
|
|
#
|
|
# ```coffee
|
|
# 'context-menu':
|
|
# 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}]
|
|
# ...
|
|
# ```
|
|
#
|
|
# The format for use in {::add} is the same minus the `context-menu` key. See
|
|
# {::add} for more information.
|
|
module.exports =
|
|
class ContextMenuManager
|
|
constructor: ({@keymapManager}) ->
|
|
@definitions = {'.overlayer': []} # TODO: Remove once color picker package stops touching private data
|
|
@clear()
|
|
|
|
@keymapManager.onDidLoadBundledKeymaps => @loadPlatformItems()
|
|
|
|
initialize: ({@resourcePath, @devMode}) ->
|
|
|
|
loadPlatformItems: ->
|
|
if platformContextMenu?
|
|
@add(platformContextMenu, @devMode ? false)
|
|
else
|
|
menusDirPath = path.join(@resourcePath, 'menus')
|
|
platformMenuPath = fs.resolve(menusDirPath, process.platform, ['cson', 'json'])
|
|
map = CSON.readFileSync(platformMenuPath)
|
|
@add(map['context-menu'])
|
|
|
|
# Public: Add context menu items scoped by CSS selectors.
|
|
#
|
|
# ## Examples
|
|
#
|
|
# To add a context menu, pass a selector matching the elements to which you
|
|
# want the menu to apply as the top level key, followed by a menu descriptor.
|
|
# The invocation below adds a global 'Help' context menu item and a 'History'
|
|
# submenu on the editor supporting undo/redo. This is just for example
|
|
# purposes and not the way the menu is actually configured in Atom by default.
|
|
#
|
|
# ```coffee
|
|
# atom.contextMenu.add {
|
|
# 'atom-workspace': [{label: 'Help', command: 'application:open-documentation'}]
|
|
# 'atom-text-editor': [{
|
|
# label: 'History',
|
|
# submenu: [
|
|
# {label: 'Undo', command:'core:undo'}
|
|
# {label: 'Redo', command:'core:redo'}
|
|
# ]
|
|
# }]
|
|
# }
|
|
# ```
|
|
#
|
|
# ## Arguments
|
|
#
|
|
# * `itemsBySelector` An {Object} whose keys are CSS selectors and whose
|
|
# values are {Array}s of item {Object}s containing the following keys:
|
|
# * `label` (optional) A {String} containing the menu item's label.
|
|
# * `command` (optional) A {String} containing the command to invoke on the
|
|
# target of the right click that invoked the context menu.
|
|
# * `enabled` (optional) A {Boolean} indicating whether the menu item
|
|
# should be clickable. Disabled menu items typically appear grayed out.
|
|
# Defaults to `true`.
|
|
# * `submenu` (optional) An {Array} of additional items.
|
|
# * `type` (optional) If you want to create a separator, provide an item
|
|
# with `type: 'separator'` and no other keys.
|
|
# * `visible` (optional) A {Boolean} indicating whether the menu item
|
|
# should appear in the menu. Defaults to `true`.
|
|
# * `created` (optional) A {Function} that is called on the item each time a
|
|
# context menu is created via a right click. You can assign properties to
|
|
# `this` to dynamically compute the command, label, etc. This method is
|
|
# actually called on a clone of the original item template to prevent state
|
|
# from leaking across context menu deployments. Called with the following
|
|
# argument:
|
|
# * `event` The click event that deployed the context menu.
|
|
# * `shouldDisplay` (optional) A {Function} that is called to determine
|
|
# whether to display this item on a given context menu deployment. Called
|
|
# with the following argument:
|
|
# * `event` The click event that deployed the context menu.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
|
# added menu items.
|
|
add: (itemsBySelector, throwOnInvalidSelector = true) ->
|
|
addedItemSets = []
|
|
|
|
for selector, items of itemsBySelector
|
|
validateSelector(selector) if throwOnInvalidSelector
|
|
itemSet = new ContextMenuItemSet(selector, items)
|
|
addedItemSets.push(itemSet)
|
|
@itemSets.push(itemSet)
|
|
|
|
new Disposable =>
|
|
for itemSet in addedItemSets
|
|
@itemSets.splice(@itemSets.indexOf(itemSet), 1)
|
|
return
|
|
|
|
templateForElement: (target) ->
|
|
@templateForEvent({target})
|
|
|
|
templateForEvent: (event) ->
|
|
template = []
|
|
currentTarget = event.target
|
|
|
|
while currentTarget?
|
|
currentTargetItems = []
|
|
matchingItemSets =
|
|
@itemSets.filter (itemSet) -> currentTarget.webkitMatchesSelector(itemSet.selector)
|
|
|
|
for itemSet in matchingItemSets
|
|
for item in itemSet.items
|
|
itemForEvent = @cloneItemForEvent(item, event)
|
|
if itemForEvent
|
|
MenuHelpers.merge(currentTargetItems, itemForEvent, itemSet.specificity)
|
|
|
|
for item in currentTargetItems
|
|
MenuHelpers.merge(template, item, false)
|
|
|
|
currentTarget = currentTarget.parentElement
|
|
|
|
@pruneRedundantSeparators(template)
|
|
@addAccelerators(template)
|
|
|
|
return @sortTemplate(template)
|
|
|
|
# Adds an `accelerator` property to items that have key bindings. Electron
|
|
# uses this property to surface the relevant keymaps in the context menu.
|
|
addAccelerators: (template) ->
|
|
for id, item of template
|
|
if item.command
|
|
keymaps = @keymapManager.findKeyBindings({command: item.command, target: document.activeElement})
|
|
keystrokes = keymaps?[0]?.keystrokes
|
|
if keystrokes
|
|
# Electron does not support multi-keystroke accelerators. Therefore,
|
|
# when the command maps to a multi-stroke key binding, show the
|
|
# keystrokes next to the item's label.
|
|
if keystrokes.includes(' ')
|
|
item.label += " [#{_.humanizeKeystroke(keystrokes)}]"
|
|
else
|
|
item.accelerator = MenuHelpers.acceleratorForKeystroke(keystrokes)
|
|
if Array.isArray(item.submenu)
|
|
@addAccelerators(item.submenu)
|
|
|
|
pruneRedundantSeparators: (menu) ->
|
|
keepNextItemIfSeparator = false
|
|
index = 0
|
|
while index < menu.length
|
|
if menu[index].type is 'separator'
|
|
if not keepNextItemIfSeparator or index is menu.length - 1
|
|
menu.splice(index, 1)
|
|
else
|
|
index++
|
|
else
|
|
keepNextItemIfSeparator = true
|
|
index++
|
|
|
|
sortTemplate: (template) ->
|
|
template = sortMenuItems(template)
|
|
for id, item of template
|
|
if Array.isArray(item.submenu)
|
|
item.submenu = @sortTemplate(item.submenu)
|
|
return template
|
|
|
|
# Returns an object compatible with `::add()` or `null`.
|
|
cloneItemForEvent: (item, event) ->
|
|
return null if item.devMode and not @devMode
|
|
item = Object.create(item)
|
|
if typeof item.shouldDisplay is 'function'
|
|
return null unless item.shouldDisplay(event)
|
|
item.created?(event)
|
|
if Array.isArray(item.submenu)
|
|
item.submenu = item.submenu
|
|
.map((submenuItem) => @cloneItemForEvent(submenuItem, event))
|
|
.filter((submenuItem) -> submenuItem isnt null)
|
|
return item
|
|
|
|
showForEvent: (event) ->
|
|
@activeElement = event.target
|
|
menuTemplate = @templateForEvent(event)
|
|
|
|
return unless menuTemplate?.length > 0
|
|
remote.getCurrentWindow().emit('context-menu', menuTemplate)
|
|
return
|
|
|
|
clear: ->
|
|
@activeElement = null
|
|
@itemSets = []
|
|
inspectElement = {
|
|
'atom-workspace': [{
|
|
label: 'Inspect Element'
|
|
command: 'application:inspect'
|
|
devMode: true
|
|
created: (event) ->
|
|
{pageX, pageY} = event
|
|
@commandDetail = {x: pageX, y: pageY}
|
|
}]
|
|
}
|
|
@add(inspectElement, false)
|
|
|
|
class ContextMenuItemSet
|
|
constructor: (@selector, @items) ->
|
|
@specificity = calculateSpecificity(@selector)
|