-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontext-menu-manager.coffee
232 lines (208 loc) · 8.1 KB
/
context-menu-manager.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
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)