Bug 1457658 - tabs.setStatusIcon WebExtension API draft
authorPeter Simonyi <pts@petersimonyi.ca>
Fri, 27 Apr 2018 21:41:30 -0400
changeset 789387 67a5ad737270207cc582698daf0248b3abd376fa
parent 788735 63a0e2f626febb98d87d2543955ab99a653654ff
push id108260
push userbmo:mozPeter@gmail.com
push dateSat, 28 Apr 2018 17:59:17 +0000
bugs1457658
milestone61.0a1
Bug 1457658 - tabs.setStatusIcon WebExtension API MozReview-Commit-ID: ELgyaB0fRPI
browser/base/content/tabbrowser.css
browser/base/content/tabbrowser.xml
browser/components/extensions/parent/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/themes/shared/tabs.inc.css
--- a/browser/base/content/tabbrowser.css
+++ b/browser/base/content/tabbrowser.css
@@ -31,16 +31,17 @@
 .tab-icon-overlay[crashed] {
   display: -moz-box;
 }
 
 .tab-label {
   white-space: nowrap;
 }
 
+.tab-flex-container,
 .tab-label-container {
   overflow: hidden;
 }
 
 .tab-label-container[pinned] {
   width: 0;
 }
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1519,24 +1519,32 @@
           <xul:image xbl:inherits="sharing,selected=visuallyselected,pinned"
                      anonid="sharing-icon"
                      class="tab-sharing-icon-overlay"
                      role="presentation"/>
           <xul:image xbl:inherits="crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked"
                      anonid="overlay-icon"
                      class="tab-icon-overlay"
                      role="presentation"/>
-          <xul:hbox class="tab-label-container"
+          <xul:hbox class="tab-flex-container"
                     xbl:inherits="pinned,selected=visuallyselected,labeldirection"
-                    onoverflow="this.setAttribute('textoverflow', 'true');"
-                    onunderflow="this.removeAttribute('textoverflow');"
                     flex="1">
-            <xul:label class="tab-text tab-label"
-                       xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention"
-                       role="presentation"/>
+
+            <xul:hbox class="tab-label-container"
+                      xbl:inherits="pinned,selected=visuallyselected,labeldirection"
+                      onoverflow="this.setAttribute('textoverflow', 'true');"
+                      onunderflow="this.removeAttribute('textoverflow');"
+                      flex="1">
+              <xul:label class="tab-text tab-label"
+                         xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention"
+                         role="presentation"/>
+            </xul:hbox>
+            <xul:hbox class="tab-extension-status-container">
+              <children/>
+            </xul:hbox>
           </xul:hbox>
           <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected,activemedia-blocked"
                      anonid="soundplaying-icon"
                      class="tab-icon-sound"
                      role="presentation"/>
           <xul:image anonid="close-button"
                      xbl:inherits="fadein,pinned,selected=visuallyselected"
                      class="tab-close-button close-icon"
--- a/browser/components/extensions/parent/ext-tabs.js
+++ b/browser/components/extensions/parent/ext-tabs.js
@@ -11,16 +11,20 @@ ChromeUtils.defineModuleGetter(this, "Se
 ChromeUtils.defineModuleGetter(this, "SessionStore",
                                "resource:///modules/sessionstore/SessionStore.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "strBundle", function() {
   return Services.strings.createBundle("chrome://global/locale/extensions.properties");
 });
 
 var {
+  IconDetails,
+} = ExtensionParent;
+
+var {
   ExtensionError,
 } = ExtensionUtils;
 
 const TABHIDE_PREFNAME = "extensions.webextensions.tabhide.enabled";
 
 function showHiddenTabs(id) {
   let windowsEnum = Services.wm.getEnumerator("navigator:browser");
   while (windowsEnum.hasMoreElements()) {
@@ -33,16 +37,30 @@ function showHiddenTabs(id) {
       if (tab.hidden && tab.ownerGlobal &&
           SessionStore.getCustomTabValue(tab, "hiddenBy") === id) {
         win.gBrowser.showTab(tab);
       }
     }
   }
 }
 
