Bug 1455300 - Part 1: Extract tab menuitem code from tabbrowser.xml r?dao draft
authorMark Striemer <mstriemer@mozilla.com>
Mon, 14 May 2018 15:10:01 -0500
changeset 800721 10b12939a132976f30117968528f598ff53fa4a2
parent 800720 c58d8cbdc302060f3ab7d0395344a54553ce28d0
child 800722 2e8a6583a0c3629cadc8d899c416f8f42efaf808
push id111447
push userbmo:mstriemer@mozilla.com
push dateMon, 28 May 2018 22:36:35 +0000
reviewersdao
bugs1455300
milestone62.0a1
Bug 1455300 - Part 1: Extract tab menuitem code from tabbrowser.xml r?dao MozReview-Commit-ID: LP0EZxe5cJ9
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/modules/TabsPopup.jsm
browser/modules/moz.build
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -52,16 +52,17 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Sanitizer: "resource:///modules/Sanitizer.jsm",
   SessionStore: "resource:///modules/sessionstore/SessionStore.jsm",
   SchedulePressure: "resource:///modules/SchedulePressure.jsm",
   ShortcutUtils: "resource://gre/modules/ShortcutUtils.jsm",
   SimpleServiceDiscovery: "resource://gre/modules/SimpleServiceDiscovery.jsm",
   SiteDataManager: "resource:///modules/SiteDataManager.jsm",
   SitePermissions: "resource:///modules/SitePermissions.jsm",
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
+  TabsPopup: "resource:///modules/TabsPopup.jsm",
   TelemetryStopwatch: "resource://gre/modules/TelemetryStopwatch.jsm",
   Translation: "resource:///modules/translation/Translation.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   Utils: "resource://gre/modules/sessionstore/Utils.jsm",
   Weave: "resource://services-sync/main.js",
   WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.jsm",
   fxAccounts: "resource://gre/modules/FxAccounts.jsm",
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2053,48 +2053,16 @@
       ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="tabbrowser-alltabs-popup"
            extends="chrome://global/content/bindings/popup.xml#popup">
     <implementation>
-      <method name="_tabOnAttrModified">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          var tab = aEvent.target;
-          if (tab.mCorrespondingMenuitem)
-            this._setMenuitemAttributes(tab.mCorrespondingMenuitem, tab);
-        ]]></body>
-      </method>
-
-      <method name="_tabOnTabClose">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          var tab = aEvent.target;
-          if (tab.mCorrespondingMenuitem)
-            this.removeChild(tab.mCorrespondingMenuitem);
-        ]]></body>
-      </method>
-
-      <method name="handleEvent">
-        <parameter name="aEvent"/>
-        <body><![CDATA[
-          switch (aEvent.type) {
-            case "TabAttrModified":
-              this._tabOnAttrModified(aEvent);
-              break;
-            case "TabClose":
-              this._tabOnTabClose(aEvent);
-              break;
-          }
-        ]]></body>
-      </method>
-
       <method name="_updateTabsVisibilityStatus">
         <body><![CDATA[
           var tabContainer = gBrowser.tabContainer;
           // We don't want menu item decoration unless there is overflow.
           if (tabContainer.getAttribute("overflow") != "true") {
             return;
           }
 
@@ -2113,100 +2081,58 @@
               menuitem.setAttribute("tabIsVisible", "true");
             } else {
               menuitem.removeAttribute("tabIsVisible");
             }
           }
         ]]></body>
       </method>
 
