blob: 9e877c75b855a0fd4f5108e305a92ae32fe4e6e9 [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 * * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @unrestricted
33 */
34UI.ContextMenuItem = class {
35 /**
36 * @param {?UI.ContextMenu} contextMenu
37 * @param {string} type
38 * @param {string=} label
39 * @param {boolean=} disabled
40 * @param {boolean=} checked
41 */
42 constructor(contextMenu, type, label, disabled, checked) {
43 this._type = type;
44 this._label = label;
45 this._disabled = disabled;
46 this._checked = checked;
47 this._contextMenu = contextMenu;
48 if (type === 'item' || type === 'checkbox')
49 this._id = contextMenu ? contextMenu._nextId() : 0;
50 }
51
52 /**
53 * @return {number}
54 */
55 id() {
56 return this._id;
57 }
58
59 /**
60 * @return {string}
61 */
62 type() {
63 return this._type;
64 }
65
66 /**
67 * @return {boolean}
68 */
69 isEnabled() {
70 return !this._disabled;
71 }
72
73 /**
74 * @param {boolean} enabled
75 */
76 setEnabled(enabled) {
77 this._disabled = !enabled;
78 }
79
80 /**
81 * @return {!InspectorFrontendHostAPI.ContextMenuDescriptor}
82 */
83 _buildDescriptor() {
84 switch (this._type) {
85 case 'item':
86 const result = {type: 'item', id: this._id, label: this._label, enabled: !this._disabled};
87 if (this._customElement)
88 result.element = this._customElement;
89 if (this._shortcut)
90 result.shortcut = this._shortcut;
91 return result;
92 case 'separator':
93 return {type: 'separator'};
94 case 'checkbox':
95 return {type: 'checkbox', id: this._id, label: this._label, checked: !!this._checked, enabled: !this._disabled};
96 }
97 throw new Error('Invalid item type:' + this._type);
98 }
99
100 /**
101 * @param {string} shortcut
102 */
103 setShortcut(shortcut) {
104 this._shortcut = shortcut;
105 }
106};
107
108/**
109 * @unrestricted
110 */
111UI.ContextMenuSection = class {
112 /**
113 * @param {?UI.ContextMenu} contextMenu
114 */
115 constructor(contextMenu) {
116 this._contextMenu = contextMenu;
117 /** @type {!Array<!UI.ContextMenuItem>} */
118 this._items = [];
119 }
120
121 /**
122 * @param {string} label
123 * @param {function(?)} handler
124 * @param {boolean=} disabled
125 * @return {!UI.ContextMenuItem}
126 */
127 appendItem(label, handler, disabled) {
128 const item = new UI.ContextMenuItem(this._contextMenu, 'item', label, disabled);
129 this._items.push(item);
130 this._contextMenu._setHandler(item.id(), handler);
131 return item;
132 }
133
134 /**
135 * @param {!Element} element
136 * @return {!UI.ContextMenuItem}
137 */
138 appendCustomItem(element) {
139 const item = new UI.ContextMenuItem(this._contextMenu, 'item', '<custom>');
140 item._customElement = element;
141 this._items.push(item);
142 return item;
143 }
144
145 /**
146 * @param {string} actionId
147 * @param {string=} label
Pavel Feldmanf5b981a2018-11-30 03:42:08148 * @param {boolean=} optional
Blink Reformat4c46d092018-04-07 15:32:37149 */
Pavel Feldmanf5b981a2018-11-30 03:42:08150 appendAction(actionId, label, optional) {
Blink Reformat4c46d092018-04-07 15:32:37151 const action = UI.actionRegistry.action(actionId);
152 if (!action) {
Pavel Feldmanf5b981a2018-11-30 03:42:08153 if (!optional)
154 console.error(`Action ${actionId} was not defined`);
Blink Reformat4c46d092018-04-07 15:32:37155 return;
156 }
157 if (!label)
158 label = action.title();
159 const result = this.appendItem(label, action.execute.bind(action));
160 const shortcut = UI.shortcutRegistry.shortcutTitleForAction(actionId);
161 if (shortcut)
162 result.setShortcut(shortcut);
163 }
164
165 /**
166 * @param {string} label
167 * @param {boolean=} disabled
168 * @return {!UI.ContextSubMenu}
169 */
170 appendSubMenuItem(label, disabled) {
171 const item = new UI.ContextSubMenu(this._contextMenu, label, disabled);
172 item._init();
173 this._items.push(item);
174 return item;
175 }
176
177 /**
178 * @param {string} label
179 * @param {function()} handler
180 * @param {boolean=} checked
181 * @param {boolean=} disabled
182 * @return {!UI.ContextMenuItem}
183 */
184 appendCheckboxItem(label, handler, checked, disabled) {
185 const item = new UI.ContextMenuItem(this._contextMenu, 'checkbox', label, disabled, checked);
186 this._items.push(item);
187 this._contextMenu._setHandler(item.id(), handler);
188 return item;
189 }
190};
191
192/**
193 * @unrestricted
194 */
195UI.ContextSubMenu = class extends UI.ContextMenuItem {
196 /**
197 * @param {?UI.ContextMenu} contextMenu
198 * @param {string=} label
199 * @param {boolean=} disabled
200 */
201 constructor(contextMenu, label, disabled) {
202 super(contextMenu, 'subMenu', label, disabled);
203 /** @type {!Map<string, !UI.ContextMenuSection>} */
204 this._sections = new Map();
205 /** @type {!Array<!UI.ContextMenuSection>} */
206 this._sectionList = [];
207 }
208
209 _init() {
210 UI.ContextMenu._groupWeights.forEach(name => this.section(name));
211 }
212
213 /**
214 * @param {string=} name
215 * @return {!UI.ContextMenuSection}
216 */
217 section(name) {
218 let section = name ? this._sections.get(name) : null;
219 if (!section) {
220 section = new UI.ContextMenuSection(this._contextMenu);
221 if (name) {
222 this._sections.set(name, section);
223 this._sectionList.push(section);
224 } else {
225 this._sectionList.splice(UI.ContextMenu._groupWeights.indexOf('default'), 0, section);
226 }
227 }
228 return section;
229 }
230
231 /**
232 * @return {!UI.ContextMenuSection}
233 */
234 headerSection() {
235 return this.section('header');
236 }
237
238 /**
239 * @return {!UI.ContextMenuSection}
240 */
241 newSection() {
242 return this.section('new');
243 }
244
245 /**
246 * @return {!UI.ContextMenuSection}
247 */
248 revealSection() {
249 return this.section('reveal');
250 }
251
252 /**
253 * @return {!UI.ContextMenuSection}
254 */
255 clipboardSection() {
256 return this.section('clipboard');
257 }
258
259 /**
260 * @return {!UI.ContextMenuSection}
261 */
262 editSection() {
263 return this.section('edit');
264 }
265
266 /**
267 * @return {!UI.ContextMenuSection}
268 */
269 debugSection() {
270 return this.section('debug');
271 }
272
273 /**
274 * @return {!UI.ContextMenuSection}
275 */
276 viewSection() {
277 return this.section('view');
278 }
279
280 /**
281 * @return {!UI.ContextMenuSection}
282 */
283 defaultSection() {
284 return this.section('default');
285 }
286
287 /**
288 * @return {!UI.ContextMenuSection}
289 */
290 saveSection() {
291 return this.section('save');
292 }
293
294 /**
295 * @return {!UI.ContextMenuSection}
296 */
297 footerSection() {
298 return this.section('footer');
299 }
300
301 /**
302 * @override
303 * @return {!InspectorFrontendHostAPI.ContextMenuDescriptor}
304 */
305 _buildDescriptor() {
306 /** @type {!InspectorFrontendHostAPI.ContextMenuDescriptor} */
307 const result = {type: 'subMenu', label: this._label, enabled: !this._disabled, subItems: []};
308
309 const nonEmptySections = this._sectionList.filter(section => !!section._items.length);
310 for (const section of nonEmptySections) {
311 for (const item of section._items)
312 result.subItems.push(item._buildDescriptor());
313 if (section !== nonEmptySections.peekLast())
314 result.subItems.push({type: 'separator'});
315 }
316 return result;
317 }
318
319 /**
320 * @param {string} location
321 */
322 appendItemsAtLocation(location) {
323 for (const extension of self.runtime.extensions('context-menu-item')) {
324 const itemLocation = extension.descriptor()['location'] || '';
325 if (!itemLocation.startsWith(location + '/'))
326 continue;
327
328 const section = itemLocation.substr(location.length + 1);
329 if (!section || section.includes('/'))
330 continue;
331
332 this.section(section).appendAction(extension.descriptor()['actionId']);
333 }
334 }
335};
336
337UI.ContextMenuItem._uniqueSectionName = 0;
338
339/**
340 * @unrestricted
341 */
342UI.ContextMenu = class extends UI.ContextSubMenu {
343 /**
344 * @param {!Event} event
345 * @param {boolean=} useSoftMenu
346 * @param {number=} x
347 * @param {number=} y
348 */
349 constructor(event, useSoftMenu, x, y) {
350 super(null);
351 this._contextMenu = this;
352 super._init();
353 this._defaultSection = this.defaultSection();
354 /** @type {!Array.<!Promise.<!Array.<!UI.ContextMenu.Provider>>>} */
355 this._pendingPromises = [];
356 /** @type {!Array<!Object>} */
357 this._pendingTargets = [];
358 this._event = event;
359 this._useSoftMenu = !!useSoftMenu;
360 this._x = x === undefined ? event.x : x;
361 this._y = y === undefined ? event.y : y;
362 this._handlers = {};
363 this._id = 0;
364
365 const target = event.deepElementFromPoint();
366 if (target)
367 this.appendApplicableItems(/** @type {!Object} */ (target));
368 }
369
370 static initialize() {
Tim van der Lippe7b190162019-09-27 15:10:44371 InspectorFrontendHost.events.addEventListener(Host.InspectorFrontendHostAPI.Events.SetUseSoftMenu, setUseSoftMenu);
Blink Reformat4c46d092018-04-07 15:32:37372 /**
373 * @param {!Common.Event} event
374 */
375 function setUseSoftMenu(event) {
376 UI.ContextMenu._useSoftMenu = /** @type {boolean} */ (event.data);
377 }
378 }
379
380 /**
381 * @param {!Document} doc
382 */
383 static installHandler(doc) {
384 doc.body.addEventListener('contextmenu', handler, false);
385
386 /**
387 * @param {!Event} event
388 */
389 function handler(event) {
390 const contextMenu = new UI.ContextMenu(event);
391 contextMenu.show();
392 }
393 }
394
395 /**
396 * @return {number}
397 */
398 _nextId() {
399 return this._id++;
400 }
401
402 show() {
403 Promise.all(this._pendingPromises).then(populate.bind(this)).then(this._innerShow.bind(this));
404 UI.ContextMenu._pendingMenu = this;
405
406 /**
407 * @param {!Array.<!Array.<!UI.ContextMenu.Provider>>} appendCallResults
408 * @this {UI.ContextMenu}
409 */
410 function populate(appendCallResults) {
411 if (UI.ContextMenu._pendingMenu !== this)
412 return;
413 delete UI.ContextMenu._pendingMenu;
414
415 for (let i = 0; i < appendCallResults.length; ++i) {
416 const providers = appendCallResults[i];
417 const target = this._pendingTargets[i];
418
419 for (let j = 0; j < providers.length; ++j) {
420 const provider = /** @type {!UI.ContextMenu.Provider} */ (providers[j]);
421 provider.appendApplicableItems(this._event, this, target);
422 }
423 }
424
425 this._pendingPromises = [];
426 this._pendingTargets = [];
427 }
428
429 this._event.consume(true);
430 }
431
432 discard() {
433 if (this._softMenu)
434 this._softMenu.discard();
435 }
436
437 _innerShow() {
438 const menuObject = this._buildMenuDescriptors();
Erik Luo3971e8a2018-08-09 07:27:08439 if (this._useSoftMenu || UI.ContextMenu._useSoftMenu || InspectorFrontendHost.isHostedMode()) {
Blink Reformat4c46d092018-04-07 15:32:37440 this._softMenu = new UI.SoftContextMenu(menuObject, this._itemSelected.bind(this));
441 this._softMenu.show(this._event.target.ownerDocument, new AnchorBox(this._x, this._y, 0, 0));
442 } else {
443 InspectorFrontendHost.showContextMenuAtPoint(this._x, this._y, menuObject, this._event.target.ownerDocument);
444
445 /**
446 * @this {UI.ContextMenu}
447 */
448 function listenToEvents() {
449 InspectorFrontendHost.events.addEventListener(
Tim van der Lippe7b190162019-09-27 15:10:44450 Host.InspectorFrontendHostAPI.Events.ContextMenuCleared, this._menuCleared, this);
Blink Reformat4c46d092018-04-07 15:32:37451 InspectorFrontendHost.events.addEventListener(
Tim van der Lippe7b190162019-09-27 15:10:44452 Host.InspectorFrontendHostAPI.Events.ContextMenuItemSelected, this._onItemSelected, this);
Blink Reformat4c46d092018-04-07 15:32:37453 }
454
455 // showContextMenuAtPoint call above synchronously issues a clear event for previous context menu (if any),
456 // so we skip it before subscribing to the clear event.
457 setImmediate(listenToEvents.bind(this));
458 }
459 }
460
461 /**
462 * @param {number} id
463 * @param {function(?)} handler
464 */
465 _setHandler(id, handler) {
466 if (handler)
467 this._handlers[id] = handler;
468 }
469
470 /**
471 * @return {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>}
472 */
473 _buildMenuDescriptors() {
474 return /** @type {!Array.<!InspectorFrontendHostAPI.ContextMenuDescriptor>} */ (super._buildDescriptor().subItems);
475 }
476
477 /**
478 * @param {!Common.Event} event
479 */
480 _onItemSelected(event) {
481 this._itemSelected(/** @type {string} */ (event.data));
482 }
483
484 /**
485 * @param {string} id
486 */
487 _itemSelected(id) {
488 if (this._handlers[id])
489 this._handlers[id].call(this);
490 this._menuCleared();
491 }
492
493 _menuCleared() {
494 InspectorFrontendHost.events.removeEventListener(
Tim van der Lippe7b190162019-09-27 15:10:44495 Host.InspectorFrontendHostAPI.Events.ContextMenuCleared, this._menuCleared, this);
Blink Reformat4c46d092018-04-07 15:32:37496 InspectorFrontendHost.events.removeEventListener(
Tim van der Lippe7b190162019-09-27 15:10:44497 Host.InspectorFrontendHostAPI.Events.ContextMenuItemSelected, this._onItemSelected, this);
Blink Reformat4c46d092018-04-07 15:32:37498 }
499
500 /**
501 * @param {!Object} target
Junyi Xiao6e3798d2019-09-23 19:12:27502 * @return {boolean}
503 */
504 containsTarget(target) {
505 return this._pendingTargets.indexOf(target) >= 0;
506 }
507
508 /**
509 * @param {!Object} target
Blink Reformat4c46d092018-04-07 15:32:37510 */
511 appendApplicableItems(target) {
512 this._pendingPromises.push(self.runtime.allInstances(UI.ContextMenu.Provider, target));
513 this._pendingTargets.push(target);
514 }
515};
516
517UI.ContextMenu._groupWeights =
518 ['header', 'new', 'reveal', 'edit', 'clipboard', 'debug', 'view', 'default', 'save', 'footer'];
519
520/**
521 * @interface
522 */
523UI.ContextMenu.Provider = function() {};
524
525UI.ContextMenu.Provider.prototype = {
526 /**
527 * @param {!Event} event
528 * @param {!UI.ContextMenu} contextMenu
529 * @param {!Object} target
530 */
531 appendApplicableItems(event, contextMenu, target) {}
532};