Bug 1455300 - Part 1: Extract tab menuitem code from tabbrowser.xml r?dao
MozReview-Commit-ID: LP0EZxe5cJ9
--- 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 += [