Bug 1408062 - Show hidden tabs that are playing audio r?dao
MozReview-Commit-ID: FHV3bmXbX9d
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -661,17 +661,17 @@
ondrop="newTabButtonObserver.onDrop(event)"
ondragover="newTabButtonObserver.onDragOver(event)"
ondragenter="newTabButtonObserver.onDragOver(event)"
ondragexit="newTabButtonObserver.onDragExit(event)"
cui-areatype="toolbar"
removable="true"/>
<toolbarbutton id="alltabs-button"
- class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button"
+ class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button badged-button"
type="menu"
label="&listAllTabs.label;"
tooltiptext="&listAllTabs.label;"
removable="false">
<menupopup id="alltabs-popup"
position="after_end">
<menuitem id="alltabs_undoCloseTab"
key="key_undoCloseTab"
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -2884,16 +2884,20 @@ window._gBrowser = {
browser.destroy();
}
var wasPinned = aTab.pinned;
// Remove the tab ...
this.tabContainer.removeChild(aTab);
+ // Update hashiddentabs if this tab was hidden.
+ if (aTab.hidden)
+ this.tabContainer._updateHiddenTabsStatus();
+
// ... and fix up the _tPos properties immediately.
for (let i = aTab._tPos; i < this.tabs.length; i++)
this.tabs[i]._tPos = i;
if (!this._windowIsClosing) {
if (wasPinned)
this.tabContainer._positionPinnedTabs();
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -99,16 +99,17 @@
style="width: 0;"/>
</xul:arrowscrollbox>
</content>
<implementation implements="nsIObserver">
<constructor>
<![CDATA[
this._tabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
+ this._hiddenSoundPlayingTabs = new Set();
let strId = PrivateBrowsingUtils.isWindowPrivate(window) ?
"emptyPrivateTabTitle" : "emptyTabTitle";
this.emptyTabTitle = gTabBrowserBundle.GetStringFromName("tabs." + strId);
var tab = this.firstChild;
tab.label = this.emptyTabTitle;
tab.setAttribute("onerror", "this.removeAttribute('image');");
@@ -902,21 +903,60 @@
<method name="onAreaReset">
<parameter name="aArea"/>
<parameter name="aContainer"/>
<body><![CDATA[
this.onAreaNodeRegistered(aArea, aContainer);
]]></body>
</method>
+
+ <method name="_hiddenSoundPlayingStatusChanged">
+ <parameter name="tab"/>
+ <parameter name="opts"/>
+ <body><![CDATA[
+ let closed = opts && opts.closed;
+ if (!closed && tab.soundPlaying && tab.hidden) {
+ this._hiddenSoundPlayingTabs.add(tab.id);
+ this.setAttribute("hiddensoundplaying", "true");
+ } else {
+ this._hiddenSoundPlayingTabs.delete(tab.id);
+ if (this._hiddenSoundPlayingTabs.size == 0) {
+ this.removeAttribute("hiddensoundplaying");
+ }
+ }
+ ]]></body>
+ </method>
</implementation>
<handlers>
<handler event="TabSelect" action="this._handleTabSelect();"/>
+ <handler event="TabClose"><![CDATA[
+ this._hiddenSoundPlayingStatusChanged(event.target, {closed: true});
+ ]]></handler>
+
+ <handler event="TabAttrModified"><![CDATA[
+ if (event.detail.changed.includes("soundplaying") && event.target.hidden) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ ]]></handler>
+
+ <handler event="TabHide"><![CDATA[
+ if (event.target.soundPlaying) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ ]]></handler>
+
+ <handler event="TabShow"><![CDATA[
+ if (event.target.soundPlaying) {
+ this._hiddenSoundPlayingStatusChanged(event.target);
+ }
+ ]]></handler>
+
<handler event="transitionend"><![CDATA[
if (event.propertyName != "max-width") {
return;
}
var tab = event.target;
if (tab.getAttribute("fadein") == "true") {
@@ -2130,33 +2170,41 @@
containersTab.setAttribute("disabled", "true");
}
document.getElementById("alltabs_undoCloseTab").disabled =
SessionStore.getClosedTabCount(window) == 0;
let showHiddenTabs = gBrowser.visibleTabs.length < gBrowser.tabs.length;
document.getElementById("alltabs_hiddenTabs").hidden = !showHiddenTabs;
- document.getElementById("alltabs-popup-separator-3").hidden = !showHiddenTabs;
+ let hiddenSeparator = document.getElementById("alltabs-popup-separator-3");
+ hiddenSeparator.hidden = !showHiddenTabs;
var tabcontainer = gBrowser.tabContainer;
// Listen for changes in the tab bar.
tabcontainer.addEventListener("TabAttrModified", this);
tabcontainer.addEventListener("TabClose", this);
- let tabs = gBrowser.visibleTabs;
+ let tabs = gBrowser.tabs;
let fragment = document.createDocumentFragment();
+ let hiddenFragment = document.createDocumentFragment();
for (var i = 0; i < tabs.length; i++) {
if (!tabs[i].pinned) {
- let li = this._createTabMenuItem(tabs[i]);
- fragment.appendChild(li);
+ if (!tabs[i].hidden) {
+ let li = this._createTabMenuItem(tabs[i]);
+ fragment.appendChild(li);
+ } else if (tabs[i].soundPlaying) {
+ let li = this._createTabMenuItem(tabs[i]);
+ hiddenFragment.appendChild(li);
+ }
}
}
this.appendChild(fragment);
+ this.insertBefore(hiddenFragment, hiddenSeparator);
this._updateTabsVisibilityStatus();
}
]]></handler>
<handler event="popuphidden">
<![CDATA[
if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
return;
--- a/browser/base/content/test/general/browser_audioTabIcon.js
+++ b/browser/base/content/test/general/browser_audioTabIcon.js
@@ -77,16 +77,28 @@ async function pause(tab, options) {
} finally {
// Make sure other tests don't timeout if an exception gets thrown above.
// Need to use setIntPref instead of clearUserPref because prefs_general.js
// overrides the default value to help this and other tests run faster.
Services.prefs.setIntPref(TABATTR_REMOVAL_PREFNAME, INITIAL_TABATTR_REMOVAL_DELAY_MS);
}
}
+async function hide_tab(tab) {
+ let tabHidden = BrowserTestUtils.waitForEvent(tab, "TabHide");
+ gBrowser.hideTab(tab);
+ return tabHidden;
+}
+
+async function show_tab(tab) {
+ let tabShown = BrowserTestUtils.waitForEvent(tab, "TabShow");
+ gBrowser.showTab(tab);
+ return tabShown;
+}
+
function disable_non_test_mouse(disable) {
let utils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.disableNonTestMouseEvents(disable);
}
function hover_icon(icon, tooltip) {
disable_non_test_mouse(true);
@@ -252,16 +264,51 @@ async function test_playing_icon_on_tab(
// Make sure it's possible to mute using the context menu.
await test_muting_using_menu(tab, false);
// Make sure it's possible to unmute using the context menu.
await test_muting_using_menu(tab, true);
}
+async function test_playing_icon_on_hidden_tab(tab) {
+ let icon = document.getAnonymousElementByAttribute(tab, "anonid", "soundplaying-icon");
+ let isActiveTab = tab === gBrowser.selectedTab;
+
+ await play(tab);
+
+ await test_tooltip(icon, "Mute tab", isActiveTab);
+
+ let alltabsButton = document.getElementById("alltabs-button");
+ let alltabsBadge = document.getAnonymousElementByAttribute(
+ alltabsButton, "class", "toolbarbutton-badge");
+
+ is(getComputedStyle(alltabsBadge).backgroundImage, "none", "The audio playing icon is hidden");
+ ok(!tab.parentNode.hasAttribute("hiddensoundplaying"), "There are no hidden audio tabs");
+
+ await hide_tab(tab);
+
+ is(getComputedStyle(alltabsBadge).backgroundImage,
+ 'url("chrome://browser/skin/tabbrowser/badge-audio-playing.svg")',
+ "The audio playing icon is shown");
+ is(tab.parentNode.getAttribute("hiddensoundplaying"), "true", "There are hidden audio tabs");
+
+ await pause(tab);
+
+ is(getComputedStyle(alltabsBadge).backgroundImage, "none", "The audio playing icon is hidden");
+ ok(!tab.parentNode.hasAttribute("hiddensoundplaying"), "There are no hidden audio tabs");
+
+ await show_tab(tab);
+
+ is(getComputedStyle(alltabsBadge).backgroundImage, "none", "The audio playing icon is hidden");
+ ok(!tab.parentNode.hasAttribute("hiddensoundplaying"), "There are no hidden audio tabs");
+
+ await play(tab);
+}
+
async function test_swapped_browser_while_playing(oldTab, newBrowser) {
// The tab was muted so it won't have soundplaying attribute even it's playing.
ok(oldTab.hasAttribute("muted"), "Expected the correct muted attribute on the old tab");
is(oldTab.muteReason, null, "Expected the correct muteReason attribute on the old tab");
ok(!oldTab.hasAttribute("soundplaying"), "Expected the correct soundplaying attribute on the old tab");
let newTab = gBrowser.getTabForBrowser(newBrowser);
let AttrChangePromise = BrowserTestUtils.waitForEvent(newTab, "TabAttrModified", false, event => {
@@ -457,35 +504,40 @@ async function test_mute_keybinding() {
}
await BrowserTestUtils.withNewTab({
gBrowser,
url: PAGE
}, taskFn);
}
-async function test_on_browser(browser) {
+async function test_on_browser(browser, lastTab) {
let tab = gBrowser.getTabForBrowser(browser);
// Test the icon in a normal tab.
await test_playing_icon_on_tab(tab, browser, false);
gBrowser.pinTab(tab);
// Test the icon in a pinned tab.
await test_playing_icon_on_tab(tab, browser, true);
gBrowser.unpinTab(tab);
+ if (lastTab) {
+ // Test for the hidden tabs icon, this must be tested on a background tab.
+ await test_playing_icon_on_hidden_tab(lastTab);
+ }
+
// Retest with another browser in the foreground tab
if (gBrowser.selectedBrowser.currentURI.spec == PAGE) {
await BrowserTestUtils.withNewTab({
gBrowser,
url: "data:text/html,test"
- }, () => test_on_browser(browser));
+ }, () => test_on_browser(browser, tab));
} else {
await test_browser_swapping(tab, browser);
}
}
async function test_delayed_tabattr_removal() {
async function taskFn(browser) {
let tab = gBrowser.getTabForBrowser(browser);
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -195,16 +195,17 @@
skin/classic/browser/tabbrowser/newtab.svg (../shared/tabbrowser/newtab.svg)
skin/classic/browser/tabbrowser/indicator-tab-attention.svg (../shared/tabbrowser/indicator-tab-attention.svg)
skin/classic/browser/tabbrowser/pendingpaint.png (../shared/tabbrowser/pendingpaint.png)
skin/classic/browser/tabbrowser/tab-audio-playing.svg (../shared/tabbrowser/tab-audio-playing.svg)
skin/classic/browser/tabbrowser/tab-audio-muted.svg (../shared/tabbrowser/tab-audio-muted.svg)
skin/classic/browser/tabbrowser/tab-audio-blocked.svg (../shared/tabbrowser/tab-audio-blocked.svg)
skin/classic/browser/tabbrowser/tab-audio-small.svg (../shared/tabbrowser/tab-audio-small.svg)
skin/classic/browser/tabbrowser/tab-overflow-indicator.png (../shared/tabbrowser/tab-overflow-indicator.png)
+ skin/classic/browser/tabbrowser/badge-audio-playing.svg (../shared/tabbrowser/badge-audio-playing.svg)
skin/classic/browser/translating-16.png (../shared/translation/translating-16.png)
skin/classic/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
skin/classic/browser/update-badge.svg (../shared/update-badge.svg)
skin/classic/browser/update-badge-failed.svg (../shared/update-badge-failed.svg)
skin/classic/browser/warning.svg (../shared/warning.svg)
new file mode 100755
--- /dev/null
+++ b/browser/themes/shared/tabbrowser/badge-audio-playing.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="12" height="12" viewBox="0 0 12 12">
+ <defs>
+ <path id="audio-12-a" d="M5.70499973,1.54742578 C5.75795864,1.49740557 5.83260727,1.48589108 5.89606106,1.51795472 C5.95951485,1.55001835 6.00012421,1.6197735 5.99999971,1.69649131 L5.99999971,10.3430764 C6.00000338,10.4043271 5.96755833,10.4599813 5.91690828,10.4856061 C5.86625823,10.5112309 5.80666296,10.5021418 5.76428544,10.4623288 L3.49999986,8 L2,8 C1.25,8 1,7.75 1,7 L1,5 C1,4.25 1.25,4 2,4 L3.49999986,4 L5.70499973,1.54742578 Z M7.5,10 C7.22385763,10 7,9.77614237 7,9.5 C7,9.22385763 7.22385763,9 7.5,9 C8.76394389,9 10,7.60819989 10,6 C10,4.39180011 8.76394389,3 7.5,3 C7.22385763,3 7,2.77614237 7,2.5 C7,2.22385763 7.22385763,2 7.5,2 C9.34440217,2 11,3.8642044 11,6 C11,8.1357956 9.34440217,10 7.5,10 Z M7,8.25 C6.72385763,8.25 6.5,8.02614237 6.5,7.75 C6.5,7.47385763 6.72385763,7.25 7,7.25 C7.48685738,7.25 8,6.67220102 8,6 C8,5.32779898 7.48685738,4.75 7,4.75 C6.72385763,4.75 6.5,4.52614237 6.5,4.25 C6.5,3.97385763 6.72385763,3.75 7,3.75 C8.06731566,3.75 9,4.80020327 9,6 C9,7.19979673 8.06731566,8.25 7,8.25 Z"/>
+ </defs>
+ <g fill="none" fill-rule="evenodd">
+ <mask id="audio-12-b" fill="#fff">
+ <use xlink:href="#audio-12-a"/>
+ </mask>
+ <g fill="context-fill" fill-opacity="context-fill-opacity" mask="url(#audio-12-b)">
+ <rect width="12" height="12"/>
+ </g>
+ </g>
+</svg>
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -714,16 +714,32 @@
}
/* All tabs button and menupopup */
#alltabs-button {
list-style-image: url(chrome://global/skin/icons/arrow-dropdown-16.svg);
}
+#tabbrowser-tabs[hiddensoundplaying] ~ #alltabs-button > .toolbarbutton-badge-stack > .toolbarbutton-badge {
+ background: transparent url(chrome://browser/skin/tabbrowser/badge-audio-playing.svg);
+ box-shadow: none;
+ /* Match the color of the button, rather than label default. */
+ color: inherit;
+ display: block;
+ -moz-context-properties: fill, fill-opacity;
+ fill: currentColor;
+ fill-opacity: var(--toolbarbutton-icon-fill-opacity);
+ /* "!important" is necessary to override the rule in toolbarbutton.css */
+ margin: -7px 0 0 !important;
+ margin-inline-end: -4px !important;
+ min-width: 12px;
+ min-height: 12px;
+}
+
.alltabs-item > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.svg");
-moz-context-properties: fill;
fill: currentColor;
}
.alltabs-item[busy] > .menu-iconic-left > .menu-iconic-icon {
list-style-image: url("chrome://global/skin/icons/loading.png");