Bug 1457658 - tabs.setStatusIcon WebExtension API
MozReview-Commit-ID: ELgyaB0fRPI
--- 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;