diff options
Diffstat (limited to 'addon-sdk/source/lib/sdk/context-menu.js')
-rw-r--r-- | addon-sdk/source/lib/sdk/context-menu.js | 1188 |
1 files changed, 0 insertions, 1188 deletions
diff --git a/addon-sdk/source/lib/sdk/context-menu.js b/addon-sdk/source/lib/sdk/context-menu.js deleted file mode 100644 index 004c642d4..000000000 --- a/addon-sdk/source/lib/sdk/context-menu.js +++ /dev/null @@ -1,1188 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -"use strict"; - -module.metadata = { - "stability": "stable", - "engines": { - // TODO Fennec support Bug 788334 - "Firefox": "*", - "SeaMonkey": "*" - } -}; - -const { Class, mix } = require("./core/heritage"); -const { addCollectionProperty } = require("./util/collection"); -const { ns } = require("./core/namespace"); -const { validateOptions, getTypeOf } = require("./deprecated/api-utils"); -const { URL, isValidURI } = require("./url"); -const { WindowTracker, browserWindowIterator } = require("./deprecated/window-utils"); -const { isBrowser, getInnerId } = require("./window/utils"); -const { MatchPattern } = require("./util/match-pattern"); -const { EventTarget } = require("./event/target"); -const { emit } = require('./event/core'); -const { when } = require('./system/unload'); -const { contract: loaderContract } = require('./content/loader'); -const { omit } = require('./util/object'); -const self = require('./self') -const { remoteRequire, processes } = require('./remote/parent'); -remoteRequire('sdk/content/context-menu'); - -// All user items we add have this class. -const ITEM_CLASS = "addon-context-menu-item"; - -// Items in the top-level context menu also have this class. -const TOPLEVEL_ITEM_CLASS = "addon-context-menu-item-toplevel"; - -// Items in the overflow submenu also have this class. -const OVERFLOW_ITEM_CLASS = "addon-context-menu-item-overflow"; - -// The class of the menu separator that separates standard context menu items -// from our user items. -const SEPARATOR_CLASS = "addon-context-menu-separator"; - -// If more than this number of items are added to the context menu, all items -// overflow into a "Jetpack" submenu. -const OVERFLOW_THRESH_DEFAULT = 10; -const OVERFLOW_THRESH_PREF = - "extensions.addon-sdk.context-menu.overflowThreshold"; - -// The label of the overflow sub-xul:menu. -// -// TODO: Localize these. -const OVERFLOW_MENU_LABEL = "Add-ons"; -const OVERFLOW_MENU_ACCESSKEY = "A"; - -// The class of the overflow sub-xul:menu. -const OVERFLOW_MENU_CLASS = "addon-content-menu-overflow-menu"; - -// The class of the overflow submenu's xul:menupopup. -const OVERFLOW_POPUP_CLASS = "addon-content-menu-overflow-popup"; - -// Holds private properties for API objects -var internal = ns(); - -// A little hacky but this is the last process ID that last opened the context -// menu -var lastContextProcessId = null; - -var uuidModule = require('./util/uuid'); -function uuid() { - return uuidModule.uuid().toString(); -} - -function getScheme(spec) { - try { - return URL(spec).scheme; - } - catch(e) { - return null; - } -} - -var Context = Class({ - initialize: function() { - internal(this).id = uuid(); - }, - - // Returns the node that made this context current - adjustPopupNode: function adjustPopupNode(popupNode) { - return popupNode; - }, - - // Returns whether this context is current for the current node - isCurrent: function isCurrent(state) { - return state; - } -}); - -// Matches when the context-clicked node doesn't have any of -// NON_PAGE_CONTEXT_ELTS in its ancestors -var PageContext = Class({ - extends: Context, - - serialize: function() { - return { - id: internal(this).id, - type: "PageContext", - args: [] - } - } -}); -exports.PageContext = PageContext; - -// Matches when there is an active selection in the window -var SelectionContext = Class({ - extends: Context, - - serialize: function() { - return { - id: internal(this).id, - type: "SelectionContext", - args: [] - } - } -}); -exports.SelectionContext = SelectionContext; - -// Matches when the context-clicked node or any of its ancestors matches the -// selector given -var SelectorContext = Class({ - extends: Context, - - initialize: function initialize(selector) { - Context.prototype.initialize.call(this); - let options = validateOptions({ selector: selector }, { - selector: { - is: ["string"], - msg: "selector must be a string." - } - }); - internal(this).selector = options.selector; - }, - - serialize: function() { - return { - id: internal(this).id, - type: "SelectorContext", - args: [internal(this).selector] - } - } -}); -exports.SelectorContext = SelectorContext; - -// Matches when the page url matches any of the patterns given -var URLContext = Class({ - extends: Context, - - initialize: function initialize(patterns) { - Context.prototype.initialize.call(this); - patterns = Array.isArray(patterns) ? patterns : [patterns]; - - try { - internal(this).patterns = patterns.map(p => new MatchPattern(p)); - } - catch (err) { - throw new Error("Patterns must be a string, regexp or an array of " + - "strings or regexps: " + err); - } - }, - - isCurrent: function isCurrent(url) { - return internal(this).patterns.some(p => p.test(url)); - }, - - serialize: function() { - return { - id: internal(this).id, - type: "URLContext", - args: [] - } - } -}); -exports.URLContext = URLContext; - -// Matches when the user-supplied predicate returns true -var PredicateContext = Class({ - extends: Context, - - initialize: function initialize(predicate) { - Context.prototype.initialize.call(this); - let options = validateOptions({ predicate: predicate }, { - predicate: { - is: ["function"], - msg: "predicate must be a function." - } - }); - internal(this).predicate = options.predicate; - }, - - isCurrent: function isCurrent(state) { - return internal(this).predicate(state); - }, - - serialize: function() { - return { - id: internal(this).id, - type: "PredicateContext", - args: [] - } - } -}); -exports.PredicateContext = PredicateContext; - -function removeItemFromArray(array, item) { - return array.filter(i => i !== item); -} - -// Converts anything that isn't false, null or undefined into a string -function stringOrNull(val) { - return val ? String(val) : val; -} - -// Shared option validation rules for Item, Menu, and Separator -var baseItemRules = { - parentMenu: { - is: ["object", "undefined"], - ok: function (v) { - if (!v) - return true; - return (v instanceof ItemContainer) || (v instanceof Menu); - }, - msg: "parentMenu must be a Menu or not specified." - }, - context: { - is: ["undefined", "object", "array"], - ok: function (v) { - if (!v) - return true; - let arr = Array.isArray(v) ? v : [v]; - return arr.every(o => o instanceof Context); - }, - msg: "The 'context' option must be a Context object or an array of " + - "Context objects." - }, - onMessage: { - is: ["function", "undefined"] - }, - contentScript: loaderContract.rules.contentScript, - contentScriptFile: loaderContract.rules.contentScriptFile -}; - -var labelledItemRules = mix(baseItemRules, { - label: { - map: stringOrNull, - is: ["string"], - ok: v => !!v, - msg: "The item must have a non-empty string label." - }, - accesskey: { - map: stringOrNull, - is: ["string", "undefined", "null"], - ok: (v) => { - if (!v) { - return true; - } - return typeof v == "string" && v.length === 1; - }, - msg: "The item must have a single character accesskey, or no accesskey." - }, - image: { - map: stringOrNull, - is: ["string", "undefined", "null"], - ok: function (url) { - if (!url) - return true; - return isValidURI(url); - }, - msg: "Image URL validation failed" - } -}); - -// Additional validation rules for Item -var itemRules = mix(labelledItemRules, { - data: { - map: stringOrNull, - is: ["string", "undefined", "null"] - } -}); - -// Additional validation rules for Menu -var menuRules = mix(labelledItemRules, { - items: { - is: ["array", "undefined"], - ok: function (v) { - if (!v) - return true; - return v.every(function (item) { - return item instanceof BaseItem; - }); - }, - msg: "items must be an array, and each element in the array must be an " + - "Item, Menu, or Separator." - } -}); - -// Returns true if any contexts match. If there are no contexts then a -// PageContext is tested instead -function hasMatchingContext(contexts, addonInfo) { - for (let context of contexts) { - if (!(internal(context).id in addonInfo.contextStates)) { - console.error("Missing state for context " + internal(context).id + " this is an error in the SDK modules."); - return false; - } - if (!context.isCurrent(addonInfo.contextStates[internal(context).id])) - return false; - } - - return true; -} - -// Tests whether an item should be visible or not based on its contexts and -// content scripts -function isItemVisible(item, addonInfo, usePageWorker) { - if (!item.context.length) { - if (!addonInfo.hasWorker) - return usePageWorker ? addonInfo.pageContext : true; - } - - if (!hasMatchingContext(item.context, addonInfo)) - return false; - - let context = addonInfo.workerContext; - if (typeof(context) === "string" && context != "") - item.label = context; - - return !!context; -} - -// Called when an item is clicked to send out click events to the content -// scripts -function itemActivated(item, clickedNode) { - let items = [internal(item).id]; - let data = item.data; - - while (item.parentMenu) { - item = item.parentMenu; - items.push(internal(item).id); - } - - let process = processes.getById(lastContextProcessId); - if (process) - process.port.emit('sdk/contextmenu/activateitems', items, data); -} - -function serializeItem(item) { - return { - id: internal(item).id, - contexts: item.context.map(c => c.serialize()), - contentScript: item.contentScript, - contentScriptFile: item.contentScriptFile, - }; -} - -// All things that appear in the context menu extend this -var BaseItem = Class({ - initialize: function initialize() { - internal(this).id = uuid(); - - internal(this).contexts = []; - if ("context" in internal(this).options && internal(this).options.context) { - let contexts = internal(this).options.context; - if (Array.isArray(contexts)) { - for (let context of contexts) - internal(this).contexts.push(context); - } - else { - internal(this).contexts.push(contexts); - } - } - - let parentMenu = internal(this).options.parentMenu; - if (!parentMenu) - parentMenu = contentContextMenu; - - parentMenu.addItem(this); - - Object.defineProperty(this, "contentScript", { - enumerable: true, - value: internal(this).options.contentScript - }); - - // Resolve URIs here as tests may have overriden self - let files = internal(this).options.contentScriptFile; - if (files) { - if (!Array.isArray(files)) - files = [files]; - files = files.map(self.data.url); - } - internal(this).options.contentScriptFile = files; - Object.defineProperty(this, "contentScriptFile", { - enumerable: true, - value: internal(this).options.contentScriptFile - }); - - // Notify all frames of this new item - sendItems([serializeItem(this)]); - }, - - destroy: function destroy() { - if (internal(this).destroyed) - return; - - // Tell all existing frames that this item has been destroyed - processes.port.emit("sdk/contextmenu/destroyitems", [internal(this).id]); - - if (this.parentMenu) - this.parentMenu.removeItem(this); - - internal(this).destroyed = true; - }, - - get context() { - let contexts = internal(this).contexts.slice(0); - contexts.add = (context) => { - internal(this).contexts.push(context); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }; - contexts.remove = (context) => { - internal(this).contexts = internal(this).contexts.filter(c => { - return c != context; - }); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }; - return contexts; - }, - - set context(val) { - internal(this).contexts = val.slice(0); - // Notify all frames that this item has changed - sendItems([serializeItem(this)]); - }, - - get parentMenu() { - return internal(this).parentMenu; - }, -}); - -function workerMessageReceived(process, id, args) { - if (internal(this).id != id) - return; - - emit(this, ...JSON.parse(args)); -} - -// All things that have a label on the context menu extend this -var LabelledItem = Class({ - extends: BaseItem, - implements: [ EventTarget ], - - initialize: function initialize(options) { - BaseItem.prototype.initialize.call(this); - EventTarget.prototype.initialize.call(this, options); - - internal(this).messageListener = workerMessageReceived.bind(this); - processes.port.on('sdk/worker/event', internal(this).messageListener); - }, - - destroy: function destroy() { - if (internal(this).destroyed) - return; - - processes.port.off('sdk/worker/event', internal(this).messageListener); - - BaseItem.prototype.destroy.call(this); - }, - - get label() { - return internal(this).options.label; - }, - - set label(val) { - internal(this).options.label = val; - - MenuManager.updateItem(this); - }, - - get accesskey() { - return internal(this).options.accesskey; - }, - - set accesskey(val) { - internal(this).options.accesskey = val; - - MenuManager.updateItem(this); - }, - - get image() { - return internal(this).options.image; - }, - - set image(val) { - internal(this).options.image = val; - - MenuManager.updateItem(this); - }, - - get data() { - return internal(this).options.data; - }, - - set data(val) { - internal(this).options.data = val; - } -}); - -var Item = Class({ - extends: LabelledItem, - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, itemRules); - - LabelledItem.prototype.initialize.call(this, options); - }, - - toString: function toString() { - return "[object Item \"" + this.label + "\"]"; - }, - - get data() { - return internal(this).options.data; - }, - - set data(val) { - internal(this).options.data = val; - - MenuManager.updateItem(this); - }, -}); -exports.Item = Item; - -var ItemContainer = Class({ - initialize: function initialize() { - internal(this).children = []; - }, - - destroy: function destroy() { - // Destroys the entire hierarchy - for (let item of internal(this).children) - item.destroy(); - }, - - addItem: function addItem(item) { - let oldParent = item.parentMenu; - - // Don't just call removeItem here as that would remove the corresponding - // UI element which is more costly than just moving it to the right place - if (oldParent) - internal(oldParent).children = removeItemFromArray(internal(oldParent).children, item); - - let after = null; - let children = internal(this).children; - if (children.length > 0) - after = children[children.length - 1]; - - children.push(item); - internal(item).parentMenu = this; - - // If there was an old parent then we just have to move the item, otherwise - // it needs to be created - if (oldParent) - MenuManager.moveItem(item, after); - else - MenuManager.createItem(item, after); - }, - - removeItem: function removeItem(item) { - // If the item isn't a child of this menu then ignore this call - if (item.parentMenu !== this) - return; - - MenuManager.removeItem(item); - - internal(this).children = removeItemFromArray(internal(this).children, item); - internal(item).parentMenu = null; - }, - - get items() { - return internal(this).children.slice(0); - }, - - set items(val) { - // Validate the arguments before making any changes - if (!Array.isArray(val)) - throw new Error(menuOptionRules.items.msg); - - for (let item of val) { - if (!(item instanceof BaseItem)) - throw new Error(menuOptionRules.items.msg); - } - - // Remove the old items and add the new ones - for (let item of internal(this).children) - this.removeItem(item); - - for (let item of val) - this.addItem(item); - }, -}); - -var Menu = Class({ - extends: LabelledItem, - implements: [ItemContainer], - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, menuRules); - - LabelledItem.prototype.initialize.call(this, options); - ItemContainer.prototype.initialize.call(this); - - if (internal(this).options.items) { - for (let item of internal(this).options.items) - this.addItem(item); - } - }, - - destroy: function destroy() { - ItemContainer.prototype.destroy.call(this); - LabelledItem.prototype.destroy.call(this); - }, - - toString: function toString() { - return "[object Menu \"" + this.label + "\"]"; - }, -}); -exports.Menu = Menu; - -var Separator = Class({ - extends: BaseItem, - - initialize: function initialize(options) { - internal(this).options = validateOptions(options, baseItemRules); - - BaseItem.prototype.initialize.call(this); - }, - - toString: function toString() { - return "[object Separator]"; - } -}); -exports.Separator = Separator; - -// Holds items for the content area context menu -var contentContextMenu = ItemContainer(); -exports.contentContextMenu = contentContextMenu; - -function getContainerItems(container) { - let items = []; - for (let item of internal(container).children) { - items.push(serializeItem(item)); - if (item instanceof Menu) - items = items.concat(getContainerItems(item)); - } - return items; -} - -// Notify all frames of these new or changed items -function sendItems(items) { - processes.port.emit("sdk/contextmenu/createitems", items); -} - -// Called when a new process is created and needs to get the current list of items -function remoteItemRequest(process) { - let items = getContainerItems(contentContextMenu); - if (items.length == 0) - return; - - process.port.emit("sdk/contextmenu/createitems", items); -} -processes.forEvery(remoteItemRequest); - -when(function() { - contentContextMenu.destroy(); -}); - -// App specific UI code lives here, it should handle populating the context -// menu and passing clicks etc. through to the items. - -function countVisibleItems(nodes) { - return Array.reduce(nodes, function(sum, node) { - return node.hidden ? sum : sum + 1; - }, 0); -} - -var MenuWrapper = Class({ - initialize: function initialize(winWrapper, items, contextMenu) { - this.winWrapper = winWrapper; - this.window = winWrapper.window; - this.items = items; - this.contextMenu = contextMenu; - this.populated = false; - this.menuMap = new Map(); - - // updateItemVisibilities will run first, updateOverflowState will run after - // all other instances of this module have run updateItemVisibilities - this._updateItemVisibilities = this.updateItemVisibilities.bind(this); - this.contextMenu.addEventListener("popupshowing", this._updateItemVisibilities, true); - this._updateOverflowState = this.updateOverflowState.bind(this); - this.contextMenu.addEventListener("popupshowing", this._updateOverflowState, false); - }, - - destroy: function destroy() { - this.contextMenu.removeEventListener("popupshowing", this._updateOverflowState, false); - this.contextMenu.removeEventListener("popupshowing", this._updateItemVisibilities, true); - - if (!this.populated) - return; - - // If we're getting unloaded at runtime then we must remove all the - // generated XUL nodes - let oldParent = null; - for (let item of internal(this.items).children) { - let xulNode = this.getXULNodeForItem(item); - oldParent = xulNode.parentNode; - oldParent.removeChild(xulNode); - } - - if (oldParent) - this.onXULRemoved(oldParent); - }, - - get separator() { - return this.contextMenu.querySelector("." + SEPARATOR_CLASS); - }, - - get overflowMenu() { - return this.contextMenu.querySelector("." + OVERFLOW_MENU_CLASS); - }, - - get overflowPopup() { - return this.contextMenu.querySelector("." + OVERFLOW_POPUP_CLASS); - }, - - get topLevelItems() { - return this.contextMenu.querySelectorAll("." + TOPLEVEL_ITEM_CLASS); - }, - - get overflowItems() { - return this.contextMenu.querySelectorAll("." + OVERFLOW_ITEM_CLASS); - }, - - getXULNodeForItem: function getXULNodeForItem(item) { - return this.menuMap.get(item); - }, - - // Recurses through the item hierarchy creating XUL nodes for everything - populate: function populate(menu) { - for (let i = 0; i < internal(menu).children.length; i++) { - let item = internal(menu).children[i]; - let after = i === 0 ? null : internal(menu).children[i - 1]; - this.createItem(item, after); - - if (item instanceof Menu) - this.populate(item); - } - }, - - // Recurses through the menu setting the visibility of items. Returns true - // if any of the items in this menu were visible - setVisibility: function setVisibility(menu, addonInfo, usePageWorker) { - let anyVisible = false; - - for (let item of internal(menu).children) { - let visible = isItemVisible(item, addonInfo[internal(item).id], usePageWorker); - - // Recurse through Menus, if none of the sub-items were visible then the - // menu is hidden too. - if (visible && (item instanceof Menu)) - visible = this.setVisibility(item, addonInfo, false); - - let xulNode = this.getXULNodeForItem(item); - xulNode.hidden = !visible; - - anyVisible = anyVisible || visible; - } - - return anyVisible; - }, - - // Works out where to insert a XUL node for an item in a browser window - insertIntoXUL: function insertIntoXUL(item, node, after) { - let menupopup = null; - let before = null; - - let menu = item.parentMenu; - if (menu === this.items) { - // Insert into the overflow popup if it exists, otherwise the normal - // context menu - menupopup = this.overflowPopup; - if (!menupopup) - menupopup = this.contextMenu; - } - else { - let xulNode = this.getXULNodeForItem(menu); - menupopup = xulNode.firstChild; - } - - if (after) { - let afterNode = this.getXULNodeForItem(after); - before = afterNode.nextSibling; - } - else if (menupopup === this.contextMenu) { - let topLevel = this.topLevelItems; - if (topLevel.length > 0) - before = topLevel[topLevel.length - 1].nextSibling; - else - before = this.separator.nextSibling; - } - - menupopup.insertBefore(node, before); - }, - - // Sets the right class for XUL nodes - updateXULClass: function updateXULClass(xulNode) { - if (xulNode.parentNode == this.contextMenu) - xulNode.classList.add(TOPLEVEL_ITEM_CLASS); - else - xulNode.classList.remove(TOPLEVEL_ITEM_CLASS); - - if (xulNode.parentNode == this.overflowPopup) - xulNode.classList.add(OVERFLOW_ITEM_CLASS); - else - xulNode.classList.remove(OVERFLOW_ITEM_CLASS); - }, - - // Creates a XUL node for an item - createItem: function createItem(item, after) { - if (!this.populated) - return; - - // Create the separator if it doesn't already exist - if (!this.separator) { - let separator = this.window.document.createElement("menuseparator"); - separator.setAttribute("class", SEPARATOR_CLASS); - - // Insert before the separator created by the old context-menu if it - // exists to avoid bug 832401 - let oldSeparator = this.window.document.getElementById("jetpack-context-menu-separator"); - if (oldSeparator && oldSeparator.parentNode != this.contextMenu) - oldSeparator = null; - this.contextMenu.insertBefore(separator, oldSeparator); - } - - let type = "menuitem"; - if (item instanceof Menu) - type = "menu"; - else if (item instanceof Separator) - type = "menuseparator"; - - let xulNode = this.window.document.createElement(type); - xulNode.setAttribute("class", ITEM_CLASS); - if (item instanceof LabelledItem) { - xulNode.setAttribute("label", item.label); - if (item.accesskey) - xulNode.setAttribute("accesskey", item.accesskey); - if (item.image) { - xulNode.setAttribute("image", item.image); - if (item instanceof Menu) - xulNode.classList.add("menu-iconic"); - else - xulNode.classList.add("menuitem-iconic"); - } - if (item.data) - xulNode.setAttribute("value", item.data); - - let self = this; - xulNode.addEventListener("command", function(event) { - // Only care about clicks directly on this item - if (event.target !== xulNode) - return; - - itemActivated(item, xulNode); - }, false); - } - - this.insertIntoXUL(item, xulNode, after); - this.updateXULClass(xulNode); - xulNode.data = item.data; - - if (item instanceof Menu) { - let menupopup = this.window.document.createElement("menupopup"); - xulNode.appendChild(menupopup); - } - - this.menuMap.set(item, xulNode); - }, - - // Updates the XUL node for an item in this window - updateItem: function updateItem(item) { - if (!this.populated) - return; - - let xulNode = this.getXULNodeForItem(item); - - // TODO figure out why this requires setAttribute - xulNode.setAttribute("label", item.label); - xulNode.setAttribute("accesskey", item.accesskey || ""); - - if (item.image) { - xulNode.setAttribute("image", item.image); - if (item instanceof Menu) - xulNode.classList.add("menu-iconic"); - else - xulNode.classList.add("menuitem-iconic"); - } - else { - xulNode.removeAttribute("image"); - xulNode.classList.remove("menu-iconic"); - xulNode.classList.remove("menuitem-iconic"); - } - - if (item.data) - xulNode.setAttribute("value", item.data); - else - xulNode.removeAttribute("value"); - }, - - // Moves the XUL node for an item in this window to its new place in the - // hierarchy - moveItem: function moveItem(item, after) { - if (!this.populated) - return; - - let xulNode = this.getXULNodeForItem(item); - let oldParent = xulNode.parentNode; - - this.insertIntoXUL(item, xulNode, after); - this.updateXULClass(xulNode); - this.onXULRemoved(oldParent); - }, - - // Removes the XUL nodes for an item in every window we've ever populated. - removeItem: function removeItem(item) { - if (!this.populated) - return; - - let xulItem = this.getXULNodeForItem(item); - - let oldParent = xulItem.parentNode; - - oldParent.removeChild(xulItem); - this.menuMap.delete(item); - - this.onXULRemoved(oldParent); - }, - - // Called when any XUL nodes have been removed from a menupopup. This handles - // making sure the separator and overflow are correct - onXULRemoved: function onXULRemoved(parent) { - if (parent == this.contextMenu) { - let toplevel = this.topLevelItems; - - // If there are no more items then remove the separator - if (toplevel.length == 0) { - let separator = this.separator; - if (separator) - separator.parentNode.removeChild(separator); - } - } - else if (parent == this.overflowPopup) { - // If there are no more items then remove the overflow menu and separator - if (parent.childNodes.length == 0) { - let separator = this.separator; - separator.parentNode.removeChild(separator); - this.contextMenu.removeChild(parent.parentNode); - } - } - }, - - // Recurses through all the items owned by this module and sets their hidden - // state - updateItemVisibilities: function updateItemVisibilities(event) { - try { - if (event.type != "popupshowing") - return; - if (event.target != this.contextMenu) - return; - - if (internal(this.items).children.length == 0) - return; - - if (!this.populated) { - this.populated = true; - this.populate(this.items); - } - - let mainWindow = event.target.ownerDocument.defaultView; - this.contextMenuContentData = mainWindow.gContextMenuContentData - if (!(self.id in this.contextMenuContentData.addonInfo)) { - console.warn("No context menu state data was provided."); - return; - } - let addonInfo = this.contextMenuContentData.addonInfo[self.id]; - lastContextProcessId = addonInfo.processID; - this.setVisibility(this.items, addonInfo.items, true); - } - catch (e) { - console.exception(e); - } - }, - - // Counts the number of visible items across all modules and makes sure they - // are in the right place between the top level context menu and the overflow - // menu - updateOverflowState: function updateOverflowState(event) { - try { - if (event.type != "popupshowing") - return; - if (event.target != this.contextMenu) - return; - - // The main items will be in either the top level context menu or the - // overflow menu at this point. Count the visible ones and if they are in - // the wrong place move them - let toplevel = this.topLevelItems; - let overflow = this.overflowItems; - let visibleCount = countVisibleItems(toplevel) + - countVisibleItems(overflow); - - if (visibleCount == 0) { - let separator = this.separator; - if (separator) - separator.hidden = true; - let overflowMenu = this.overflowMenu; - if (overflowMenu) - overflowMenu.hidden = true; - } - else if (visibleCount > MenuManager.overflowThreshold) { - this.separator.hidden = false; - let overflowPopup = this.overflowPopup; - if (overflowPopup) - overflowPopup.parentNode.hidden = false; - - if (toplevel.length > 0) { - // The overflow menu shouldn't exist here but let's play it safe - if (!overflowPopup) { - let overflowMenu = this.window.document.createElement("menu"); - overflowMenu.setAttribute("class", OVERFLOW_MENU_CLASS); - overflowMenu.setAttribute("label", OVERFLOW_MENU_LABEL); - overflowMenu.setAttribute("accesskey", OVERFLOW_MENU_ACCESSKEY); - this.contextMenu.insertBefore(overflowMenu, this.separator.nextSibling); - - overflowPopup = this.window.document.createElement("menupopup"); - overflowPopup.setAttribute("class", OVERFLOW_POPUP_CLASS); - overflowMenu.appendChild(overflowPopup); - } - - for (let xulNode of toplevel) { - overflowPopup.appendChild(xulNode); - this.updateXULClass(xulNode); - } - } - } - else { - this.separator.hidden = false; - - if (overflow.length > 0) { - // Move all the overflow nodes out of the overflow menu and position - // them immediately before it - for (let xulNode of overflow) { - this.contextMenu.insertBefore(xulNode, xulNode.parentNode.parentNode); - this.updateXULClass(xulNode); - } - this.contextMenu.removeChild(this.overflowMenu); - } - } - } - catch (e) { - console.exception(e); - } - } -}); - -// This wraps every window that we've seen -var WindowWrapper = Class({ - initialize: function initialize(window) { - this.window = window; - this.menus = [ - new MenuWrapper(this, contentContextMenu, window.document.getElementById("contentAreaContextMenu")), - ]; - }, - - destroy: function destroy() { - for (let menuWrapper of this.menus) - menuWrapper.destroy(); - }, - - getMenuWrapperForItem: function getMenuWrapperForItem(item) { - let root = item.parentMenu; - while (root.parentMenu) - root = root.parentMenu; - - for (let wrapper of this.menus) { - if (wrapper.items === root) - return wrapper; - } - - return null; - } -}); - -var MenuManager = { - windowMap: new Map(), - - get overflowThreshold() { - let prefs = require("./preferences/service"); - return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); - }, - - // When a new window is added start watching it for context menu shows - onTrack: function onTrack(window) { - if (!isBrowser(window)) - return; - - // Generally shouldn't happen, but just in case - if (this.windowMap.has(window)) { - console.warn("Already seen this window"); - return; - } - - let winWrapper = WindowWrapper(window); - this.windowMap.set(window, winWrapper); - }, - - onUntrack: function onUntrack(window) { - if (!isBrowser(window)) - return; - - let winWrapper = this.windowMap.get(window); - // This shouldn't happen but protect against it anyway - if (!winWrapper) - return; - winWrapper.destroy(); - - this.windowMap.delete(window); - }, - - // Creates a XUL node for an item in every window we've already populated - createItem: function createItem(item, after) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.createItem(item, after); - } - }, - - // Updates the XUL node for an item in every window we've already populated - updateItem: function updateItem(item) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.updateItem(item); - } - }, - - // Moves the XUL node for an item in every window we've ever populated to its - // new place in the hierarchy - moveItem: function moveItem(item, after) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.moveItem(item, after); - } - }, - - // Removes the XUL nodes for an item in every window we've ever populated. - removeItem: function removeItem(item) { - for (let [window, winWrapper] of this.windowMap) { - let menuWrapper = winWrapper.getMenuWrapperForItem(item); - if (menuWrapper) - menuWrapper.removeItem(item); - } - } -}; - -WindowTracker(MenuManager); |