Bug 1321544 - Support icons for context menu items; r=zombie
authorSwapnesh Kumar Sahoo <swapneshks@gmail.com>
Tue, 11 Jul 2017 22:27:20 +0530
changeset 369047 231631a953dae1595cbf6f271d1d460ebe691c07
parent 369046 3298b85ae1dc040cf7c6ea7c2f2abddd11a9e207
child 369048 610ba2072434c4e5193010b2784ec6c3362f2cdc
push id46530
push userkwierso@gmail.com
push dateSat, 15 Jul 2017 05:20:53 +0000
treeherderautoland@231631a953da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerszombie
bugs1321544
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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);
+});