Bug 1408062 - Show hidden tabs that are playing audio r?dao draft
authorMark Striemer <mstriemer@mozilla.com>
Fri, 20 Apr 2018 13:15:28 -0500
changeset 787999 a4919b615b5163e9d98d2ccbbe8b98a2a18bd6aa
parent 786770 bfc5f524402eb6545090da94477c2409b0be4252
push id107871
push userbmo:mstriemer@mozilla.com
push dateWed, 25 Apr 2018 20:26:47 +0000
reviewersdao
bugs1408062
milestone61.0a1
Bug 1408062 - Show hidden tabs that are playing audio r?dao MozReview-Commit-ID: FHV3bmXbX9d
browser/base/content/browser.xul
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/general/browser_audioTabIcon.js
browser/themes/shared/jar.inc.mn
browser/themes/shared/tabbrowser/badge-audio-playing.svg
browser/themes/shared/tabs.inc.css
--- 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");