+function removeStatusIcons(id) {
+  let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+  while (windowsEnum.hasMoreElements()) {
+    let win = windowsEnum.getNext();
+    if (win.closed || !win.gBrowser) {
+      continue;
+    }
+
+    for (let elem of win.document.getElementsByAttribute('extension-id', id)) {
+      elem.parentNode.removeChild(elem);
+    }
+  }
+}
+
 let tabListener = {
   tabReadyInitialized: false,
   tabReadyPromises: new WeakMap(),
   initializingTabs: new WeakSet(),
 
   initTabReady() {
     if (!this.tabReadyInitialized) {
       windowTracker.addListener("progress", this);
@@ -308,16 +326,17 @@ this.tabs = class extends ExtensionAPI {
   static onUpdate(id, manifest) {
     if (!manifest.permissions || !manifest.permissions.includes("tabHide")) {
       showHiddenTabs(id);
     }
   }
 
   static onDisable(id) {
     showHiddenTabs(id);
+    removeStatusIcons(id);
   }
 
   getAPI(context) {
     let {extension} = context;
 
     let {tabManager} = extension;
 
     function getTabOrActive(tabId) {
@@ -1224,13 +1243,40 @@ this.tabs = class extends ExtensionAPI {
               tab.ownerGlobal.gBrowser.hideTab(tab, extension.id);
               if (tab.hidden) {
                 hidden.push(tabTracker.getId(tab));
               }
             }
           }
           return hidden;
         },
+
+        // TODO - the icon won't stay on a tab that's detached or dragged
+        // between windows.
+        // TODO - this ought to support ThemeIcons.
+        setStatusIcon(tabId, iconSpec) {
+          let tab = tabTracker.getTab(tabId);
+          let img = tab.getElementsByAttribute('extension-id', extension.id)[0];
+
+          if (iconSpec === null) {
+            img && img.parentNode.removeChild(img);
+            return;
+          }
+
+          let {_size, icon} = IconDetails.getPreferredIcon(
+            IconDetails.normalize(iconSpec, extension, context),
+            extension,
+            16);
+
+          if (!img) {
+            img = tab.ownerDocument.createElementNS(XUL_NS, "image");
+            img.className = 'tab-extension-status';
+            img.setAttribute('extension-id', extension.id);
+            tab.appendChild(img);
+          }
+
+          img.src = icon;
+        },
       },
     };
     return self;
   }
 };
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -1367,16 +1367,69 @@
             "name": "tabIds",
             "description": "The TAB ID or list of TAB IDs to hide.",
             "choices": [
               {"type": "integer", "minimum": 0},
               {"type": "array", "items": {"type": "integer", "minimum": 0}}
             ]
           }
         ]
+      },
+      {
+        "name": "setStatusIcon",
+        "type": "function",
+        "description": "Sets or removes a status icon from the tab.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "integer",
+            "name": "tabId",
+            "minimum": 0
+          },
+          {
+            "name": "icon",
+            "type": "object",
+            "optional": true,
+            "description": "The icon to show in the tab, or null to remove the status icon.",
+            "properties": {
+              "imageData": {
+                "choices": [
+                  { "$ref": "ImageDataType" },
+                  {
+                    "type": "object",
+                    "patternProperties": {
+                      "^[1-9]\\d*$": {"$ref": "ImageDataType"}
+                    }
+                  }
+                ],
+                "optional": true,
+                "description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density."
+              },
+              "path": {
+                "choices": [
+                  { "type": "string" },
+                  {
+                    "type": "object",
+                    "patternProperties": {
+                      "^[1-9]\\d*$": { "type": "string" }
+                    }
+                  }
+                ],
+                "optional": true,
+                "description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density."
+              }
+            }
+          },
+          {
+            "type": "function",
+            "name": "callback",
+            "optional": true,
+            "parameters": []
+          }
+        ]
       }
     ],
     "events": [
       {
         "name": "onCreated",
         "type": "function",
         "description": "Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events to be notified when a URL is set.",
         "parameters": [
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -445,16 +445,24 @@
 }
 
 .tab-icon-sound[soundplaying-scheduledremoval]:not([muted]):not(:hover),
 .tab-icon-overlay[soundplaying-scheduledremoval]:not([muted]):not(:hover) {
   transition: opacity .3s linear var(--soundplaying-removal-delay);
   opacity: 0;
 }
 
+.tab-extension-status {
+  width: 16px;
+  height: 16px;
+  margin-top: 1px;
+  margin-inline-start: 1px;
+  padding: 0;
+}
+
 /* Tab Overflow */
 .tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
 .tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
   width: 18px;
   background-image: url(chrome://browser/skin/tabbrowser/tab-overflow-indicator.png);
   background-size: 17px 100%;
   background-repeat: no-repeat;
   border-left: 1px solid;