273 lines
7.4 KiB
JavaScript
273 lines
7.4 KiB
JavaScript
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2009 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ContextMenuItem = class ContextMenuItem extends WI.Object
|
|
{
|
|
constructor(topLevelMenu, type, label, disabled, checked)
|
|
{
|
|
super();
|
|
|
|
this._type = type;
|
|
this._label = label;
|
|
this._disabled = disabled;
|
|
this._checked = checked;
|
|
this._contextMenu = topLevelMenu || this;
|
|
|
|
if (type === "item" || type === "checkbox")
|
|
this._id = topLevelMenu.nextId();
|
|
}
|
|
|
|
// Public
|
|
|
|
id()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
isEnabled()
|
|
{
|
|
return !this._disabled;
|
|
}
|
|
|
|
setEnabled(enabled)
|
|
{
|
|
this._disabled = !enabled;
|
|
}
|
|
|
|
// Private
|
|
|
|
_buildDescriptor()
|
|
{
|
|
switch (this._type) {
|
|
case "item":
|
|
return {type: "item", id: this._id, label: this._label, enabled: !this._disabled};
|
|
case "separator":
|
|
return {type: "separator"};
|
|
case "checkbox":
|
|
return {type: "checkbox", id: this._id, label: this._label, checked: !!this._checked, enabled: !this._disabled};
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.ContextSubMenuItem = class ContextSubMenuItem extends WI.ContextMenuItem
|
|
{
|
|
constructor(topLevelMenu, label, disabled)
|
|
{
|
|
super(topLevelMenu, "subMenu", label, disabled);
|
|
|
|
this._items = [];
|
|
}
|
|
|
|
// Public
|
|
|
|
appendItem(label, handler, disabled)
|
|
{
|
|
let item = new WI.ContextMenuItem(this._contextMenu, "item", label, disabled);
|
|
this.pushItem(item);
|
|
this._contextMenu._setHandler(item.id(), handler);
|
|
return item;
|
|
}
|
|
|
|
appendSubMenuItem(label, disabled)
|
|
{
|
|
let item = new WI.ContextSubMenuItem(this._contextMenu, label, disabled);
|
|
this.pushItem(item);
|
|
return item;
|
|
}
|
|
|
|
appendCheckboxItem(label, handler, checked, disabled)
|
|
{
|
|
let item = new WI.ContextMenuItem(this._contextMenu, "checkbox", label, disabled, checked);
|
|
this.pushItem(item);
|
|
this._contextMenu._setHandler(item.id(), handler);
|
|
return item;
|
|
}
|
|
|
|
appendHeader(label)
|
|
{
|
|
return this.appendItem(label, () => {
|
|
console.assert(false, "not reached");
|
|
}, true);
|
|
}
|
|
|
|
appendSeparator()
|
|
{
|
|
if (this._items.length)
|
|
this._pendingSeparator = true;
|
|
}
|
|
|
|
pushItem(item)
|
|
{
|
|
if (this._pendingSeparator) {
|
|
this._items.push(new WI.ContextMenuItem(this._contextMenu, "separator"));
|
|
this._pendingSeparator = null;
|
|
}
|
|
this._items.push(item);
|
|
}
|
|
|
|
isEmpty()
|
|
{
|
|
return !this._items.length;
|
|
}
|
|
|
|
// Private
|
|
|
|
_buildDescriptor()
|
|
{
|
|
if (this.isEmpty())
|
|
return null;
|
|
|
|
let subItems = this._items.map((item) => item._buildDescriptor()).filter((item) => !!item);
|
|
return {type: "subMenu", label: this._label, enabled: !this._disabled, subItems};
|
|
}
|
|
};
|
|
|
|
WI.ContextMenu = class ContextMenu extends WI.ContextSubMenuItem
|
|
{
|
|
constructor(event)
|
|
{
|
|
super(null, "");
|
|
|
|
this._event = event;
|
|
this._handlers = {};
|
|
this._id = 0;
|
|
|
|
this._beforeShowCallbacks = [];
|
|
}
|
|
|
|
// Static
|
|
|
|
static createFromEvent(event, onlyExisting = false)
|
|
{
|
|
if (!event[WI.ContextMenu.ProposedMenuSymbol] && !onlyExisting)
|
|
event[WI.ContextMenu.ProposedMenuSymbol] = new WI.ContextMenu(event);
|
|
|
|
return event[WI.ContextMenu.ProposedMenuSymbol] || null;
|
|
}
|
|
|
|
static contextMenuItemSelected(id)
|
|
{
|
|
if (WI.ContextMenu._lastContextMenu)
|
|
WI.ContextMenu._lastContextMenu._itemSelected(id);
|
|
}
|
|
|
|
static contextMenuCleared()
|
|
{
|
|
// FIXME: Unfortunately, contextMenuCleared is invoked between show and item selected
|
|
// so we can't delete last menu object from WI. Fix the contract.
|
|
}
|
|
|
|
// Public
|
|
|
|
nextId()
|
|
{
|
|
return this._id++;
|
|
}
|
|
|
|
show()
|
|
{
|
|
console.assert(this._event instanceof MouseEvent);
|
|
|
|
let menuObject = this._buildDescriptor();
|
|
if (menuObject.length) {
|
|
WI.ContextMenu._lastContextMenu = this;
|
|
|
|
if (this._event.type !== "contextmenu" && typeof InspectorFrontendHost.dispatchEventAsContextMenuEvent === "function") {
|
|
console.assert(event.type !== "mousedown" || this._beforeShowCallbacks.length > 0, "Calling show() in a mousedown handler should have a before show callback to enable quick selection.");
|
|
|
|
this._menuObject = menuObject;
|
|
this._event.target.addEventListener("contextmenu", this, true);
|
|
InspectorFrontendHost.dispatchEventAsContextMenuEvent(this._event);
|
|
} else
|
|
this._showSoftContextMenu(this._event, menuObject);
|
|
}
|
|
|
|
if (this._event)
|
|
this._event.stopImmediatePropagation();
|
|
}
|
|
|
|
addBeforeShowCallback(callback)
|
|
{
|
|
this._beforeShowCallbacks.push(callback);
|
|
}
|
|
|
|
// Protected
|
|
|
|
handleEvent(event)
|
|
{
|
|
console.assert(event.type === "contextmenu");
|
|
|
|
for (let callback of this._beforeShowCallbacks)
|
|
callback(this);
|
|
|
|
this._event.target.removeEventListener("contextmenu", this, true);
|
|
this._showSoftContextMenu(event, this._menuObject);
|
|
this._menuObject = null;
|
|
|
|
event.stopImmediatePropagation();
|
|
}
|
|
|
|
// Private
|
|
|
|
_setHandler(id, handler)
|
|
{
|
|
if (handler)
|
|
this._handlers[id] = handler;
|
|
}
|
|
|
|
_buildDescriptor()
|
|
{
|
|
return this._items.map((item) => item._buildDescriptor()).filter((item) => !!item);
|
|
}
|
|
|
|
_itemSelected(id)
|
|
{
|
|
try {
|
|
if (this._handlers[id])
|
|
this._handlers[id].call(this);
|
|
} catch (e) {
|
|
WI.reportInternalError(e);
|
|
}
|
|
}
|
|
|
|
_showSoftContextMenu(event, menuObject)
|
|
{
|
|
new WI.SoftContextMenu(menuObject).show(event);
|
|
}
|
|
};
|
|
|
|
WI.ContextMenu.ProposedMenuSymbol = Symbol("context-menu-proposed-menu");
|