Bug 1272219: Use 16px icons for BrowserAction buttons, support 18px as a legacy fallback. r=bwinton ui-r=bwinton a=sylvestre
authorKris Maglione <maglione.k@gmail.com>
Sun, 05 Jun 2016 21:05:36 -0700
changeset 339762 c8e295f7c12ff745034ed9934a488f90bdb72db0
parent 339761 3053d997f2e9f0fc7dd3960927c92496df1d9ace
child 339763 ddf1c2c6a9e31c96b3a83862bb4972d5d009e823
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbwinton, bwinton, sylvestre
bugs1272219
milestone49.0a2
Bug 1272219: Use 16px icons for BrowserAction buttons, support 18px as a legacy fallback. r=bwinton ui-r=bwinton a=sylvestre MozReview-Commit-ID: 2c0UWR6hITQ
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-utils.js
browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/windows/browser.css
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -141,19 +141,35 @@ BrowserAction.prototype = {
     if (badgeNode) {
       let color = tabData.badgeBackgroundColor;
       if (Array.isArray(color)) {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
       badgeNode.style.backgroundColor = color || "";
     }
 
-    let iconURL = IconDetails.getURL(
-      tabData.icon, node.ownerDocument.defaultView, this.extension);
-    node.setAttribute("image", iconURL);
+    const LEGACY_CLASS = "toolbarbutton-legacy-addon";
+    node.classList.remove(LEGACY_CLASS);
+
+
+    let win = node.ownerDocument.defaultView;
+    let {icon, size} = IconDetails.getURL(tabData.icon, win, this.extension);
+
+    // If the best available icon size is not divisible by 16, check if we have
+    // an 18px icon to fall back to, and trim off the padding instead.
+    if (size % 16 && !icon.endsWith(".svg")) {
+      let result = IconDetails.getURL(tabData.icon, win, this.extension, 18);
+
+      if (result.size % 18 == 0) {
+        icon = result.icon;
+        node.classList.add(LEGACY_CLASS);
+      }
+    }
+
+    node.setAttribute("image", icon);
   },
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -86,17 +86,17 @@ PageAction.prototype = {
 
     if (tabData.show) {
       // Update the title and icon only if the button is visible.
 
       let title = tabData.title || this.extension.name;
       button.setAttribute("tooltiptext", title);
       button.setAttribute("aria-label", title);
 
-      let icon = IconDetails.getURL(tabData.icon, window, this.extension);
+      let {icon} = IconDetails.getURL(tabData.icon, window, this.extension);
       button.setAttribute("src", icon);
     }
 
     button.hidden = !tabData.show;
   },
 
   // Create an |image| node and add it to the |urlbar-icons|
   // container in the given window.
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -98,20 +98,39 @@ global.IconDetails = {
       extension.manifestError(`Invalid icon data: ${e}`);
     }
 
     return result;
   },
 
   // Returns the appropriate icon URL for the given icons object and the
   // screen resolution of the given window.