-      <method name="_createTabMenuItem">
-        <parameter name="aTab"/>
-        <body><![CDATA[
-          var menuItem = document.createElementNS(
-            "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
-            "menuitem");
-
-          menuItem.setAttribute("class", "menuitem-iconic alltabs-item menuitem-with-favicon");
-
-          this._setMenuitemAttributes(menuItem, aTab);
-
-          aTab.mCorrespondingMenuitem = menuItem;
-          menuItem.tab = aTab;
-
-          return menuItem;
-        ]]></body>
-      </method>
-
-      <method name="_setMenuitemAttributes">
-        <parameter name="aMenuitem"/>
-        <parameter name="aTab"/>
+      <method name="_initializeTabsPopups">
+        <parameter name="event"/>
         <body><![CDATA[
-          aMenuitem.setAttribute("label", aTab.label);
-          aMenuitem.setAttribute("crop", "end");
-
-          if (aTab.hasAttribute("busy")) {
-            aMenuitem.setAttribute("busy", aTab.getAttribute("busy"));
-            aMenuitem.removeAttribute("iconloadingprincipal");
-            aMenuitem.removeAttribute("image");
-          } else {
-            aMenuitem.setAttribute("iconloadingprincipal", aTab.getAttribute("iconloadingprincipal"));
-            aMenuitem.setAttribute("image", aTab.getAttribute("image"));
-            aMenuitem.removeAttribute("busy");
+          if (this._tabsPopups) {
+            return;
           }
-
-          if (aTab.hasAttribute("pending"))
-            aMenuitem.setAttribute("pending", aTab.getAttribute("pending"));
-          else
-            aMenuitem.removeAttribute("pending");
-
-          if (aTab.selected)
-            aMenuitem.setAttribute("selected", "true");
-          else
-            aMenuitem.removeAttribute("selected");
-
-          function addEndImage() {
-            let endImage = document.createElement("image");
-            endImage.setAttribute("class", "alltabs-endimage");
-            let endImageContainer = document.createElement("hbox");
-            endImageContainer.setAttribute("align", "center");
-            endImageContainer.setAttribute("pack", "center");
-            endImageContainer.appendChild(endImage);
-            aMenuitem.appendChild(endImageContainer);
-            return endImage;
-          }
-
-          if (aMenuitem.firstChild)
-            aMenuitem.firstChild.remove();
-          if (aTab.hasAttribute("muted"))
-            addEndImage().setAttribute("muted", "true");
-          else if (aTab.hasAttribute("soundplaying"))
-            addEndImage().setAttribute("soundplaying", "true");
+          // These TabsPopup objects will handle creating menuitem elements
+          // for tabs in this popup. They have their own popupshowing and
+          // popuphidden listeners to manage the items.
+          //
+          // Since gBrowser isn't initialized yet in the constructor these are
+          // created on the first popupshowing event. The initial event is
+          // proxied once the popups are created.
+          this._tabsPopups = [
+            new TabsPopup({
+              className: "alltabs-item",
+              filterFn: (tab) => !tab.pinned && !tab.hidden,
+              popup: document.getElementById("alltabs-popup"),
+              onPopulate: () => this._updateTabsVisibilityStatus(),
+            }),
+            new TabsPopup({
+              filterFn: (tab) => tab.hidden && tab.soundPlaying,
+              popup: document.getElementById("alltabs-popup"),
+              insertBefore: document.getElementById("alltabs-popup-separator-3"),
+            }),
+            new TabsPopup({
+              filterFn: (tab) => tab.hidden,
+              popup: document.getElementById("alltabs_hiddenTabsMenu"),
+            }),
+          ];
+          this._tabsPopups.forEach(popup => popup.handleEvent(event));
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="popupshowing">
       <![CDATA[
         if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
           createUserContextMenu(event, {useAccessKeys: false});
           return;
-        } else if (event.target.getAttribute("id") == "alltabs_hiddenTabsMenu") {
-          let fragment = document.createDocumentFragment();
-
-          for (let tab of gBrowser.tabs) {
-            if (tab.hidden) {
-              fragment.appendChild(this._createTabMenuItem(tab));
-            }
-          }
-
-          event.target.textContent = "";
-          event.target.appendChild(fragment);
-
+        } else if (event.target != this) {
           return;
         }
 
         let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
 
         if (event.target.getAttribute("anonid") == "newtab-popup" ||
             event.target.id == "newtab-popup") {
           createUserContextMenu(event, {
@@ -2222,69 +2148,19 @@
             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;
-          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);
+          document.getElementById("alltabs-popup-separator-3").hidden = !showHiddenTabs;
 
-          let tabs = gBrowser.tabs;
-          let fragment = document.createDocumentFragment();
-          let hiddenFragment = document.createDocumentFragment();
-          for (var i = 0; i < tabs.length; i++) {
-            if (!tabs[i].pinned) {
-              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;
-        }
-
-        // This could be the visible or hidden tabs menu container.
-        let container = event.target;
-
-        // clear out the menu popup and remove the listeners
-        for (let i = container.childNodes.length - 1; i > 0; i--) {
-          let menuItem = container.childNodes[i];
-          if (menuItem.tab) {
-            menuItem.tab.mCorrespondingMenuitem = null;
-            menuItem.remove();
-          }
-          if (menuItem.hasAttribute("usercontextid")) {
-            menuItem.remove();
-          }
-        }
-
-        if (container == this) {
-          gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
-          gBrowser.tabContainer.removeEventListener("TabClose", this);
+          this._initializeTabsPopups(event);
         }
       ]]></handler>
 
       <handler event="DOMMenuItemActive">
       <![CDATA[
         var tab = event.target.tab;
         if (tab) {
           let overLink = tab.linkedBrowser.currentURI.displaySpec;
@@ -2293,27 +2169,16 @@
           XULBrowserWindow.setOverLink(overLink, null);
         }
       ]]></handler>
 
       <handler event="DOMMenuItemInactive">
       <![CDATA[
         XULBrowserWindow.setOverLink("", null);
       ]]></handler>
-
-      <handler event="command"><![CDATA[
-        if (event.target.tab) {
-          if (gBrowser.selectedTab != event.target.tab) {
-            gBrowser.selectedTab = event.target.tab;
-          } else {
-            gBrowser.tabContainer._handleTabSelect();
-          }
-        }
-      ]]></handler>
-
     </handlers>
   </binding>
 
   <binding id="tabbrowser-tabpanels"
            extends="chrome://global/content/bindings/tabbox.xml#tabpanels">
     <implementation>
       <field name="_selectedIndex">0</field>
 
new file mode 100644
--- /dev/null
+++ b/browser/modules/TabsPopup.jsm
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TabsPopup"];
+
+class TabsPopup {
+  /*
+   * Handle menuitem rows for tab objects in a menupopup.
+   *
+   * @param {object} opts Options for configuring this instance.
+   * @param {string} opts.className
+   *                 An optional class name to be added to menuitem elements.
+   * @param {function} opts.filterFn
+   *                   A function to filter which tabs are added to the popup.
+   * @param {object} opts.insertBefore
+   *                 An optional element to insert the menuitems before in the
+   *                 popup, if omitted they will be appended to popup.
+   * @param {function} opts.onPopulate
+   *                   An optional function that will be called with the
+   *                   popupshowing event that caused the menu to be populated.
+   * @param {object} opts.popup
+   *                 A menupopup element to populate and register the show/hide
+   *                 listeners on.
+   */
+  constructor({className, filterFn, insertBefore, onPopulate, popup}) {
+    this.className = className;
+    this.filterFn = filterFn;
+    this.insertBefore = insertBefore;
+    this.onPopulate = onPopulate;
+    this.popup = popup;
+
+    this.doc = popup.ownerDocument;
+    this.gBrowser = this.doc.defaultView.gBrowser;
+    this.tabToMenuitem = new Map();
+    this.popup.addEventListener("popupshowing", this);
+  }
+
+  handleEvent(event) {
+    switch (event.type) {
+      case "TabAttrModified":
+        this._tabAttrModified(event.target);
+        break;
+      case "TabClose":
+        this._tabClose(event.target);
+        break;
+      case "command":
+        this._handleCommand(event.target.tab);
+        break;
+      case "popuphidden":
+        if (event.target == this.popup) {
+          this._cleanup();
+        }
+        break;
+      case "popupshowing":
+        if (event.target == this.popup) {
+          this._populate();
+          if (typeof this.onPopulate == "function") {
+            this.onPopulate(event);
+          }
+        }
+        break;
+    }
+  }
+
+  /*
+   * Populate the popup with menuitems and setup the listeners.
+   */
+  _populate() {
+    let fragment = this.doc.createDocumentFragment();
+
+    for (let tab of this.gBrowser.tabs) {
+      if (this.filterFn(tab)) {
+        fragment.appendChild(this._createMenuitem(tab));
+      }
+    }
+
+    if (this.insertBefore) {
+      this.popup.insertBefore(fragment, this.insertBefore);
+    } else {
+      this.popup.appendChild(fragment);
+    }
+
+    this._setupListeners();
+  }
+
+  /*
+   * Remove the menuitems from the DOM, cleanup internal state and listeners.
+   */
+  _cleanup() {
+    for (let item of this.tabToMenuitem.values()) {
+      item.remove();
+    }
+    this.tabToMenuitem = new Map();
+    this._cleanupListeners();
+  }
+
+  _setupListeners() {
+    this.gBrowser.tabContainer.addEventListener("TabAttrModified", this);
+    this.gBrowser.tabContainer.addEventListener("TabClose", this);
+    this.popup.addEventListener("popuphidden", this);
+  }
+
+  _cleanupListeners() {
+    this.gBrowser.tabContainer.removeEventListener("TabAttrModified", this);
+    this.gBrowser.tabContainer.removeEventListener("TabClose", this);
+    this.popup.removeEventListener("popuphidden", this);
+  }
+
+  _tabAttrModified(tab) {
+    let item = this.tabToMenuitem.get(tab);
+    if (item) {
+      if (!this.filterFn(tab)) {
+        // If the tab is no longer in this set of tabs, hide the item.
+        this._removeItem(item, tab);
+      } else {
+        this._setMenuitemAttributes(item, tab);
+      }
+    }
+  }
+
+  _tabClose(tab) {
+    let item = this.tabToMenuitem.get(tab);
+    if (item) {
+      this._removeItem(item, tab);
+    }
+  }
+
+  _removeItem(item, tab) {
+    this.tabToMenuitem.delete(tab);
+    item.remove();
+  }
+
+  _handleCommand(tab) {
+    if (this.gBrowser.selectedTab != tab) {
+      this.gBrowser.selectedTab = tab;
+    } else {
+      this.gBrowser.tabContainer._handleTabSelect();
+    }
+  }
+
+  _createMenuitem(tab) {
+    let item = this.doc.createElementNS(
+      "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+      "menuitem");
+    item.tab = tab;
+
+    item.setAttribute("class", "menuitem-iconic menuitem-with-favicon");
+    if (this.className) {
+      item.classList.add(this.className);
+    }
+    this._setMenuitemAttributes(item, tab);
+
+    this.tabToMenuitem.set(tab, item);
+
+    item.addEventListener("command", this);
+
+    return item;
+  }
+
+  _setMenuitemAttributes(item, tab) {
+    item.setAttribute("label", tab.label);
+    item.setAttribute("crop", "end");
+
+    if (tab.hasAttribute("busy")) {
+      item.setAttribute("busy", tab.getAttribute("busy"));
+      item.removeAttribute("iconloadingprincipal");
+      item.removeAttribute("image");
+    } else {
+      item.setAttribute("iconloadingprincipal", tab.getAttribute("iconloadingprincipal"));
+      item.setAttribute("image", tab.getAttribute("image"));
+      item.removeAttribute("busy");
+    }
+
+    if (tab.hasAttribute("pending"))
+      item.setAttribute("pending", tab.getAttribute("pending"));
+    else
+      item.removeAttribute("pending");
+
+    if (tab.selected)
+      item.setAttribute("selected", "true");
+    else
+      item.removeAttribute("selected");
+
+    let addEndImage = () => {
+      let endImage = this.doc.createElement("image");
+      endImage.setAttribute("class", "alltabs-endimage");
+      let endImageContainer = this.doc.createElement("hbox");
+      endImageContainer.setAttribute("align", "center");
+      endImageContainer.setAttribute("pack", "center");
+      endImageContainer.appendChild(endImage);
+      item.appendChild(endImageContainer);
+      return endImage;
+    };
+
+    if (item.firstChild)
+      item.firstChild.remove();
+    if (tab.hasAttribute("muted"))
+      addEndImage().setAttribute("muted", "true");
+    else if (tab.hasAttribute("soundplaying"))
+      addEndImage().setAttribute("soundplaying", "true");
+  }
+}
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -98,16 +98,19 @@ with Files("Sanitizer.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SiteDataManager.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("SitePermissions.jsm"):
     BUG_COMPONENT = ("Firefox", "Site Identity and Permission Panels")
 
+with Files("TabsPopup.jsm"):
+    BUG_COMPONENT = ("Firefox", "Tabbed Browser")
+
 with Files("ThemeVariableMap.jsm"):
     BUG_COMPONENT = ("Toolkit", "WebExtensions: Themes")
 
 with Files("TransientPrefs.jsm"):
     BUG_COMPONENT = ("Firefox", "Preferences")
 
 with Files("Windows8WindowFrameColor.jsm"):
     BUG_COMPONENT = ("Firefox", "Theme")
@@ -162,16 +165,17 @@ EXTRA_JS_MODULES += [
     'PluginContent.jsm',
     'ProcessHangMonitor.jsm',
     'ReaderParent.jsm',
     'RemotePrompt.jsm',
     'Sanitizer.jsm',
     'SchedulePressure.jsm',
     'SiteDataManager.jsm',
     'SitePermissions.jsm',
+    'TabsPopup.jsm',
     'ThemeVariableMap.jsm',
     'TransientPrefs.jsm',
     'webrtcUI.jsm',
     'ZoomUI.jsm',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES += [