Bug 1321544 - Support icons for context menu items; r?zombie
MozReview-Commit-ID: 2caNYYCQ7C7
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -53,33 +53,17 @@ var gMenuBuilder = {
// the root menu item itself either.
continue;
}
rootElement.setAttribute("ext-type", "top-level-menu");
rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
// Display the extension icon on the root element.
if (root.extension.manifest.icons) {
- let parentWindow = contextData.menu.ownerGlobal;
- let extension = root.extension;
-
- let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
- 16 * parentWindow.devicePixelRatio);
-
- // The extension icons in the manifest are not pre-resolved, since
- // they're sometimes used by the add-on manager when the extension is
- // not enabled, and its URLs are not resolvable.
- let resolvedURL = root.extension.baseURI.resolve(icon);
-
- if (rootElement.localName == "menu") {
- rootElement.setAttribute("class", "menu-iconic");
- } else if (rootElement.localName == "menuitem") {
- rootElement.setAttribute("class", "menuitem-iconic");
- }
- rootElement.setAttribute("image", resolvedURL);
+ this.setMenuItemIcon(rootElement, root.extension, contextData, root.extension.manifest.icons);
}
if (firstItem) {
firstItem = false;
const separator = xulMenu.ownerDocument.createElement("menuseparator");
this.itemsToCleanUp.add(separator);
xulMenu.append(separator);
}
@@ -204,16 +188,20 @@ var gMenuBuilder = {
element.setAttribute("label", label);
}
if (item.id && item.extension && item.extension.id) {
element.setAttribute("id",
`${makeWidgetId(item.extension.id)}_${item.id}`);
}
+ if (item.icons) {
+ this.setMenuItemIcon(element, item.extension, contextData, item.icons);
+ }
+
if (item.type == "checkbox") {
element.setAttribute("type", "checkbox");
if (item.checked) {
element.setAttribute("checked", "true");
}
} else if (item.type == "radio") {
element.setAttribute("type", "radio");
element.setAttribute("name", item.groupName);
@@ -268,16 +256,36 @@ var gMenuBuilder = {
}
item.extension.emit("webext-contextmenu-menuitem-click", info, tab);
});
return element;
},
+ setMenuItemIcon(element, extension, contextData, icons) {
+ let parentWindow = contextData.menu.ownerGlobal;
+
+ let {icon} = IconDetails.getPreferredIcon(icons, extension,
+ 16 * parentWindow.devicePixelRatio);
+
+ // The extension icons in the manifest are not pre-resolved, since
+ // they're sometimes used by the add-on manager when the extension is
+ // not enabled, and its URLs are not resolvable.
+ let resolvedURL = extension.baseURI.resolve(icon);
+
+ if (element.localName == "menu") {
+ element.setAttribute("class", "menu-iconic");
+ } else if (element.localName == "menuitem") {
+ element.setAttribute("class", "menuitem-iconic");
+ }
+
+ element.setAttribute("image", resolvedURL);
+ },
+
handleEvent(event) {
if (this.xulMenu != event.target || event.type != "popuphidden") {
return;
}
delete this.xulMenu;
let target = event.target;
target.removeEventListener("popuphidden", this);
--- a/browser/components/extensions/schemas/context_menus.json
+++ b/browser/components/extensions/schemas/context_menus.json
@@ -137,16 +137,23 @@
"optional": true,
"description": "The type of menu item. Defaults to 'normal' if not specified."
},
"id": {
"type": "string",
"optional": true,
"description": "The unique ID to assign to this item. Mandatory for event pages. Cannot be the same as another ID for this extension."
},
+ "icons": {
+ "type": "object",
+ "optional" : true,
+ "patternProperties" : {
+ "^[1-9]\\d*$": { "type" : "string" }
+ }
+ },
"title": {
"type": "string",
"optional": true,
"description": "The text to be displayed in the item; this is <em>required</em> unless <code>type</code> is 'separator'. When the context is 'selection', you can use <code>%s</code> within the string to show the selected text. For example, if this parameter's value is \"Translate '%s' to Pig Latin\" and the user selects the word \"cool\", the context menu item for the selection is \"Translate 'cool' to Pig Latin\"."
},
"checked": {
"type": "boolean",
"optional": true,
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
@@ -1,15 +1,16 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
-add_task(async function() {
- let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser,
- "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=icons");
+const PAGE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html?test=icons";
+
+add_task(async function test_root_icon() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
let encodedImageData = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
const IMAGE_ARRAYBUFFER = imageBufferFromDataURI(encodedImageData);
let extension = ExtensionTestUtils.loadExtension({
manifest: {
"permissions": ["contextMenus"],
"icons": {
@@ -61,10 +62,108 @@ add_task(async function() {
contextMenu = document.getElementById("contentAreaContextMenu");
topLevelMenuItem = contextMenu.getElementsByAttribute("label", "child")[0];
confirmContextMenuIcon(topLevelMenuItem);
await closeContextMenu();
await extension.unload();
- await BrowserTestUtils.removeTab(tab1);
+ await BrowserTestUtils.removeTab(tab);
});
+
+add_task(async function test_child_icon() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ let blackIconData = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEhkO2P07+gAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAARSURBVCjPY2AYBaNgFAxPAAAD3gABo0ohTgAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_BLACK = imageBufferFromDataURI(blackIconData);
+
+ let redIconData = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEgw1XkM0ygAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAYSURBVCjPY/zPQA5gYhjVNqptVNsg1wYAItkBI/GNR3YAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_RED = imageBufferFromDataURI(redIconData);
+
+ let blueIconData = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0QDFzRzAAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAbSURBVCjPY2SQ+89AOmBiIAuMahvVNqqNftoAlKMBQZXKX9kAAAAASUVORK5CYII=";
+ const IMAGE_ARRAYBUFFER_BLUE = imageBufferFromDataURI(blueIconData);
+
+ let greenIconData = "iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QYGEg0rvVc46AAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAaSURBVCjPY+Q8xkAGYGJgGNU2qm1U2+DWBgBolADz1beTnwAAAABJRU5ErkJggg==";
+ const IMAGE_ARRAYBUFFER_GREEN = imageBufferFromDataURI(greenIconData);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ "permissions": ["contextMenus"],
+ "icons": {
+ "18": "black_icon.png",
+ },
+ },
+
+ files: {
+ "black_icon.png": IMAGE_ARRAYBUFFER_BLACK,
+ "red_icon.png": IMAGE_ARRAYBUFFER_RED,
+ "blue_icon.png": IMAGE_ARRAYBUFFER_BLUE,
+ "green_icon.png": IMAGE_ARRAYBUFFER_GREEN,
+ },
+
+ background: function() {
+ browser.contextMenus.create({
+ title: "child1",
+ id: "contextmenu-child1",
+ icons: {
+ 18: "red_icon.png",
+ },
+ });
+
+ browser.test.onMessage.addListener(msg => {
+ if (msg !== "add-additional-menu-items") {
+ return;
+ }
+
+ browser.contextMenus.create({
+ title: "child2",
+ id: "contextmenu-child2",
+ icons: {
+ 18: "blue_icon.png",
+ },
+ });
+
+ browser.contextMenus.create({
+ title: "child3",
+ id: "contextmenu-child3",
+ icons: {
+ 18: "green_icon.png",
+ },
+ });
+ });
+
+ browser.test.notifyPass("contextmenus-icons");
+ },
+ });
+
+ let confirmContextMenuIcon = (element, imageName) => {
+ let imageURL = element.getAttribute("image");
+ ok(imageURL.endsWith(imageName), "The context menu should display the extension icon next to the child element");
+ };
+
+ await extension.startup();
+ await extension.awaitFinish("contextmenus-icons");
+
+ let contextMenu = await openContextMenu();
+ let contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+ confirmContextMenuIcon(contextMenuChild1, "black_icon.png");
+
+ await closeContextMenu();
+
+ extension.sendMessage("add-additional-menu-items");
+
+ contextMenu = await openExtensionContextMenu();
+
+ contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+ confirmContextMenuIcon(contextMenuChild1, "red_icon.png");
+
+ let contextMenuChild2 = contextMenu.getElementsByAttribute("label", "child2")[0];
+ confirmContextMenuIcon(contextMenuChild2, "blue_icon.png");
+
+ let contextMenuChild3 = contextMenu.getElementsByAttribute("label", "child3")[0];
+ confirmContextMenuIcon(contextMenuChild3, "green_icon.png");
+
+ await closeContextMenu();
+
+ await extension.unload();
+ await BrowserTestUtils.removeTab(tab);
+});