-  getURL(icons, window, extension, size = 18) {
+  getURL(icons, window, extension, size = 16) {
     const DEFAULT = "chrome://browser/content/extension.svg";
 
-    return AddonManager.getPreferredIconURL({icons: icons}, size, window) || DEFAULT;
+    size *= window.devicePixelRatio;
+
+    let bestSize = null;
+    if (icons[size]) {
+      bestSize = size;
+    } else if (icons[2 * size]) {
+      bestSize = 2 * size;
+    } else {
+      let sizes = Object.keys(icons)
+                        .map(key => parseInt(key, 10))
+                        .sort((a, b) => a - b);
+
+      bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
+    }
+
+    if (bestSize) {
+      return {size: bestSize, icon: icons[bestSize]};
+    }
+
+    return {size, icon: DEFAULT};
   },
 
   convertImageDataToPNG(imageData, context) {
     let document = context.contentWindow.document;
     let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     canvas.width = imageData.width;
     canvas.height = imageData.height;
     canvas.getContext("2d").putImageData(imageData, 0, 0);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -99,37 +99,66 @@ add_task(function* testDetailsObjects() 
       {details: {
           "path": {"38": "a.png"},
           "imageData": imageData.red.imageData},
         resolutions: {
           "1": imageData.red.url,
           "2": browser.runtime.getURL("data/a.png")}},
 
       // Various resolutions
-      {details: {"path": {"18": "a.png", "32": "a-x2.png"}},
+      {details: {"path": {"18": "a.png", "36": "a-x2.png"}},
+        legacy: true,
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a-x2.png")}},
+      {details: {"path": {"16": "a.png", "30": "a-x2.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a-x2.png")}},
       {details: {"path": {"16": "16.png", "100": "100.png"}},
         resolutions: {
-          "1": browser.runtime.getURL("data/100.png"),
+          "1": browser.runtime.getURL("data/16.png"),
           "2": browser.runtime.getURL("data/100.png")}},
       {details: {"path": {"2": "2.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/2.png"),
           "2": browser.runtime.getURL("data/2.png")}},
       {details: {"path": {
+        "16": "16.svg",
+        "18": "18.svg"}},
+        resolutions: {
+          "1": browser.runtime.getURL("data/16.svg"),
+          "2": browser.runtime.getURL("data/18.svg")}},
+      {details: {"path": {
         "6": "6.png",
         "18": "18.png",
+        "36": "36.png",
+        "48": "48.png",
+        "128": "128.png"}},
+        legacy: true,
+        resolutions: {
+          "1": browser.runtime.getURL("data/18.png"),
+          "2": browser.runtime.getURL("data/36.png")}},
+      {details: {"path": {
+        "16": "16.png",
+        "18": "18.png",
         "32": "32.png",
         "48": "48.png",
         "128": "128.png"}},
         resolutions: {
-          "1": browser.runtime.getURL("data/18.png"),
-          "2": browser.runtime.getURL("data/48.png")}},
+          "1": browser.runtime.getURL("data/16.png"),
+          "2": browser.runtime.getURL("data/32.png")}},
+      {details: {"path": {
+        "18": "18.png",
+        "32": "32.png",
+        "48": "48.png",
+        "128": "128.png"}},
+        resolutions: {
+          "1": browser.runtime.getURL("data/32.png"),
+          "2": browser.runtime.getURL("data/32.png")}},
     ];
 
     // Allow serializing ImageData objects for logging.
     ImageData.prototype.toJSON = () => "<ImageData>";
 
     let tabId;
 
     browser.test.onMessage.addListener((msg, test) => {
@@ -141,17 +170,17 @@ add_task(function* testDetailsObjects() 
       let expectedURL = details.resolutions[test.resolution];
 
       let detailString = JSON.stringify(details);
       browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`);
 
       browser.browserAction.setIcon(Object.assign({tabId}, details.details));
       browser.pageAction.setIcon(Object.assign({tabId}, details.details));
 
-      browser.test.sendMessage("imageURL", expectedURL);
+      browser.test.sendMessage("imageURL", [expectedURL, !!details.legacy]);
     });
 
     // Generate a list of tests and resolutions to send back to the test
     // context.
     //
     // This process is a bit convoluted, because the outer test context needs
     // to handle checking the button nodes and changing the screen resolution,
     // but it can't pass us icon definitions with ImageData objects. This
@@ -204,21 +233,24 @@ add_task(function* testDetailsObjects() 
   let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
   for (let test of tests) {
     SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
     is(window.devicePixelRatio, test.resolution, "window has the required resolution");
 
     extension.sendMessage("setIcon", test);
 
-    let imageURL = yield extension.awaitMessage("imageURL");
+    let [imageURL, legacy] = yield extension.awaitMessage("imageURL");
 
     let browserActionButton = document.getElementById(browserActionId);
     is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
 
+    let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
+    is(isLegacy, legacy, "Legacy class should be present?");
+
     let pageActionImage = document.getElementById(pageActionId);
     is(pageActionImage.src, imageURL, "page action has the correct image");
   }
 
   yield extension.unload();
 });
 
 // Test that an error is thrown when providing invalid icon sizes
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -570,18 +570,18 @@ menuitem:not([type]):not(.menuitem-toolt
 
 /* Primary toolbar buttons */
 
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > .toolbarbutton-icon,
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
   max-width: 16px;
 }
 
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > .toolbarbutton-icon,
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon,
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > .toolbarbutton-icon,
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon,
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   max-width: 18px;
 }
 
 .findbar-button,
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 {
   -moz-appearance: none;
@@ -600,19 +600,19 @@ menuitem:not([type]):not(.menuitem-toolt
   margin-inline-end: 0;
   padding: 2px 6px;
   border: 1px solid transparent;
   border-radius: 2px;
   transition-property: background-color, border-color;
   transition-duration: 150ms;
 }
 
-:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-icon,
-:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-badge-stack,
-:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
+:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-icon,
+:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-badge-stack,
+:-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
   padding: 3px 7px;
 }
 
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-icon,
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon,
 :-moz-any(#TabsToolbar, #nav-bar) #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   /* horizontal padding + border + actual icon width */
   max-width: 32px !important /* bug 561154 */;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -596,18 +596,18 @@ toolbarpaletteitem[place="palette"] > #p
 /* ----- PRIMARY TOOLBAR BUTTONS ----- */
 
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > .toolbarbutton-icon,
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
   max-width: 16px;
   margin: 1px;
 }
 
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > .toolbarbutton-icon,
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > .toolbarbutton-icon,
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
   max-width: 18px;
   margin: 0;
 }
 
 toolbar .toolbarbutton-1:not([type="menu-button"]),
 .toolbarbutton-1 > .toolbarbutton-menubutton-button,
 .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-box-orient: vertical;
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -676,18 +676,18 @@ toolbar[brighttext] .toolbarbutton-1 > .
   margin-inline-end: 0;
 }
 
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > .toolbarbutton-icon,
 :-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1 > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon {
   max-width: 16px;
 }
 
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > .toolbarbutton-icon,
-:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon,
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > .toolbarbutton-icon,
+:-moz-any(toolbar, .widget-overflow-list) .toolbarbutton-1:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon) > :-moz-any(.toolbarbutton-menubutton-button, .toolbarbutton-badge-stack) > .toolbarbutton-icon,
 #bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
   max-width: 18px;
 }
 
 .findbar-button,
 #nav-bar .toolbarbutton-1,
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   -moz-appearance: none;
@@ -788,19 +788,19 @@ toolbar[brighttext] .toolbarbutton-1 > .
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
 }
 
 #nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
   border-inline-end-style: none;
 }
 
-#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-icon,
-#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-badge-stack,
-#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@)) > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
+#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-icon,
+#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-badge-stack,
+#nav-bar .toolbarbutton-1:not(:-moz-any(@primaryToolbarButtons@, .toolbarbutton-legacy-addon)) > .toolbarbutton-menubutton-button > .toolbarbutton-icon {
   padding: calc(var(--toolbarbutton-vertical-inner-padding) + 1px) 7px;
 }
 
 #nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-icon,
 #nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-badge-stack,
 #nav-bar .toolbarbutton-1[type=menu]:not(#PanelUI-menu-button):not(#back-button):not(#forward-button) > .toolbarbutton-icon,
 #nav-bar .toolbarbutton-1[type=menu]:not(#PanelUI-menu-button) > .toolbarbutton-badge-stack,
 #nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ {