Bug 1321544 - Support icons for context menu items; r?zombie draft
authorSwapnesh Kumar Sahoo <swapneshks@gmail.com>
Tue, 11 Jul 2017 22:27:20 +0530
changeset 607014 4dd5cef5499b9b6089cb3c2224dae6cc7f5a629b
parent 605754 13716663a04056df2777a19082b16bbcf05654b9
child 636914 8a37f6fb290492b279f536b82242f9f28107d359
push id67861
push userswapneshks@gmail.com
push dateTue, 11 Jul 2017 18:31:22 +0000
reviewerszombie
bugs1321544
milestone56.0a1
Bug 1321544 - Support icons for context menu items; r?zombie MozReview-Commit-ID: HLaL8h0WK2c
browser/components/extensions/ext-menus.js
browser/components/extensions/schemas/menus.json
browser/components/extensions/test/browser/browser_ext_contextMenus_icons.js
--- a/browser/components/extensions/ext-menus.js
+++ b/browser/components/extensions/ext-menus.js
@@ -56,33 +56,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);
       }
@@ -207,16 +191,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);
@@ -271,16 +259,36 @@ var gMenuBuilder = {
       }
 
       item.extension.emit("webext-menu-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/menus.json
+++ b/browser/components/extensions/schemas/menus.json
@@ -149,16 +149,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: {
       "name": "contextMenus icons",
       "permissions": ["contextMenus"],
@@ -64,10 +65,112 @@ add_task(async function() {
 
   contextMenu = document.getElementById("contentAreaContextMenu");
   topLevelMenuItem = contextMenu.getElementsByAttribute("label", "child");
 
   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.sendMessage("single-contextmenu-item-added");
+
+      browser.test.onMessage.addListener(msg => {
+        if (msg !== "add-additional-contextmenu-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("extra-contextmenu-items-added");
+      });
+    },
+  });
+
+  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.awaitMessage("single-contextmenu-item-added");
+
+  let contextMenu = await openContextMenu();
+  let contextMenuChild1 = contextMenu.getElementsByAttribute("label", "child1")[0];
+  confirmContextMenuIcon(contextMenuChild1, "black_icon.png");
+
+  await closeContextMenu();
+
+  extension.sendMessage("add-additional-contextmenu-items");
+  await extension.awaitFinish("extra-contextmenu-items-added");
+
+  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);
+});