summaryrefslogtreecommitdiffstats
path: root/browser/base/content/tabbrowser.xml
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/tabbrowser.xml')
-rw-r--r--browser/base/content/tabbrowser.xml7417
1 files changed, 7417 insertions, 0 deletions
diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml
new file mode 100644
index 000000000..3f4c3518e
--- /dev/null
+++ b/browser/base/content/tabbrowser.xml
@@ -0,0 +1,7417 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<bindings id="tabBrowserBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="tabbrowser">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content>
+ <xul:stringbundle anonid="tbstringbundle" src="chrome://browser/locale/tabbrowser.properties"/>
+ <xul:tabbox anonid="tabbox" class="tabbrowser-tabbox"
+ flex="1" eventnode="document" xbl:inherits="handleCtrlPageUpDown"
+ onselect="if (event.target.localName == 'tabpanels') this.parentNode.updateCurrentBrowser();">
+ <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer">
+ <xul:notificationbox flex="1" notificationside="top">
+ <xul:hbox flex="1" class="browserSidebarContainer">
+ <xul:vbox flex="1" class="browserContainer">
+ <xul:stack flex="1" class="browserStack" anonid="browserStack">
+ <xul:browser anonid="initialBrowser" type="content-primary" message="true" messagemanagergroup="browsers"
+ xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,selectmenulist,datetimepicker"/>
+ </xul:stack>
+ </xul:vbox>
+ </xul:hbox>
+ </xul:notificationbox>
+ </xul:tabpanels>
+ </xul:tabbox>
+ <children/>
+ </content>
+ <implementation implements="nsIDOMEventListener, nsIMessageListener, nsIObserver">
+
+ <property name="tabContextMenu" readonly="true"
+ onget="return this.tabContainer.contextMenu;"/>
+
+ <field name="tabContainer" readonly="true">
+ document.getElementById(this.getAttribute("tabcontainer"));
+ </field>
+ <field name="tabs" readonly="true">
+ this.tabContainer.childNodes;
+ </field>
+
+ <property name="visibleTabs" readonly="true">
+ <getter><![CDATA[
+ if (!this._visibleTabs)
+ this._visibleTabs = Array.filter(this.tabs,
+ tab => !tab.hidden && !tab.closing);
+ return this._visibleTabs;
+ ]]></getter>
+ </property>
+
+ <field name="closingTabsEnum" readonly="true">({ ALL: 0, OTHER: 1, TO_END: 2 });</field>
+
+ <field name="_visibleTabs">null</field>
+
+ <field name="mURIFixup" readonly="true">
+ Components.classes["@mozilla.org/docshell/urifixup;1"]
+ .getService(Components.interfaces.nsIURIFixup);
+ </field>
+ <field name="_unifiedComplete" readonly="true">
+ Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
+ .getService(Components.interfaces.mozIPlacesAutoComplete);
+ </field>
+ <field name="AppConstants" readonly="true">
+ (Components.utils.import("resource://gre/modules/AppConstants.jsm", {})).AppConstants;
+ </field>
+ <field name="mTabBox" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "tabbox");
+ </field>
+ <field name="mPanelContainer" readonly="true">
+ document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer");
+ </field>
+ <field name="mStringBundle">
+ document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle");
+ </field>
+ <field name="mCurrentTab">
+ null
+ </field>
+ <field name="_lastRelatedTab">
+ null
+ </field>
+ <field name="mCurrentBrowser">
+ null
+ </field>
+ <field name="mProgressListeners">
+ []
+ </field>
+ <field name="mActiveResizeDisplayportSuppression">
+ null
+ </field>
+ <field name="mTabsProgressListeners">
+ []
+ </field>
+ <field name="_tabListeners">
+ new Map()
+ </field>
+ <field name="_tabFilters">
+ new Map()
+ </field>
+ <field name="mIsBusy">
+ false
+ </field>
+ <field name="_outerWindowIDBrowserMap">
+ new Map();
+ </field>
+ <field name="arrowKeysShouldWrap" readonly="true">
+ this.AppConstants.platform == "macosx";
+ </field>
+
+ <field name="_autoScrollPopup">
+ null
+ </field>
+
+ <field name="_previewMode">
+ false
+ </field>
+
+ <field name="_lastFindValue">
+ ""
+ </field>
+
+ <field name="_contentWaitingCount">
+ 0
+ </field>
+
+ <property name="_numPinnedTabs" readonly="true">
+ <getter><![CDATA[
+ for (var i = 0; i < this.tabs.length; i++) {
+ if (!this.tabs[i].pinned)
+ break;
+ }
+ return i;
+ ]]></getter>
+ </property>
+
+ <property name="popupAnchor" readonly="true">
+ <getter><![CDATA[
+ if (this.mCurrentTab._popupAnchor) {
+ return this.mCurrentTab._popupAnchor;
+ }
+ let stack = this.mCurrentBrowser.parentNode;
+ // Create an anchor for the popup
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let popupAnchor = document.createElementNS(NS_XUL, "hbox");
+ popupAnchor.className = "popup-anchor";
+ popupAnchor.hidden = true;
+ stack.appendChild(popupAnchor);
+ return this.mCurrentTab._popupAnchor = popupAnchor;
+ ]]></getter>
+ </property>
+
+ <method name="isFindBarInitialized">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ return (aTab || this.selectedTab)._findBar != undefined;
+ ]]></body>
+ </method>
+
+ <method name="getFindBar">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!aTab)
+ aTab = this.selectedTab;
+
+ if (aTab._findBar)
+ return aTab._findBar;
+
+ let findBar = document.createElementNS(this.namespaceURI, "findbar");
+ let browser = this.getBrowserForTab(aTab);
+ let browserContainer = this.getBrowserContainer(browser);
+ browserContainer.appendChild(findBar);
+
+ // Force a style flush to ensure that our binding is attached.
+ findBar.clientTop;
+
+ findBar.browser = browser;
+ findBar._findField.value = this._lastFindValue;
+
+ aTab._findBar = findBar;
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabFindInitialized", true, false);
+ aTab.dispatchEvent(event);
+
+ return findBar;
+ ]]></body>
+ </method>
+
+ <method name="getStatusPanel">
+ <body><![CDATA[
+ if (!this._statusPanel) {
+ this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
+ this._statusPanel.setAttribute("inactive", "true");
+ this._statusPanel.setAttribute("layer", "true");
+ this._appendStatusPanel();
+ }
+ return this._statusPanel;
+ ]]></body>
+ </method>
+
+ <method name="_appendStatusPanel">
+ <body><![CDATA[
+ if (this._statusPanel) {
+ let browser = this.selectedBrowser;
+ let browserContainer = this.getBrowserContainer(browser);
+ browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling);
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateWindowResizers">
+ <body><![CDATA[
+ if (!window.gShowPageResizers)
+ return;
+
+ var show = window.windowState == window.STATE_NORMAL;
+ for (let i = 0; i < this.browsers.length; i++) {
+ this.browsers[i].showWindowResizer = show;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_setCloseKeyState">
+ <parameter name="aEnabled"/>
+ <body><![CDATA[
+ let keyClose = document.getElementById("key_close");
+ let closeKeyEnabled = keyClose.getAttribute("disabled") != "true";
+ if (closeKeyEnabled == aEnabled)
+ return;
+
+ if (aEnabled)
+ keyClose.removeAttribute("disabled");
+ else
+ keyClose.setAttribute("disabled", "true");
+
+ // We also want to remove the keyboard shortcut from the file menu
+ // when the shortcut is disabled, and bring it back when it's
+ // renabled.
+ //
+ // Fixing bug 630826 could make that happen automatically.
+ // Fixing bug 630830 could avoid the ugly hack below.
+
+ let closeMenuItem = document.getElementById("menu_close");
+ let parentPopup = closeMenuItem.parentNode;
+ let nextItem = closeMenuItem.nextSibling;
+ let clonedItem = closeMenuItem.cloneNode(true);
+
+ parentPopup.removeChild(closeMenuItem);
+
+ if (aEnabled)
+ clonedItem.setAttribute("key", "key_close");
+ else
+ clonedItem.removeAttribute("key");
+
+ parentPopup.insertBefore(clonedItem, nextItem);
+ ]]></body>
+ </method>
+
+ <method name="pinTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (aTab.pinned)
+ return;
+
+ if (aTab.hidden)
+ this.showTab(aTab);
+
+ this.moveTabTo(aTab, this._numPinnedTabs);
+ aTab.setAttribute("pinned", "true");
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer.adjustTabstrip();
+
+ this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true })
+
+ if (aTab.selected)
+ this._setCloseKeyState(false);
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabPinned", true, false);
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="unpinTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!aTab.pinned)
+ return;
+
+ this.moveTabTo(aTab, this._numPinnedTabs - 1);
+ aTab.removeAttribute("pinned");
+ aTab.style.marginInlineStart = "";
+ this.tabContainer._unlockTabSizing();
+ this.tabContainer._positionPinnedTabs();
+ this.tabContainer.adjustTabstrip();
+
+ this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false })
+
+ if (aTab.selected)
+ this._setCloseKeyState(true);
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabUnpinned", true, false);
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="previewTab">
+ <parameter name="aTab"/>
+ <parameter name="aCallback"/>
+ <body>
+ <![CDATA[
+ let currentTab = this.selectedTab;
+ try {
+ // Suppress focus, ownership and selected tab changes
+ this._previewMode = true;
+ this.selectedTab = aTab;
+ aCallback();
+ } finally {
+ this.selectedTab = currentTab;
+ this._previewMode = false;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserAtIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ return this.browsers[aIndex];
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserIndexForDocument">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aDocument.defaultView);
+ return tab ? tab._tPos : -1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForDocument">
+ <parameter name="aDocument"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aDocument.defaultView);
+ return tab ? tab.linkedBrowser : null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForContentWindow">
+ <parameter name="aWindow"/>
+ <body>
+ <![CDATA[
+ var tab = this._getTabForContentWindow(aWindow);
+ return tab ? tab.linkedBrowser : null;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForOuterWindowID">
+ <parameter name="aID"/>
+ <body>
+ <![CDATA[
+ return this._outerWindowIDBrowserMap.get(aID);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_getTabForContentWindow">
+ <parameter name="aWindow"/>
+ <body>
+ <![CDATA[
+ // When not using remote browsers, we can take a fast path by getting
+ // directly from the content window to the browser without looping
+ // over all browsers.
+ if (!gMultiProcessBrowser) {
+ let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ return this.getTabForBrowser(browser);
+ }
+
+ for (let i = 0; i < this.browsers.length; i++) {
+ // NB: We use contentWindowAsCPOW so that this code works both
+ // for remote browsers as well. aWindow may be a CPOW.
+ if (this.browsers[i].contentWindowAsCPOW == aWindow)
+ return this.tabs[i];
+ }
+ return null;
+ ]]>
+ </body>
+ </method>
+
+ <!-- Binding from browser to tab -->
+ <field name="_tabForBrowser" readonly="true">
+ <![CDATA[
+ new WeakMap();
+ ]]>
+ </field>
+
+ <method name="_getTabForBrowser">
+ <parameter name="aBrowser" />
+ <body>
+ <![CDATA[
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser";
+ let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser";
+ Deprecated.warning(text, url);
+ return this.getTabForBrowser(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabForBrowser">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this._tabForBrowser.get(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getNotificationBox">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this.getSidebarContainer(aBrowser).parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getSidebarContainer">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return this.getBrowserContainer(aBrowser).parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserContainer">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ return (aBrowser || this.mCurrentBrowser).parentNode.parentNode;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabModalPromptBox">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ let browser = (aBrowser || this.mCurrentBrowser);
+ if (!browser.tabModalPromptBox) {
+ browser.tabModalPromptBox = new TabModalPromptBox(browser);
+ }
+ return browser.tabModalPromptBox;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabFromAudioEvent">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
+ !aEvent.isTrusted) {
+ return null;
+ }
+
+ var browser = aEvent.originalTarget;
+ var tab = this.getTabForBrowser(browser);
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_callProgressListeners">
+ <parameter name="aBrowser"/>
+ <parameter name="aMethod"/>
+ <parameter name="aArguments"/>
+ <parameter name="aCallGlobalListeners"/>
+ <parameter name="aCallTabsListeners"/>
+ <body><![CDATA[
+ var rv = true;
+
+ function callListeners(listeners, args) {
+ for (let p of listeners) {
+ if (aMethod in p) {
+ try {
+ if (!p[aMethod].apply(p, args))
+ rv = false;
+ } catch (e) {
+ // don't inhibit other listeners
+ Components.utils.reportError(e);
+ }
+ }
+ }
+ }
+
+ if (!aBrowser)
+ aBrowser = this.mCurrentBrowser;
+
+ if (aCallGlobalListeners != false &&
+ aBrowser == this.mCurrentBrowser) {
+ callListeners(this.mProgressListeners, aArguments);
+ }
+
+ if (aCallTabsListeners != false) {
+ aArguments.unshift(aBrowser);
+
+ callListeners(this.mTabsProgressListeners, aArguments);
+ }
+
+ return rv;
+ ]]></body>
+ </method>
+
+ <!-- A web progress listener object definition for a given tab. -->
+ <method name="mTabProgressListener">
+ <parameter name="aTab"/>
+ <parameter name="aBrowser"/>
+ <parameter name="aStartsBlank"/>
+ <parameter name="aWasPreloadedBrowser"/>
+ <parameter name="aOrigStateFlags"/>
+ <body>
+ <![CDATA[
+ let stateFlags = aOrigStateFlags || 0;
+ // Initialize mStateFlags to non-zero e.g. when creating a progress
+ // listener for preloaded browsers as there was no progress listener
+ // around when the content started loading. If the content didn't
+ // quite finish loading yet, mStateFlags will very soon be overridden
+ // with the correct value and end up at STATE_STOP again.
+ if (aWasPreloadedBrowser) {
+ stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
+ Ci.nsIWebProgressListener.STATE_IS_REQUEST;
+ }
+
+ return ({
+ mTabBrowser: this,
+ mTab: aTab,
+ mBrowser: aBrowser,
+ mBlank: aStartsBlank,
+
+ // cache flags for correct status UI update after tab switching
+ mStateFlags: stateFlags,
+ mStatus: 0,
+ mMessage: "",
+ mTotalProgress: 0,
+
+ // count of open requests (should always be 0 or 1)
+ mRequestCount: 0,
+
+ destroy: function () {
+ delete this.mTab;
+ delete this.mBrowser;
+ delete this.mTabBrowser;
+ },
+
+ _callProgressListeners: function () {
+ Array.unshift(arguments, this.mBrowser);
+ return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments);
+ },
+
+ _shouldShowProgress: function (aRequest) {
+ if (this.mBlank)
+ return false;
+
+ // Don't show progress indicators in tabs for about: URIs
+ // pointing to local resources.
+ if ((aRequest instanceof Ci.nsIChannel) &&
+ aRequest.originalURI.schemeIs("about") &&
+ (aRequest.URI.schemeIs("jar") || aRequest.URI.schemeIs("file")))
+ return false;
+
+ return true;
+ },
+
+ _isForInitialAboutBlank: function (aWebProgress, aLocation) {
+ if (!this.mBlank || !aWebProgress.isTopLevel) {
+ return false;
+ }
+
+ let location = aLocation ? aLocation.spec : "";
+ return location == "about:blank";
+ },
+
+ onProgressChange: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
+
+ if (!this._shouldShowProgress(aRequest))
+ return;
+
+ if (this.mTotalProgress)
+ this.mTab.setAttribute("progress", "true");
+
+ this._callProgressListeners("onProgressChange",
+ [aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress]);
+ },
+
+ onProgressChange64: function (aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress) {
+ return this.onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
+ aMaxTotalProgress);
+ },
+
+ onStateChange: function (aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (!aRequest)
+ return;
+
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ const nsIChannel = Components.interfaces.nsIChannel;
+ let location, originalLocation;
+ try {
+ aRequest.QueryInterface(nsIChannel)
+ location = aRequest.URI;
+ originalLocation = aRequest.originalURI;
+ } catch (ex) {}
+
+ let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, location);
+ // If we were ignoring some messages about the initial about:blank, and we
+ // got the STATE_STOP for it, we'll want to pay attention to those messages
+ // from here forward. Similarly, if we conclude that this state change
+ // is one that we shouldn't be ignoring, then stop ignoring.
+ if ((ignoreBlank &&
+ aStateFlags & nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
+ !ignoreBlank && this.mBlank) {
+ this.mBlank = false;
+ }
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START) {
+ this.mRequestCount++;
+ }
+ else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
+ const NS_ERROR_UNKNOWN_HOST = 2152398878;
+ if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
+ // to prevent bug 235825: wait for the request handled
+ // by the automatic keyword resolver
+ return;
+ }
+ // since we (try to) only handle STATE_STOP of the last request,
+ // the count of open requests should now be 0
+ this.mRequestCount = 0;
+ }
+
+ if (aStateFlags & nsIWebProgressListener.STATE_START &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (aWebProgress.isTopLevel) {
+ // Need to use originalLocation rather than location because things
+ // like about:home and about:privatebrowsing arrive with nsIRequest
+ // pointing to their resolved jar: or file: URIs.
+ if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
+ originalLocation != "about:blank" &&
+ this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
+ this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
+ // Indicating that we started a load will allow the location
+ // bar to be cleared when the load finishes.
+ // In order to not overwrite user-typed content, we avoid it
+ // (see if condition above) in a very specific case:
+ // If the load is of an 'initial' page (e.g. about:privatebrowsing,
+ // about:newtab, etc.), was not explicitly typed in the location
+ // bar by the user, is not about:blank (because about:blank can be
+ // loaded by websites under their principal), and the current
+ // page in the browser is about:blank (indicating it is a newly
+ // created or re-created browser, e.g. because it just switched
+ // remoteness or is a new tab/window).
+ this.mBrowser.urlbarChangeTracker.startedLoad();
+ }
+ delete this.mBrowser.initialPageLoadedFromURLBar;
+ // If the browser is loading it must not be crashed anymore
+ this.mTab.removeAttribute("crashed");
+ }
+
+ if (this._shouldShowProgress(aRequest)) {
+ if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
+ this.mTab.setAttribute("busy", "true");
+
+ if (aWebProgress.isTopLevel &&
+ !(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD))
+ this.mTabBrowser.setTabTitleLoading(this.mTab);
+ }
+
+ if (this.mTab.selected)
+ this.mTabBrowser.mIsBusy = true;
+ }
+ }
+ else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
+
+ if (this.mTab.hasAttribute("busy")) {
+ this.mTab.removeAttribute("busy");
+ this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
+ if (!this.mTab.selected)
+ this.mTab.setAttribute("unread", "true");
+ }
+ this.mTab.removeAttribute("progress");
+
+ if (aWebProgress.isTopLevel) {
+ let isSuccessful = Components.isSuccessCode(aStatus);
+ if (!isSuccessful && !isTabEmpty(this.mTab)) {
+ // Restore the current document's location in case the
+ // request was stopped (possibly from a content script)
+ // before the location changed.
+
+ this.mBrowser.userTypedValue = null;
+
+ let inLoadURI = this.mBrowser.inLoadURI;
+ if (this.mTab.selected && gURLBar && !inLoadURI) {
+ URLBarSetURI();
+ }
+ } else if (isSuccessful) {
+ this.mBrowser.urlbarChangeTracker.finishedLoad();
+ }
+
+ if (!this.mBrowser.mIconURL)
+ this.mTabBrowser.useDefaultIcon(this.mTab);
+ }
+
+ // For keyword URIs clear the user typed value since they will be changed into real URIs
+ if (location.scheme == "keyword")
+ this.mBrowser.userTypedValue = null;
+
+ if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.connecting"))
+ this.mTabBrowser.setTabTitle(this.mTab);
+
+ if (this.mTab.selected)
+ this.mTabBrowser.mIsBusy = false;
+ }
+
+ if (ignoreBlank) {
+ this._callProgressListeners("onUpdateCurrentBrowser",
+ [aStateFlags, aStatus, "", 0],
+ true, false);
+ } else {
+ this._callProgressListeners("onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ true, false);
+ }
+
+ this._callProgressListeners("onStateChange",
+ [aWebProgress, aRequest, aStateFlags, aStatus],
+ false);
+
+ if (aStateFlags & (nsIWebProgressListener.STATE_START |
+ nsIWebProgressListener.STATE_STOP)) {
+ // reset cached temporary values at beginning and end
+ this.mMessage = "";
+ this.mTotalProgress = 0;
+ }
+ this.mStateFlags = aStateFlags;
+ this.mStatus = aStatus;
+ },
+
+ onLocationChange: function (aWebProgress, aRequest, aLocation,
+ aFlags) {
+ // OnLocationChange is called for both the top-level content
+ // and the subframes.
+ let topLevel = aWebProgress.isTopLevel;
+
+ if (topLevel) {
+ let isSameDocument =
+ !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
+ // We need to clear the typed value
+ // if the document failed to load, to make sure the urlbar reflects the
+ // failed URI (particularly for SSL errors). However, don't clear the value
+ // if the error page's URI is about:blank, because that causes complete
+ // loss of urlbar contents for invalid URI errors (see bug 867957).
+ // Another reason to clear the userTypedValue is if this was an anchor
+ // navigation initiated by the user.
+ if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
+ ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
+ aLocation.spec != "about:blank") ||
+ (isSameDocument && this.mBrowser.inLoadURI)) {
+ this.mBrowser.userTypedValue = null;
+ }
+
+ // If the browser was playing audio, we should remove the playing state.
+ if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
+ clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
+ this.mTab._soundPlayingAttrRemovalTimer = 0;
+ this.mTab.removeAttribute("soundplaying");
+ this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
+ }
+
+ // If the browser was previously muted, we should restore the muted state.
+ if (this.mTab.hasAttribute("muted")) {
+ this.mTab.linkedBrowser.mute();
+ }
+
+ if (this.mTabBrowser.isFindBarInitialized(this.mTab)) {
+ let findBar = this.mTabBrowser.getFindBar(this.mTab);
+
+ // Close the Find toolbar if we're in old-style TAF mode
+ if (findBar.findMode != findBar.FIND_NORMAL) {
+ findBar.close();
+ }
+ }
+
+ // Don't clear the favicon if this onLocationChange was
+ // triggered by a pushState or a replaceState (bug 550565) or
+ // a hash change (bug 408415).
+ if (aWebProgress.isLoadingDocument && !isSameDocument) {
+ this.mBrowser.mIconURL = null;
+ }
+
+ let unifiedComplete = this.mTabBrowser._unifiedComplete;
+ let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
+ if (this.mBrowser.registeredOpenURI) {
+ unifiedComplete.unregisterOpenPage(this.mBrowser.registeredOpenURI,
+ userContextId);
+ delete this.mBrowser.registeredOpenURI;
+ }
+ // Tabs in private windows aren't registered as "Open" so
+ // that they don't appear as switch-to-tab candidates.
+ if (!isBlankPageURL(aLocation.spec) &&
+ (!PrivateBrowsingUtils.isWindowPrivate(window) ||
+ PrivateBrowsingUtils.permanentPrivateBrowsing)) {
+ unifiedComplete.registerOpenPage(aLocation, userContextId);
+ this.mBrowser.registeredOpenURI = aLocation;
+ }
+ }
+
+ if (!this.mBlank) {
+ this._callProgressListeners("onLocationChange",
+ [aWebProgress, aRequest, aLocation,
+ aFlags]);
+ }
+
+ if (topLevel) {
+ this.mBrowser.lastURI = aLocation;
+ this.mBrowser.lastLocationChange = Date.now();
+ }
+ },
+
+ onStatusChange: function (aWebProgress, aRequest, aStatus, aMessage) {
+ if (this.mBlank)
+ return;
+
+ this._callProgressListeners("onStatusChange",
+ [aWebProgress, aRequest, aStatus, aMessage]);
+
+ this.mMessage = aMessage;
+ },
+
+ onSecurityChange: function (aWebProgress, aRequest, aState) {
+ this._callProgressListeners("onSecurityChange",
+ [aWebProgress, aRequest, aState]);
+ },
+
+ onRefreshAttempted: function (aWebProgress, aURI, aDelay, aSameURI) {
+ return this._callProgressListeners("onRefreshAttempted",
+ [aWebProgress, aURI, aDelay, aSameURI]);
+ },
+
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
+ aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
+ aIID.equals(Components.interfaces.nsISupports))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ }
+ });
+ ]]>
+ </body>
+ </method>
+
+ <field name="serializationHelper">
+ Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+ </field>
+
+ <field name="mIconLoadingPrincipal">
+ null
+ </field>
+
+ <method name="setIcon">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <parameter name="aLoadingPrincipal"/>
+ <body>
+ <![CDATA[
+ let browser = this.getBrowserForTab(aTab);
+ browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
+ let loadingPrincipal = aLoadingPrincipal
+ ? aLoadingPrincipal
+ : Services.scriptSecurityManager.getSystemPrincipal();
+
+ if (aURI) {
+ if (!(aURI instanceof Ci.nsIURI)) {
+ aURI = makeURI(aURI);
+ }
+ PlacesUIUtils.loadFavicon(browser, loadingPrincipal, aURI);
+ }
+
+ let sizedIconUrl = browser.mIconURL || "";
+ if (sizedIconUrl != aTab.getAttribute("image")) {
+ if (sizedIconUrl) {
+ aTab.setAttribute("image", sizedIconUrl);
+ if (!browser.mIconLoadingPrincipal ||
+ !browser.mIconLoadingPrincipal.equals(loadingPrincipal)) {
+ aTab.setAttribute("iconLoadingPrincipal",
+ this.serializationHelper.serializeToString(loadingPrincipal));
+ browser.mIconLoadingPrincipal = loadingPrincipal;
+ }
+ }
+ else {
+ aTab.removeAttribute("image");
+ aTab.removeAttribute("iconLoadingPrincipal");
+ delete browser.mIconLoadingPrincipal;
+ }
+ this._tabAttrModified(aTab, ["image"]);
+ }
+
+ this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getIcon">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
+ return browser.mIconURL;
+ ]]>
+ </body>
+ </method>
+
+ <method name="shouldLoadFavIcon">
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ return (aURI &&
+ Services.prefs.getBoolPref("browser.chrome.site_icons") &&
+ Services.prefs.getBoolPref("browser.chrome.favicons") &&
+ ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https")));
+ ]]>
+ </body>
+ </method>
+
+ <method name="useDefaultIcon">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var browser = this.getBrowserForTab(aTab);
+ var documentURI = browser.documentURI;
+ var icon = null;
+
+ if (browser.imageDocument) {
+ if (Services.prefs.getBoolPref("browser.chrome.site_icons")) {
+ let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size");
+ if (browser.imageDocument.width <= sz &&
+ browser.imageDocument.height <= sz) {
+ icon = browser.currentURI;
+ }
+ }
+ }
+
+ // Use documentURIObject in the check for shouldLoadFavIcon so that we
+ // do the right thing with about:-style error pages. Bug 453442
+ if (!icon && this.shouldLoadFavIcon(documentURI)) {
+ let url = documentURI.prePath + "/favicon.ico";
+ if (!this.isFailedIcon(url))
+ icon = url;
+ }
+ this.setIcon(aTab, icon, browser.contentPrincipal);
+ ]]>
+ </body>
+ </method>
+
+ <method name="isFailedIcon">
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ if (!(aURI instanceof Ci.nsIURI))
+ aURI = makeURI(aURI);
+ return PlacesUtils.favicons.isFailedFavicon(aURI);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getWindowTitleForBrowser">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ var newTitle = "";
+ var docElement = this.ownerDocument.documentElement;
+ var sep = docElement.getAttribute("titlemenuseparator");
+
+ // Strip out any null bytes in the content title, since the
+ // underlying widget implementations of nsWindow::SetTitle pass
+ // null-terminated strings to system APIs.
+ var docTitle = aBrowser.contentTitle.replace(/\0/g, "");
+
+ if (!docTitle)
+ docTitle = docElement.getAttribute("titledefault");
+
+ var modifier = docElement.getAttribute("titlemodifier");
+ if (docTitle) {
+ newTitle += docElement.getAttribute("titlepreface");
+ newTitle += docTitle;
+ if (modifier)
+ newTitle += sep;
+ }
+ newTitle += modifier;
+
+ // If location bar is hidden and the URL type supports a host,
+ // add the scheme and host to the title to prevent spoofing.
+ // XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
+ try {
+ if (docElement.getAttribute("chromehidden").includes("location")) {
+ var uri = this.mURIFixup.createExposableURI(
+ aBrowser.currentURI);
+ if (uri.scheme == "about")
+ newTitle = uri.spec + sep + newTitle;
+ else
+ newTitle = uri.prePath + sep + newTitle;
+ }
+ } catch (e) {}
+
+ return newTitle;
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateTitlebar">
+ <body>
+ <![CDATA[
+ this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Holds a unique ID for the tab change that's currently being timed.
+ Used to make sure that multiple, rapid tab switches do not try to
+ create overlapping timers. -->
+ <field name="_tabSwitchID">null</field>
+
+ <method name="updateCurrentBrowser">
+ <parameter name="aForceUpdate"/>
+ <body>
+ <![CDATA[
+ var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
+ if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
+ return;
+
+ if (!aForceUpdate) {
+ TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
+ if (!gMultiProcessBrowser) {
+ // old way of measuring tab paint which is not valid with e10s.
+ // Waiting until the next MozAfterPaint ensures that we capture
+ // the time it takes to paint, upload the textures to the compositor,
+ // and then composite.
+ if (this._tabSwitchID) {
+ TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
+ }
+
+ let tabSwitchID = Symbol();
+
+ TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
+ this._tabSwitchID = tabSwitchID;
+
+ let onMozAfterPaint = () => {
+ if (this._tabSwitchID === tabSwitchID) {
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
+ this._tabSwitchID = null;
+ }
+ window.removeEventListener("MozAfterPaint", onMozAfterPaint);
+ }
+ window.addEventListener("MozAfterPaint", onMozAfterPaint);
+ }
+ }
+
+ var oldTab = this.mCurrentTab;
+
+ // Preview mode should not reset the owner
+ if (!this._previewMode && !oldTab.selected)
+ oldTab.owner = null;
+
+ if (this._lastRelatedTab) {
+ if (!this._lastRelatedTab.selected)
+ this._lastRelatedTab.owner = null;
+ this._lastRelatedTab = null;
+ }
+
+ var oldBrowser = this.mCurrentBrowser;
+
+ if (!gMultiProcessBrowser) {
+ oldBrowser.setAttribute("type", "content-targetable");
+ oldBrowser.docShellIsActive = false;
+ newBrowser.setAttribute("type", "content-primary");
+ newBrowser.docShellIsActive =
+ (window.windowState != window.STATE_MINIMIZED);
+ }
+
+ var updateBlockedPopups = false;
+ if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) ||
+ (!oldBrowser.blockedPopups && newBrowser.blockedPopups))
+ updateBlockedPopups = true;
+
+ this.mCurrentBrowser = newBrowser;
+ this.mCurrentTab = this.tabContainer.selectedItem;
+ this.showTab(this.mCurrentTab);
+
+ var forwardButtonContainer = document.getElementById("urlbar-wrapper");
+ if (forwardButtonContainer) {
+ forwardButtonContainer.setAttribute("switchingtabs", "true");
+ window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() {
+ window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr);
+ forwardButtonContainer.removeAttribute("switchingtabs");
+ });
+ }
+
+ this._appendStatusPanel();
+
+ if (updateBlockedPopups)
+ this.mCurrentBrowser.updateBlockedPopups();
+
+ // Update the URL bar.
+ var loc = this.mCurrentBrowser.currentURI;
+
+ var webProgress = this.mCurrentBrowser.webProgress;
+ var securityUI = this.mCurrentBrowser.securityUI;
+
+ this._callProgressListeners(null, "onLocationChange",
+ [webProgress, null, loc, 0], true,
+ false);
+
+ if (securityUI) {
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(null, "onSecurityChange",
+ [webProgress, null, securityUI.state, true],
+ true, false);
+ }
+
+ var listener = this._tabListeners.get(this.mCurrentTab);
+ if (listener && listener.mStateFlags) {
+ this._callProgressListeners(null, "onUpdateCurrentBrowser",
+ [listener.mStateFlags, listener.mStatus,
+ listener.mMessage, listener.mTotalProgress],
+ true, false);
+ }
+
+ if (!this._previewMode) {
+ this._recordTabAccess(this.mCurrentTab);
+
+ this.mCurrentTab.updateLastAccessed();
+ this.mCurrentTab.removeAttribute("unread");
+ oldTab.updateLastAccessed();
+
+ let oldFindBar = oldTab._findBar;
+ if (oldFindBar &&
+ oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
+ !oldFindBar.hidden)
+ this._lastFindValue = oldFindBar._findField.value;
+
+ this.updateTitlebar();
+
+ this.mCurrentTab.removeAttribute("titlechanged");
+ this.mCurrentTab.removeAttribute("attention");
+ }
+
+ // If the new tab is busy, and our current state is not busy, then
+ // we need to fire a start to all progress listeners.
+ const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
+ if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) {
+ this.mIsBusy = true;
+ this._callProgressListeners(null, "onStateChange",
+ [webProgress, null,
+ nsIWebProgressListener.STATE_START |
+ nsIWebProgressListener.STATE_IS_NETWORK, 0],
+ true, false);
+ }
+
+ // If the new tab is not busy, and our current state is busy, then
+ // we need to fire a stop to all progress listeners.
+ if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) {
+ this.mIsBusy = false;
+ this._callProgressListeners(null, "onStateChange",
+ [webProgress, null,
+ nsIWebProgressListener.STATE_STOP |
+ nsIWebProgressListener.STATE_IS_NETWORK, 0],
+ true, false);
+ }
+
+ this._setCloseKeyState(!this.mCurrentTab.pinned);
+
+ // TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
+ // that might rely upon the other changes suppressed.
+ // Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
+ if (!this._previewMode) {
+ // We've selected the new tab, so go ahead and notify listeners.
+ let event = new CustomEvent("TabSelect", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ previousTab: oldTab
+ }
+ });
+ this.mCurrentTab.dispatchEvent(event);
+
+ this._tabAttrModified(oldTab, ["selected"]);
+ this._tabAttrModified(this.mCurrentTab, ["selected"]);
+
+ if (oldBrowser != newBrowser &&
+ oldBrowser.getInPermitUnload) {
+ oldBrowser.getInPermitUnload(inPermitUnload => {
+ if (!inPermitUnload) {
+ return;
+ }
+ // Since the user is switching away from a tab that has
+ // a beforeunload prompt active, we remove the prompt.
+ // This prevents confusing user flows like the following:
+ // 1. User attempts to close Firefox
+ // 2. User switches tabs (ingoring a beforeunload prompt)
+ // 3. User returns to tab, presses "Leave page"
+ let promptBox = this.getTabModalPromptBox(oldBrowser);
+ let prompts = promptBox.listPrompts();
+ // There might not be any prompts here if the tab was closed
+ // while in an onbeforeunload prompt, which will have
+ // destroyed aforementioned prompt already, so check there's
+ // something to remove, first:
+ if (prompts.length) {
+ // NB: This code assumes that the beforeunload prompt
+ // is the top-most prompt on the tab.
+ prompts[prompts.length - 1].abortPrompt();
+ }
+ });
+ }
+
+ oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
+ if (this.isFindBarInitialized(oldTab)) {
+ let findBar = this.getFindBar(oldTab);
+ oldTab._findBarFocused = (!findBar.hidden &&
+ findBar._findField.getAttribute("focused") == "true");
+ }
+
+ // If focus is in the tab bar, retain it there.
+ if (document.activeElement == oldTab) {
+ // We need to explicitly focus the new tab, because
+ // tabbox.xml does this only in some cases.
+ this.mCurrentTab.focus();
+ } else if (gMultiProcessBrowser && document.activeElement !== newBrowser) {
+ // Clear focus so that _adjustFocusAfterTabSwitch can detect if
+ // some element has been focused and respect that.
+ document.activeElement.blur();
+ }
+
+ if (!gMultiProcessBrowser)
+ this._adjustFocusAfterTabSwitch(this.mCurrentTab);
+ }
+
+ updateUserContextUIIndicator();
+ gIdentityHandler.updateSharingIndicator();
+
+ this.tabContainer._setPositionalAttributes();
+
+ if (!gMultiProcessBrowser) {
+ let event = new CustomEvent("TabSwitchDone", {
+ bubbles: true,
+ cancelable: true
+ });
+ this.dispatchEvent(event);
+ }
+
+ if (!aForceUpdate)
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
+ ]]>
+ </body>
+ </method>
+
+ <method name="_adjustFocusAfterTabSwitch">
+ <parameter name="newTab"/>
+ <body><![CDATA[
+ // Don't steal focus from the tab bar.
+ if (document.activeElement == newTab)
+ return;
+
+ let newBrowser = this.getBrowserForTab(newTab);
+
+ // If there's a tabmodal prompt showing, focus it.
+ if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
+ let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
+ let prompt = prompts[prompts.length - 1];
+ prompt.Dialog.setDefaultFocus();
+ return;
+ }
+
+ // Focus the location bar if it was previously focused for that tab.
+ // In full screen mode, only bother making the location bar visible
+ // if the tab is a blank one.
+ if (newBrowser._urlbarFocused && gURLBar) {
+ // Explicitly close the popup if the URL bar retains focus
+ gURLBar.closePopup();
+
+ if (!window.fullScreen) {
+ gURLBar.focus();
+ return;
+ }
+
+ if (isTabEmpty(this.mCurrentTab)) {
+ focusAndSelectUrlBar();
+ return;
+ }
+ }
+
+ // Focus the find bar if it was previously focused for that tab.
+ if (gFindBarInitialized && !gFindBar.hidden &&
+ this.selectedTab._findBarFocused) {
+ gFindBar._findField.focus();
+ return;
+ }
+
+ // Don't focus the content area if something has been focused after the
+ // tab switch was initiated.
+ if (gMultiProcessBrowser &&
+ document.activeElement != document.documentElement)
+ return;
+
+ // We're now committed to focusing the content area.
+ let fm = Services.focus;
+ let focusFlags = fm.FLAG_NOSCROLL;
+
+ if (!gMultiProcessBrowser) {
+ let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {});
+
+ // for anchors, use FLAG_SHOWRING so that it is clear what link was
+ // last clicked when switching back to that tab
+ if (newFocusedElement &&
+ (newFocusedElement instanceof HTMLAnchorElement ||
+ newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple"))
+ focusFlags |= fm.FLAG_SHOWRING;
+ }
+
+ fm.setFocus(newBrowser, focusFlags);
+ ]]></body>
+ </method>
+
+ <!--
+ This function assumes we have an LRU cache of tabs (either
+ images of tab content or their layers). The goal is to find
+ out how far into the cache we need to look in order to find
+ aTab. We record this number in telemetry and also move aTab to
+ the front of the cache.
+
+ A newly created tab has position Infinity in the cache.
+ If a tab is closed, it has no effect on the position of other
+ tabs in the cache since we assume that closing a tab doesn't
+ cause us to load in any other tabs.
+
+ We ignore the effect of dragging tabs between windows.
+ -->
+ <method name="_recordTabAccess">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (!Services.telemetry.canRecordExtended) {
+ return;
+ }
+
+ let tabs = Array.from(this.visibleTabs);
+
+ let pos = aTab.cachePosition;
+ for (let i = 0; i < tabs.length; i++) {
+ // If aTab is moving to the front, everything that was
+ // previously in front of it is bumped up one position.
+ if (tabs[i].cachePosition < pos) {
+ tabs[i].cachePosition++;
+ }
+ }
+ aTab.cachePosition = 0;
+
+ if (isFinite(pos)) {
+ Services.telemetry.getHistogramById("TAB_SWITCH_CACHE_POSITION").add(pos);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_tabAttrModified">
+ <parameter name="aTab"/>
+ <parameter name="aChanged"/>
+ <body><![CDATA[
+ if (aTab.closing)
+ return;
+
+ let event = new CustomEvent("TabAttrModified", {
+ bubbles: true,
+ cancelable: false,
+ detail: {
+ changed: aChanged,
+ }
+ });
+ aTab.dispatchEvent(event);
+ ]]></body>
+ </method>
+
+ <method name="setBrowserSharing">
+ <parameter name="aBrowser"/>
+ <parameter name="aState"/>
+ <body><![CDATA[
+ let tab = this.getTabForBrowser(aBrowser);
+ if (!tab)
+ return;
+
+ let sharing;
+ if (aState.screen) {
+ sharing = "screen";
+ } else if (aState.camera) {
+ sharing = "camera";
+ } else if (aState.microphone) {
+ sharing = "microphone";
+ }
+
+ if (sharing) {
+ tab.setAttribute("sharing", sharing);
+ tab._sharingState = aState;
+ } else {
+ tab.removeAttribute("sharing");
+ tab._sharingState = null;
+ }
+ this._tabAttrModified(tab, ["sharing"]);
+
+ if (aBrowser == this.mCurrentBrowser)
+ gIdentityHandler.updateSharingIndicator();
+ ]]></body>
+ </method>
+
+
+ <method name="setTabTitleLoading">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ aTab.label = this.mStringBundle.getString("tabs.connecting");
+ aTab.crop = "end";
+ this._tabAttrModified(aTab, ["label", "crop"]);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setTabTitle">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var browser = this.getBrowserForTab(aTab);
+ var crop = "end";
+ var title = browser.contentTitle;
+
+ if (!title) {
+ if (browser.currentURI.spec) {
+ try {
+ title = this.mURIFixup.createExposableURI(browser.currentURI).spec;
+ } catch (ex) {
+ title = browser.currentURI.spec;
+ }
+ }
+
+ if (title && !isBlankPageURL(title)) {
+ // At this point, we now have a URI.
+ // Let's try to unescape it using a character set
+ // in case the URI is not ASCII.
+ try {
+ var characterSet = browser.characterSet;
+ const textToSubURI = Components.classes["@mozilla.org/intl/texttosuburi;1"]
+ .getService(Components.interfaces.nsITextToSubURI);
+ title = textToSubURI.unEscapeNonAsciiURI(characterSet, title);
+ } catch (ex) { /* Do nothing. */ }
+
+ crop = "center";
+
+ } else if (aTab.hasAttribute("customizemode")) {
+ let brandBundle = document.getElementById("bundle_brand");
+ let brandShortName = brandBundle.getString("brandShortName");
+ title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle",
+ [ brandShortName ]);
+ } else // Still no title? Fall back to our untitled string.
+ title = this.mStringBundle.getString("tabs.emptyTabTitle");
+ }
+
+ if (aTab.label == title &&
+ aTab.crop == crop)
+ return false;
+
+ aTab.label = title;
+ aTab.crop = crop;
+ this._tabAttrModified(aTab, ["label", "crop"]);
+
+ if (aTab.selected)
+ this.updateTitlebar();
+
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="loadOneTab">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <parameter name="aLoadInBackground"/>
+ <parameter name="aAllowThirdPartyFixup"/>
+ <body>
+ <![CDATA[
+ var aReferrerPolicy;
+ var aFromExternal;
+ var aRelatedToCurrent;
+ var aAllowMixedContent;
+ var aSkipAnimation;
+ var aForceNotRemote;
+ var aNoReferrer;
+ var aUserContextId;
+ var aRelatedBrowser;
+ var aOriginPrincipal;
+ var aOpener;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object" &&
+ !(arguments[1] instanceof Ci.nsIURI)) {
+ let params = arguments[1];
+ aReferrerURI = params.referrerURI;
+ aReferrerPolicy = params.referrerPolicy;
+ aCharset = params.charset;
+ aPostData = params.postData;
+ aLoadInBackground = params.inBackground;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aFromExternal = params.fromExternal;
+ aRelatedToCurrent = params.relatedToCurrent;
+ aAllowMixedContent = params.allowMixedContent;
+ aSkipAnimation = params.skipAnimation;
+ aForceNotRemote = params.forceNotRemote;
+ aNoReferrer = params.noReferrer;
+ aUserContextId = params.userContextId;
+ aRelatedBrowser = params.relatedBrowser;
+ aOriginPrincipal = params.originPrincipal;
+ aOpener = params.opener;
+ }
+
+ var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
+ Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+ var owner = bgLoad ? null : this.selectedTab;
+ var tab = this.addTab(aURI, {
+ referrerURI: aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ charset: aCharset,
+ postData: aPostData,
+ ownerTab: owner,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ fromExternal: aFromExternal,
+ relatedToCurrent: aRelatedToCurrent,
+ skipAnimation: aSkipAnimation,
+ allowMixedContent: aAllowMixedContent,
+ forceNotRemote: aForceNotRemote,
+ noReferrer: aNoReferrer,
+ userContextId: aUserContextId,
+ originPrincipal: aOriginPrincipal,
+ relatedBrowser: aRelatedBrowser,
+ opener: aOpener });
+ if (!bgLoad)
+ this.selectedTab = tab;
+
+ return tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="loadTabs">
+ <parameter name="aURIs"/>
+ <parameter name="aLoadInBackground"/>
+ <parameter name="aReplace"/>
+ <body><![CDATA[
+ let aAllowThirdPartyFixup;
+ let aTargetTab;
+ let aNewIndex = -1;
+ let aPostDatas = [];
+ let aUserContextId;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object") {
+ let params = arguments[1];
+ aLoadInBackground = params.inBackground;
+ aReplace = params.replace;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aTargetTab = params.targetTab;
+ aNewIndex = typeof params.newIndex === "number" ?
+ params.newIndex : aNewIndex;
+ aPostDatas = params.postDatas || aPostDatas;
+ aUserContextId = params.userContextId;
+ }
+
+ if (!aURIs.length)
+ return;
+
+ // The tab selected after this new tab is closed (i.e. the new tab's
+ // "owner") is the next adjacent tab (i.e. not the previously viewed tab)
+ // when several urls are opened here (i.e. closing the first should select
+ // the next of many URLs opened) or if the pref to have UI links opened in
+ // the background is set (i.e. the link is not being opened modally)
+ //
+ // i.e.
+ // Number of URLs Load UI Links in BG Focus Last Viewed?
+ // == 1 false YES
+ // == 1 true NO
+ // > 1 false/true NO
+ var multiple = aURIs.length > 1;
+ var owner = multiple || aLoadInBackground ? null : this.selectedTab;
+ var firstTabAdded = null;
+ var targetTabIndex = -1;
+
+ if (aReplace) {
+ let browser;
+ if (aTargetTab) {
+ browser = this.getBrowserForTab(aTargetTab);
+ targetTabIndex = aTargetTab._tPos;
+ } else {
+ browser = this.mCurrentBrowser;
+ targetTabIndex = this.tabContainer.selectedIndex;
+ }
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
+ Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ try {
+ browser.loadURIWithFlags(aURIs[0], {
+ flags, postData: aPostDatas[0]
+ });
+ } catch (e) {
+ // Ignore failure in case a URI is wrong, so we can continue
+ // opening the next ones.
+ }
+ } else {
+ firstTabAdded = this.addTab(aURIs[0], {
+ ownerTab: owner,
+ skipAnimation: multiple,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostDatas[0],
+ userContextId: aUserContextId
+ });
+ if (aNewIndex !== -1) {
+ this.moveTabTo(firstTabAdded, aNewIndex);
+ targetTabIndex = firstTabAdded._tPos;
+ }
+ }
+
+ let tabNum = targetTabIndex;
+ for (let i = 1; i < aURIs.length; ++i) {
+ let tab = this.addTab(aURIs[i], {
+ skipAnimation: true,
+ allowThirdPartyFixup: aAllowThirdPartyFixup,
+ postData: aPostDatas[i],
+ userContextId: aUserContextId
+ });
+ if (targetTabIndex !== -1)
+ this.moveTabTo(tab, ++tabNum);
+ }
+
+ if (!aLoadInBackground) {
+ if (firstTabAdded) {
+ // .selectedTab setter focuses the content area
+ this.selectedTab = firstTabAdded;
+ }
+ else
+ this.selectedBrowser.focus();
+ }
+ ]]></body>
+ </method>
+
+ <method name="updateBrowserRemoteness">
+ <parameter name="aBrowser"/>
+ <parameter name="aShouldBeRemote"/>
+ <parameter name="aOpener"/>
+ <parameter name="aFreshProcess"/>
+ <body>
+ <![CDATA[
+ let isRemote = aBrowser.getAttribute("remote") == "true";
+
+ // If we are passed an opener, we must be making the browser non-remote, and
+ // if the browser is _currently_ non-remote, we need the openers to match,
+ // because it is already too late to change it.
+ if (aOpener) {
+ if (aShouldBeRemote) {
+ throw new Exception("Cannot set an opener on a browser which should be remote!");
+ }
+ if (!isRemote && aBrowser.contentWindow.opener != aOpener) {
+ throw new Exception("Cannot change opener on an already non-remote browser!");
+ }
+ }
+
+ // Abort if we're not going to change anything
+ if (isRemote == aShouldBeRemote && !aFreshProcess) {
+ return false;
+ }
+
+ let tab = this.getTabForBrowser(aBrowser);
+ let evt = document.createEvent("Events");
+ evt.initEvent("BeforeTabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ let wasActive = document.activeElement == aBrowser;
+
+ // Unmap the old outerWindowID.
+ this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
+
+ // Unhook our progress listener.
+ let filter = this._tabFilters.get(tab);
+ let listener = this._tabListeners.get(tab);
+ aBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+
+ // We'll be creating a new listener, so destroy the old one.
+ listener.destroy();
+
+ let oldUserTypedValue = aBrowser.userTypedValue;
+ let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
+
+ // Make sure the browser is destroyed so it unregisters from observer notifications
+ aBrowser.destroy();
+
+ // Make sure to restore the original droppedLinkHandler and
+ // relatedBrowser.
+ let droppedLinkHandler = aBrowser.droppedLinkHandler;
+ let relatedBrowser = aBrowser.relatedBrowser;
+
+ // Change the "remote" attribute.
+ let parent = aBrowser.parentNode;
+ parent.removeChild(aBrowser);
+ aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
+
+ // NB: This works with the hack in the browser constructor that
+ // turns this normal property into a field.
+ aBrowser.relatedBrowser = relatedBrowser;
+
+ // Set the opener window on the browser, such that when the frame
+ // loader is created the opener is set correctly.
+ aBrowser.presetOpenerWindow(aOpener);
+
+ // Set the freshProcess attribute so that the frameloader knows to
+ // create a new process
+ if (aFreshProcess) {
+ aBrowser.setAttribute("freshProcess", "true");
+ }
+
+ parent.appendChild(aBrowser);
+
+ // Remove the freshProcess attribute if we set it, as we don't
+ // want it to apply for the next time the frameloader is created
+ aBrowser.removeAttribute("freshProcess");
+
+ aBrowser.userTypedValue = oldUserTypedValue;
+ if (hadStartedLoad) {
+ aBrowser.urlbarChangeTracker.startedLoad();
+ }
+
+ aBrowser.droppedLinkHandler = droppedLinkHandler;
+
+ // Switching a browser's remoteness will create a new frameLoader.
+ // As frameLoaders start out with an active docShell we have to
+ // deactivate it if this is not the selected tab's browser or the
+ // browser window is minimized.
+ aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
+
+ // Create a new tab progress listener for the new browser we just injected,
+ // since tab progress listeners have logic for handling the initial about:blank
+ // load
+ listener = this.mTabProgressListener(tab, aBrowser, true, false);
+ this._tabListeners.set(tab, listener);
+ filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the progress listener.
+ aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+
+ // Restore the securityUI state.
+ let securityUI = aBrowser.securityUI;
+ let state = securityUI ? securityUI.state
+ : Ci.nsIWebProgressListener.STATE_IS_INSECURE;
+ // Include the true final argument to indicate that this event is
+ // simulated (instead of being observed by the webProgressListener).
+ this._callProgressListeners(aBrowser, "onSecurityChange",
+ [aBrowser.webProgress, null, state, true],
+ true, false);
+
+ if (aShouldBeRemote) {
+ // Switching the browser to be remote will connect to a new child
+ // process so the browser can no longer be considered to be
+ // crashed.
+ tab.removeAttribute("crashed");
+ } else {
+ aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
+
+ // Register the new outerWindowID.
+ this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
+ }
+
+ if (wasActive)
+ aBrowser.focus();
+
+ // If the findbar has been initialised, reset its browser reference.
+ if (this.isFindBarInitialized(tab)) {
+ this.getFindBar(tab).browser = aBrowser;
+ }
+
+ evt = document.createEvent("Events");
+ evt.initEvent("TabRemotenessChange", true, false);
+ tab.dispatchEvent(evt);
+
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="switchBrowserIntoFreshProcess">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ if (!gMultiProcessBrowser) {
+ return this.updateBrowserRemoteness(aBrowser, false);
+ }
+
+ return this.updateBrowserRemoteness(aBrowser,
+ /* aShouldBeRemote */ true,
+ /* aOpener */ null,
+ /* aFreshProcess */ true);
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateBrowserRemotenessByURL">
+ <parameter name="aBrowser"/>
+ <parameter name="aURL"/>
+ <body>
+ <![CDATA[
+ if (!gMultiProcessBrowser)
+ return this.updateBrowserRemoteness(aBrowser, false);
+
+ let process = aBrowser.isRemoteBrowser ? Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT
+ : Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+ // If this URL can't load in the browser's current process then flip
+ // it to the other process
+ if (!E10SUtils.canLoadURIInProcess(aURL, process))
+ return this.updateBrowserRemoteness(aBrowser, !aBrowser.isRemoteBrowser);
+
+ return false;
+ ]]>
+ </body>
+ </method>
+
+ <field name="_preloadedBrowser">null</field>
+ <method name="_getPreloadedBrowser">
+ <body>
+ <![CDATA[
+ if (!this._isPreloadingEnabled()) {
+ return null;
+ }
+
+ // The preloaded browser might be null.
+ let browser = this._preloadedBrowser;
+
+ // Consume the browser.
+ this._preloadedBrowser = null;
+
+ // Attach the nsIFormFillController now that we know the browser
+ // will be used. If we do that before and the preloaded browser
+ // won't be consumed until shutdown then we leak a docShell.
+ // Also, we do not need to take care of attaching nsIFormFillControllers
+ // in the case that the browser is remote, as remote browsers take
+ // care of that themselves.
+ if (browser && this.hasAttribute("autocompletepopup")) {
+ browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+ }
+
+ return browser;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_isPreloadingEnabled">
+ <body>
+ <![CDATA[
+ // Preloading for the newtab page is enabled when the pref is true
+ // and the URL is "about:newtab". We do not support preloading for
+ // custom newtab URLs.
+ return Services.prefs.getBoolPref("browser.newtab.preload") &&
+ !aboutNewTabService.overridden;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createPreloadBrowser">
+ <body>
+ <![CDATA[
+ // Do nothing if we have a preloaded browser already
+ // or preloading of newtab pages is disabled.
+ if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
+ return;
+ }
+
+ let remote = gMultiProcessBrowser &&
+ E10SUtils.canLoadURIInProcess(BROWSER_NEW_TAB_URL, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
+ let browser = this._createBrowser({isPreloadBrowser: true, remote: remote});
+ this._preloadedBrowser = browser;
+
+ let notificationbox = this.getNotificationBox(browser);
+ this.mPanelContainer.appendChild(notificationbox);
+
+ if (remote) {
+ // For remote browsers, we need to make sure that the webProgress is
+ // instantiated, otherwise the parent won't get informed about the state
+ // of the preloaded browser until it gets attached to a tab.
+ browser.webProgress;
+ }
+
+ browser.loadURI(BROWSER_NEW_TAB_URL);
+ browser.docShellIsActive = false;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_createBrowser">
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ // Supported parameters:
+ // userContextId, remote, isPreloadBrowser, uriIsAboutBlank, permanentKey
+
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+ let b = document.createElementNS(NS_XUL, "browser");
+ b.permanentKey = aParams.permanentKey || {};
+ b.setAttribute("type", "content-targetable");
+ b.setAttribute("message", "true");
+ b.setAttribute("messagemanagergroup", "browsers");
+ b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
+ b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
+
+ if (aParams.userContextId) {
+ b.setAttribute("usercontextid", aParams.userContextId);
+ }
+
+ if (aParams.remote) {
+ b.setAttribute("remote", "true");
+ }
+
+ if (aParams.opener) {
+ if (aParams.remote) {
+ throw new Exception("Cannot set opener window on a remote browser!");
+ }
+ b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.opener);
+ }
+
+ if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) {
+ b.setAttribute("showresizer", "true");
+ }
+
+ if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) {
+ b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
+ }
+
+ if (this.hasAttribute("selectmenulist"))
+ b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
+
+ if (this.hasAttribute("datetimepicker")) {
+ b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
+ }
+
+ b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+
+ if (aParams.relatedBrowser) {
+ b.relatedBrowser = aParams.relatedBrowser;
+ }
+
+ // Create the browserStack container
+ var stack = document.createElementNS(NS_XUL, "stack");
+ stack.className = "browserStack";
+ stack.appendChild(b);
+ stack.setAttribute("flex", "1");
+
+ // Create the browserContainer
+ var browserContainer = document.createElementNS(NS_XUL, "vbox");
+ browserContainer.className = "browserContainer";
+ browserContainer.appendChild(stack);
+ browserContainer.setAttribute("flex", "1");
+
+ // Create the sidebar container
+ var browserSidebarContainer = document.createElementNS(NS_XUL,
+ "hbox");
+ browserSidebarContainer.className = "browserSidebarContainer";
+ browserSidebarContainer.appendChild(browserContainer);
+ browserSidebarContainer.setAttribute("flex", "1");
+
+ // Add the Message and the Browser to the box
+ var notificationbox = document.createElementNS(NS_XUL,
+ "notificationbox");
+ notificationbox.setAttribute("flex", "1");
+ notificationbox.setAttribute("notificationside", "top");
+ notificationbox.appendChild(browserSidebarContainer);
+
+ // Prevent the superfluous initial load of a blank document
+ // if we're going to load something other than about:blank.
+ if (!aParams.uriIsAboutBlank) {
+ b.setAttribute("nodefaultsrc", "true");
+ }
+
+ return b;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_linkBrowserToTab">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ "use strict";
+
+ // Supported parameters:
+ // forceNotRemote, userContextId
+
+ let uriIsAboutBlank = !aURI || aURI == "about:blank";
+
+ // The new browser should be remote if this is an e10s window and
+ // the uri to load can be loaded remotely.
+ let remote = gMultiProcessBrowser &&
+ !aParams.forceNotRemote &&
+ E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
+
+ let browser;
+ let usingPreloadedContent = false;
+
+ // If we open a new tab with the newtab URL in the default
+ // userContext, check if there is a preloaded browser ready.
+ // Private windows are not included because both the label and the
+ // icon for the tab would be set incorrectly (see bug 1195981).
+ if (aURI == BROWSER_NEW_TAB_URL &&
+ !aParams.userContextId &&
+ !PrivateBrowsingUtils.isWindowPrivate(window)) {
+ browser = this._getPreloadedBrowser();
+ if (browser) {
+ usingPreloadedContent = true;
+ aTab.permanentKey = browser.permanentKey;
+ }
+ }
+
+ if (!browser) {
+ // No preloaded browser found, create one.
+ browser = this._createBrowser({permanentKey: aTab.permanentKey,
+ remote: remote,
+ uriIsAboutBlank: uriIsAboutBlank,
+ userContextId: aParams.userContextId,
+ relatedBrowser: aParams.relatedBrowser,
+ opener: aParams.opener});
+ }
+
+ let notificationbox = this.getNotificationBox(browser);
+ let uniqueId = this._generateUniquePanelID();
+ notificationbox.id = uniqueId;
+ aTab.linkedPanel = uniqueId;
+ aTab.linkedBrowser = browser;
+ aTab.hasBrowser = true;
+ this._tabForBrowser.set(browser, aTab);
+
+ // Inject the <browser> into the DOM if necessary.
+ if (!notificationbox.parentNode) {
+ // NB: this appendChild call causes us to run constructors for the
+ // browser element, which fires off a bunch of notifications. Some
+ // of those notifications can cause code to run that inspects our
+ // state, so it is important that the tab element is fully
+ // initialized by this point.
+ this.mPanelContainer.appendChild(notificationbox);
+ }
+
+ // wire up a progress listener for the new browser object.
+ let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
+ const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(Ci.nsIWebProgress);
+ filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
+ browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
+ this._tabListeners.set(aTab, tabListener);
+ this._tabFilters.set(aTab, filter);
+
+ browser.droppedLinkHandler = handleDroppedLink;
+
+ // We start our browsers out as inactive, and then maintain
+ // activeness in the tab switcher.
+ browser.docShellIsActive = false;
+
+ // When addTab() is called with an URL that is not "about:blank" we
+ // set the "nodefaultsrc" attribute that prevents a frameLoader
+ // from being created as soon as the linked <browser> is inserted
+ // into the DOM. We thus have to register the new outerWindowID
+ // for non-remote browsers after we have called browser.loadURI().
+ if (!remote) {
+ this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
+ }
+
+ var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: {} });
+ aTab.dispatchEvent(evt);
+
+ return { usingPreloadedContent: usingPreloadedContent };
+ ]]>
+ </body>
+ </method>
+
+ <method name="addTab">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <parameter name="aOwner"/>
+ <parameter name="aAllowThirdPartyFixup"/>
+ <body>
+ <![CDATA[
+ "use strict";
+
+ const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ var aReferrerPolicy;
+ var aFromExternal;
+ var aRelatedToCurrent;
+ var aSkipAnimation;
+ var aAllowMixedContent;
+ var aForceNotRemote;
+ var aNoReferrer;
+ var aUserContextId;
+ var aEventDetail;
+ var aRelatedBrowser;
+ var aOriginPrincipal;
+ var aOpener;
+ if (arguments.length == 2 &&
+ typeof arguments[1] == "object" &&
+ !(arguments[1] instanceof Ci.nsIURI)) {
+ let params = arguments[1];
+ aReferrerURI = params.referrerURI;
+ aReferrerPolicy = params.referrerPolicy;
+ aCharset = params.charset;
+ aPostData = params.postData;
+ aOwner = params.ownerTab;
+ aAllowThirdPartyFixup = params.allowThirdPartyFixup;
+ aFromExternal = params.fromExternal;
+ aRelatedToCurrent = params.relatedToCurrent;
+ aSkipAnimation = params.skipAnimation;
+ aAllowMixedContent = params.allowMixedContent;
+ aForceNotRemote = params.forceNotRemote;
+ aNoReferrer = params.noReferrer;
+ aUserContextId = params.userContextId;
+ aEventDetail = params.eventDetail;
+ aRelatedBrowser = params.relatedBrowser;
+ aOriginPrincipal = params.originPrincipal;
+ aOpener = params.opener;
+ }
+
+ // if we're adding tabs, we're past interrupt mode, ditch the owner
+ if (this.mCurrentTab.owner)
+ this.mCurrentTab.owner = null;
+
+ var t = document.createElementNS(NS_XUL, "tab");
+
+ var uriIsAboutBlank = !aURI || aURI == "about:blank";
+ let aURIObject = null;
+ try {
+ aURIObject = Services.io.newURI(aURI || "about:blank");
+ } catch (ex) { /* we'll try to fix up this URL later */ }
+
+ if (!aURI || isBlankPageURL(aURI)) {
+ t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle"));
+ } else if (aURI.toLowerCase().startsWith("javascript:")) {
+ // This can go away when bug 672618 or bug 55696 are fixed.
+ t.setAttribute("label", aURI);
+ }
+
+ if (aUserContextId) {
+ t.setAttribute("usercontextid", aUserContextId);
+ ContextualIdentityService.setTabStyle(t);
+ }
+
+ t.setAttribute("crop", "end");
+ t.setAttribute("onerror", "this.removeAttribute('image');");
+ t.className = "tabbrowser-tab";
+
+ this.tabContainer._unlockTabSizing();
+
+ // When overflowing, new tabs are scrolled into view smoothly, which
+ // doesn't go well together with the width transition. So we skip the
+ // transition in that case.
+ let animate = !aSkipAnimation &&
+ this.tabContainer.getAttribute("overflow") != "true" &&
+ Services.prefs.getBoolPref("browser.tabs.animate");
+ if (!animate) {
+ t.setAttribute("fadein", "true");
+ setTimeout(function (tabContainer) {
+ tabContainer._handleNewTab(t);
+ }, 0, this.tabContainer);
+ }
+
+ // invalidate cache
+ this._visibleTabs = null;
+
+ this.tabContainer.appendChild(t);
+
+ // If this new tab is owned by another, assert that relationship
+ if (aOwner)
+ t.owner = aOwner;
+
+ var position = this.tabs.length - 1;
+ t._tPos = position;
+ t.permanentKey = {};
+ this.tabContainer._setPositionalAttributes();
+
+ this.tabContainer.updateVisibility();
+
+ // Currently in this incarnation of bug 906076, we are forcing the
+ // browser to immediately be linked. In future incarnations of this
+ // bug this will be removed so we can leave the tab in its "lazy"
+ // state to be exploited for startup optimization. Note that for
+ // now this must occur before "TabOpen" event is fired, as that will
+ // trigger SessionStore.jsm to run code that expects the existence
+ // of tab.linkedBrowser.
+ let browserParams = {
+ forceNotRemote: aForceNotRemote,
+ userContextId: aUserContextId,
+ relatedBrowser: aRelatedBrowser,
+ opener: aOpener,
+ };
+ let { usingPreloadedContent } = this._linkBrowserToTab(t, aURI, browserParams);
+ let b = t.linkedBrowser;
+
+ // Dispatch a new tab notification. We do this once we're
+ // entirely done, so that things are in a consistent state
+ // even if the event listener opens or closes tabs.
+ var detail = aEventDetail || {};
+ var evt = new CustomEvent("TabOpen", { bubbles: true, detail });
+ t.dispatchEvent(evt);
+
+ if (!usingPreloadedContent && aOriginPrincipal && aURI) {
+ let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler;
+ // Unless we know for sure we're not inheriting principals,
+ // force the about:blank viewer to have the right principal:
+ if (!aURIObject ||
+ (Services.io.getProtocolFlags(aURIObject.scheme) & URI_INHERITS_SECURITY_CONTEXT)) {
+ b.createAboutBlankContentViewer(aOriginPrincipal);
+ }
+ }
+
+ // If we didn't swap docShells with a preloaded browser
+ // then let's just continue loading the page normally.
+ if (!usingPreloadedContent && !uriIsAboutBlank) {
+ // pretend the user typed this so it'll be available till
+ // the document successfully loads
+ if (aURI && gInitialPages.indexOf(aURI) == -1)
+ b.userTypedValue = aURI;
+
+ let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ if (aAllowThirdPartyFixup) {
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
+ }
+ if (aFromExternal)
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
+ if (aAllowMixedContent)
+ flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
+ try {
+ b.loadURIWithFlags(aURI, {
+ flags,
+ referrerURI: aNoReferrer ? null: aReferrerURI,
+ referrerPolicy: aReferrerPolicy,
+ charset: aCharset,
+ postData: aPostData,
+ });
+ } catch (ex) {
+ Cu.reportError(ex);
+ }
+ }
+
+ // Check if we're opening a tab related to the current tab and
+ // move it to after the current tab.
+ // aReferrerURI is null or undefined if the tab is opened from
+ // an external application or bookmark, i.e. somewhere other
+ // than the current tab.
+ if ((aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent) &&
+ Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
+ let newTabPos = (this._lastRelatedTab ||
+ this.selectedTab)._tPos + 1;
+ if (this._lastRelatedTab)
+ this._lastRelatedTab.owner = null;
+ else
+ t.owner = this.selectedTab;
+ this.moveTabTo(t, newTabPos);
+ this._lastRelatedTab = t;
+ }
+
+ if (animate) {
+ requestAnimationFrame(function () {
+ this.tabContainer._handleTabTelemetryStart(t, aURI);
+
+ // kick the animation off
+ t.setAttribute("fadein", "true");
+ }.bind(this));
+ }
+
+ return t;
+ ]]>
+ </body>
+ </method>
+
+ <method name="warnAboutClosingTabs">
+ <parameter name="aCloseTabs"/>
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var tabsToClose;
+ switch (aCloseTabs) {
+ case this.closingTabsEnum.ALL:
+ tabsToClose = this.tabs.length - this._removingTabs.length -
+ gBrowser._numPinnedTabs;
+ break;
+ case this.closingTabsEnum.OTHER:
+ tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs;
+ break;
+ case this.closingTabsEnum.TO_END:
+ if (!aTab)
+ throw new Error("Required argument missing: aTab");
+
+ tabsToClose = this.getTabsToTheEndFrom(aTab).length;
+ break;
+ default:
+ throw new Error("Invalid argument: " + aCloseTabs);
+ }
+
+ if (tabsToClose <= 1)
+ return true;
+
+ const pref = aCloseTabs == this.closingTabsEnum.ALL ?
+ "browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs";
+ var shouldPrompt = Services.prefs.getBoolPref(pref);
+ if (!shouldPrompt)
+ return true;
+
+ var ps = Services.prompt;
+
+ // default to true: if it were false, we wouldn't get this far
+ var warnOnClose = { value: true };
+ var bundle = this.mStringBundle;
+
+ // focus the window before prompting.
+ // this will raise any minimized window, which will
+ // make it obvious which window the prompt is for and will
+ // solve the problem of windows "obscuring" the prompt.
+ // see bug #350299 for more details
+ window.focus();
+ var warningMessage =
+ PluralForm.get(tabsToClose, bundle.getString("tabs.closeWarningMultiple"))
+ .replace("#1", tabsToClose);
+ var buttonPressed =
+ ps.confirmEx(window,
+ bundle.getString("tabs.closeWarningTitle"),
+ warningMessage,
+ (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0)
+ + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1),
+ bundle.getString("tabs.closeButtonMultiple"),
+ null, null,
+ aCloseTabs == this.closingTabsEnum.ALL ?
+ bundle.getString("tabs.closeWarningPromptMe") : null,
+ warnOnClose);
+ var reallyClose = (buttonPressed == 0);
+
+ // don't set the pref unless they press OK and it's false
+ if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
+ Services.prefs.setBoolPref(pref, false);
+
+ return reallyClose;
+ ]]>
+ </body>
+ </method>
+
+ <method name="getTabsToTheEndFrom">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ var tabsToEnd = [];
+ let tabs = this.visibleTabs;
+ for (let i = tabs.length - 1; tabs[i] != aTab && i >= 0; --i) {
+ tabsToEnd.push(tabs[i]);
+ }
+ return tabsToEnd.reverse();
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeTabsToTheEndFrom">
+ <parameter name="aTab"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ if (this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab)) {
+ let tabs = this.getTabsToTheEndFrom(aTab);
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ this.removeTab(tabs[i], aParams);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeAllTabsBut">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (aTab.pinned)
+ return;
+
+ if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
+ let tabs = this.visibleTabs;
+ this.selectedTab = aTab;
+
+ for (let i = tabs.length - 1; i >= 0; --i) {
+ if (tabs[i] != aTab && !tabs[i].pinned)
+ this.removeTab(tabs[i], {animate: true});
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeCurrentTab">
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ this.removeTab(this.mCurrentTab, aParams);
+ ]]>
+ </body>
+ </method>
+
+ <field name="_removingTabs">
+ []
+ </field>
+
+ <method name="removeTab">
+ <parameter name="aTab"/>
+ <parameter name="aParams"/>
+ <body>
+ <![CDATA[
+ if (aParams) {
+ var animate = aParams.animate;
+ var byMouse = aParams.byMouse;
+ var skipPermitUnload = aParams.skipPermitUnload;
+ }
+
+ // Handle requests for synchronously removing an already
+ // asynchronously closing tab.
+ if (!animate &&
+ aTab.closing) {
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
+
+ if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload))
+ return;
+
+ if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
+ this.tabContainer._lockTabSizing(aTab);
+ else
+ this.tabContainer._unlockTabSizing();
+
+ if (!animate /* the caller didn't opt in */ ||
+ isLastTab ||
+ aTab.pinned ||
+ aTab.hidden ||
+ this._removingTabs.length > 3 /* don't want lots of concurrent animations */ ||
+ aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ ||
+ window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ ||
+ !Services.prefs.getBoolPref("browser.tabs.animate")) {
+ this._endRemoveTab(aTab);
+ return;
+ }
+
+ this.tabContainer._handleTabTelemetryStart(aTab);
+
+ this._blurTab(aTab);
+ aTab.style.maxWidth = ""; // ensure that fade-out transition happens
+ aTab.removeAttribute("fadein");
+
+ setTimeout(function (tab, tabbrowser) {
+ if (tab.parentNode &&
+ window.getComputedStyle(tab).maxWidth == "0.1px") {
+ NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
+ tabbrowser._endRemoveTab(tab);
+ }
+ }, 3000, aTab, this);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Tab close requests are ignored if the window is closing anyway,
+ e.g. when holding Ctrl+W. -->
+ <field name="_windowIsClosing">
+ false
+ </field>
+
+ <method name="_beginRemoveTab">
+ <parameter name="aTab"/>
+ <parameter name="aAdoptedByTab"/>
+ <parameter name="aCloseWindowWithLastTab"/>
+ <parameter name="aCloseWindowFastpath"/>
+ <parameter name="aSkipPermitUnload"/>
+ <body>
+ <![CDATA[
+ if (aTab.closing ||
+ this._windowIsClosing)
+ return false;
+
+ var browser = this.getBrowserForTab(aTab);
+
+ if (!aTab._pendingPermitUnload && !aAdoptedByTab && !aSkipPermitUnload) {
+ // We need to block while calling permitUnload() because it
+ // processes the event queue and may lead to another removeTab()
+ // call before permitUnload() returns.
+ aTab._pendingPermitUnload = true;
+ let {permitUnload, timedOut} = browser.permitUnload();
+ delete aTab._pendingPermitUnload;
+ // If we were closed during onbeforeunload, we return false now
+ // so we don't (try to) close the same tab again. Of course, we
+ // also stop if the unload was cancelled by the user:
+ if (aTab.closing || (!timedOut && !permitUnload)) {
+ // NB: deliberately keep the _closedDuringPermitUnload set to
+ // true so we keep exiting early in case of multiple calls.
+ return false;
+ }
+ }
+
+
+ var closeWindow = false;
+ var newTab = false;
+ if (this.tabs.length - this._removingTabs.length == 1) {
+ closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
+ !window.toolbar.visible ||
+ Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
+
+ if (closeWindow) {
+ // We've already called beforeunload on all the relevant tabs if we get here,
+ // so avoid calling it again:
+ window.skipNextCanClose = true;
+ }
+
+ // Closing the tab and replacing it with a blank one is notably slower
+ // than closing the window right away. If the caller opts in, take
+ // the fast path.
+ if (closeWindow &&
+ aCloseWindowFastpath &&
+ this._removingTabs.length == 0) {
+ // This call actually closes the window, unless the user
+ // cancels the operation. We are finished here in both cases.
+ this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
+ return null;
+ }
+
+ newTab = true;
+ }
+
+ aTab.closing = true;
+ this._removingTabs.push(aTab);
+ this._visibleTabs = null; // invalidate cache
+
+ // Invalidate hovered tab state tracking for this closing tab.
+ if (this.tabContainer._hoveredTab == aTab)
+ aTab._mouseleave();
+
+ if (newTab)
+ this.addTab(BROWSER_NEW_TAB_URL, {skipAnimation: true});
+ else
+ this.tabContainer.updateVisibility();
+
+ // We're committed to closing the tab now.
+ // Dispatch a notification.
+ // We dispatch it before any teardown so that event listeners can
+ // inspect the tab that's about to close.
+ var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } });
+ aTab.dispatchEvent(evt);
+
+ if (!aAdoptedByTab && !gMultiProcessBrowser) {
+ // Prevent this tab from showing further dialogs, since we're closing it
+ var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.disableDialogs();
+ }
+
+ // Remove the tab's filter and progress listener.
+ const filter = this._tabFilters.get(aTab);
+
+ browser.webProgress.removeProgressListener(filter);
+
+ const listener = this._tabListeners.get(aTab);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+
+ if (browser.registeredOpenURI && !aAdoptedByTab) {
+ this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
+ browser.getAttribute("usercontextid") || 0);
+ delete browser.registeredOpenURI;
+ }
+
+ // We are no longer the primary content area.
+ browser.setAttribute("type", "content-targetable");
+
+ // Remove this tab as the owner of any other tabs, since it's going away.
+ for (let tab of this.tabs) {
+ if ("owner" in tab && tab.owner == aTab)
+ // |tab| is a child of the tab we're removing, make it an orphan
+ tab.owner = null;
+ }
+
+ aTab._endRemoveArgs = [closeWindow, newTab];
+ return true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_endRemoveTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab || !aTab._endRemoveArgs)
+ return;
+
+ var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
+ aTab._endRemoveArgs = null;
+
+ if (this._windowIsClosing) {
+ aCloseWindow = false;
+ aNewTab = false;
+ }
+
+ this._lastRelatedTab = null;
+
+ // update the UI early for responsiveness
+ aTab.collapsed = true;
+ this.tabContainer._fillTrailingGap();
+ this._blurTab(aTab);
+
+ this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
+
+ if (aCloseWindow) {
+ this._windowIsClosing = true;
+ while (this._removingTabs.length)
+ this._endRemoveTab(this._removingTabs[0]);
+ } else if (!this._windowIsClosing) {
+ if (aNewTab)
+ focusAndSelectUrlBar();
+
+ // workaround for bug 345399
+ this.tabContainer.mTabstrip._updateScrollButtonsDisabledState();
+ }
+
+ // We're going to remove the tab and the browser now.
+ this._tabFilters.delete(aTab);
+ this._tabListeners.delete(aTab);
+
+ var browser = this.getBrowserForTab(aTab);
+ this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
+
+ // Because of the way XBL works (fields just set JS
+ // properties on the element) and the code we have in place
+ // to preserve the JS objects for any elements that have
+ // JS properties set on them, the browser element won't be
+ // destroyed until the document goes away. So we force a
+ // cleanup ourselves.
+ // This has to happen before we remove the child so that the
+ // XBL implementation of nsIObserver still works.
+ browser.destroy();
+
+ var wasPinned = aTab.pinned;
+
+ // Remove the tab ...
+ this.tabContainer.removeChild(aTab);
+
+ // ... 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();
+
+ // update tab close buttons state
+ this.tabContainer.adjustTabstrip();
+
+ setTimeout(function(tabs) {
+ tabs._lastTabClosedByMouse = false;
+ }, 0, this.tabContainer);
+ }
+
+ // update tab positional properties and attributes
+ this.selectedTab._selected = true;
+ this.tabContainer._setPositionalAttributes();
+
+ // Removing the panel requires fixing up selectedPanel immediately
+ // (see below), which would be hindered by the potentially expensive
+ // browser removal. So we remove the browser and the panel in two
+ // steps.
+
+ var panel = this.getNotificationBox(browser);
+
+ // In the multi-process case, it's possible an asynchronous tab switch
+ // is still underway. If so, then it's possible that the last visible
+ // browser is the one we're in the process of removing. There's the
+ // risk of displaying preloaded browsers that are at the end of the
+ // deck if we remove the browser before the switch is complete, so
+ // we alert the switcher in order to show a spinner instead.
+ if (this._switcher) {
+ this._switcher.onTabRemoved(aTab);
+ }
+
+ // This will unload the document. An unload handler could remove
+ // dependant tabs, so it's important that the tabbrowser is now in
+ // a consistent state (tab removed, tab positions updated, etc.).
+ browser.parentNode.removeChild(browser);
+
+ // Release the browser in case something is erroneously holding a
+ // reference to the tab after its removal.
+ this._tabForBrowser.delete(aTab.linkedBrowser);
+ aTab.linkedBrowser = null;
+
+ // As the browser is removed, the removal of a dependent document can
+ // cause the whole window to close. So at this point, it's possible
+ // that the binding is destructed.
+ if (this.mTabBox) {
+ this.mPanelContainer.removeChild(panel);
+ }
+
+ if (aCloseWindow)
+ this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_blurTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab.selected)
+ return;
+
+ if (aTab.owner &&
+ !aTab.owner.hidden &&
+ !aTab.owner.closing &&
+ Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
+ this.selectedTab = aTab.owner;
+ return;
+ }
+
+ // Switch to a visible tab unless there aren't any others remaining
+ let remainingTabs = this.visibleTabs;
+ let numTabs = remainingTabs.length;
+ if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
+ remainingTabs = Array.filter(this.tabs, function(tab) {
+ return !tab.closing;
+ }, this);
+ }
+
+ // Try to find a remaining tab that comes after the given tab
+ var tab = aTab;
+ do {
+ tab = tab.nextSibling;
+ } while (tab && remainingTabs.indexOf(tab) == -1);
+
+ if (!tab) {
+ tab = aTab;
+
+ do {
+ tab = tab.previousSibling;
+ } while (tab && remainingTabs.indexOf(tab) == -1);
+ }
+
+ this.selectedTab = tab;
+ ]]>
+ </body>
+ </method>
+
+ <method name="swapBrowsersAndCloseOther">
+ <parameter name="aOurTab"/>
+ <parameter name="aOtherTab"/>
+ <body>
+ <![CDATA[
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerDocument.defaultView))
+ return;
+
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ let otherBrowser = aOtherTab.linkedBrowser;
+
+ // Can't swap between chrome and content processes.
+ if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
+ return;
+
+ // Keep the userContextId if set on other browser
+ if (otherBrowser.hasAttribute("usercontextid")) {
+ ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
+ }
+
+ // That's gBrowser for the other window, not the tab's browser!
+ var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser;
+ var isPending = aOtherTab.hasAttribute("pending");
+
+ let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
+ let stateFlags = otherTabListener.mStateFlags;
+
+ // Expedite the removal of the icon if it was already scheduled.
+ if (aOtherTab._soundPlayingAttrRemovalTimer) {
+ clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
+ aOtherTab._soundPlayingAttrRemovalTimer = 0;
+ aOtherTab.removeAttribute("soundplaying");
+ remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
+ }
+
+ // First, start teardown of the other browser. Make sure to not
+ // fire the beforeunload event in the process. Close the other
+ // window if this was its last tab.
+ if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true))
+ return;
+
+ let modifiedAttrs = [];
+ if (aOtherTab.hasAttribute("muted")) {
+ aOurTab.setAttribute("muted", "true");
+ aOurTab.muteReason = aOtherTab.muteReason;
+ ourBrowser.mute();
+ modifiedAttrs.push("muted");
+ }
+ if (aOtherTab.hasAttribute("soundplaying")) {
+ aOurTab.setAttribute("soundplaying", "true");
+ modifiedAttrs.push("soundplaying");
+ }
+ if (aOtherTab.hasAttribute("usercontextid")) {
+ aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
+ modifiedAttrs.push("usercontextid");
+ }
+ if (aOtherTab.hasAttribute("sharing")) {
+ aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
+ modifiedAttrs.push("sharing");
+ aOurTab._sharingState = aOtherTab._sharingState;
+ webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
+ }
+
+ // If the other tab is pending (i.e. has not been restored, yet)
+ // then do not switch docShells but retrieve the other tab's state
+ // and apply it to our tab.
+ if (isPending) {
+ SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
+ } else {
+ // Workarounds for bug 458697
+ // Icon might have been set on DOMLinkAdded, don't override that.
+ if (!ourBrowser.mIconURL && otherBrowser.mIconURL)
+ this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal);
+ var isBusy = aOtherTab.hasAttribute("busy");
+ if (isBusy) {
+ aOurTab.setAttribute("busy", "true");
+ modifiedAttrs.push("busy");
+ if (aOurTab.selected)
+ this.mIsBusy = true;
+ }
+
+ this._swapBrowserDocShells(aOurTab, otherBrowser, stateFlags);
+ }
+
+ // Handle findbar data (if any)
+ let otherFindBar = aOtherTab._findBar;
+ if (otherFindBar &&
+ otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
+ let ourFindBar = this.getFindBar(aOurTab);
+ ourFindBar._findField.value = otherFindBar._findField.value;
+ if (!otherFindBar.hidden)
+ ourFindBar.onFindCommand();
+ }
+
+ // Finish tearing down the tab that's going away.
+ remoteBrowser._endRemoveTab(aOtherTab);
+
+ if (isBusy)
+ this.setTabTitleLoading(aOurTab);
+ else
+ this.setTabTitle(aOurTab);
+
+ // If the tab was already selected (this happpens in the scenario
+ // of replaceTabWithWindow), notify onLocationChange, etc.
+ if (aOurTab.selected)
+ this.updateCurrentBrowser(true);
+
+ if (modifiedAttrs.length) {
+ this._tabAttrModified(aOurTab, modifiedAttrs);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="_swapBrowserDocShells">
+ <parameter name="aOurTab"/>
+ <parameter name="aOtherBrowser"/>
+ <parameter name="aStateFlags"/>
+ <body>
+ <![CDATA[
+ // Unhook our progress listener
+ const filter = this._tabFilters.get(aOurTab);
+ let tabListener = this._tabListeners.get(aOurTab);
+ let ourBrowser = this.getBrowserForTab(aOurTab);
+ ourBrowser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(tabListener);
+
+ // Make sure to unregister any open URIs.
+ this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
+
+ // Unmap old outerWindowIDs.
+ this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID);
+ let remoteBrowser = aOtherBrowser.ownerDocument.defaultView.gBrowser;
+ if (remoteBrowser) {
+ remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID);
+ }
+
+ // If switcher is active, it will intercept swap events and
+ // react as needed.
+ if (!this._switcher) {
+ aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser);
+ }
+
+ // Swap the docshells
+ ourBrowser.swapDocShells(aOtherBrowser);
+
+ if (ourBrowser.isRemoteBrowser) {
+ // Switch outerWindowIDs for remote browsers.
+ let ourOuterWindowID = ourBrowser._outerWindowID;
+ ourBrowser._outerWindowID = aOtherBrowser._outerWindowID;
+ aOtherBrowser._outerWindowID = ourOuterWindowID;
+ }
+
+ // Register new outerWindowIDs.
+ this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser);
+ if (remoteBrowser) {
+ remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser);
+ }
+
+ // Swap permanentKey properties.
+ let ourPermanentKey = ourBrowser.permanentKey;
+ ourBrowser.permanentKey = aOtherBrowser.permanentKey;
+ aOtherBrowser.permanentKey = ourPermanentKey;
+ aOurTab.permanentKey = ourBrowser.permanentKey;
+ if (remoteBrowser) {
+ let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
+ if (otherTab) {
+ otherTab.permanentKey = aOtherBrowser.permanentKey;
+ }
+ }
+
+ // Restore the progress listener
+ tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false,
+ aStateFlags);
+ this._tabListeners.set(aOurTab, tabListener);
+
+ const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
+ filter.addProgressListener(tabListener, notifyAll);
+ ourBrowser.webProgress.addProgressListener(filter, notifyAll);
+ ]]>
+ </body>
+ </method>
+
+ <method name="_swapRegisteredOpenURIs">
+ <parameter name="aOurBrowser"/>
+ <parameter name="aOtherBrowser"/>
+ <body>
+ <![CDATA[
+ // If the current URI is registered as open remove it from the list.
+ if (aOurBrowser.registeredOpenURI) {
+ this._unifiedComplete.unregisterOpenPage(aOurBrowser.registeredOpenURI,
+ aOurBrowser.getAttribute("usercontextid") || 0);
+ delete aOurBrowser.registeredOpenURI;
+ }
+
+ // If the other/new URI is registered as open then copy it over.
+ if (aOtherBrowser.registeredOpenURI) {
+ aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
+ delete aOtherBrowser.registeredOpenURI;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadAllTabs">
+ <body>
+ <![CDATA[
+ let tabs = this.visibleTabs;
+ let l = tabs.length;
+ for (var i = 0; i < l; i++) {
+ try {
+ this.getBrowserForTab(tabs[i]).reload();
+ } catch (e) {
+ // ignore failure to reload so others will be reloaded
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ this.getBrowserForTab(aTab).reload();
+ ]]>
+ </body>
+ </method>
+
+ <method name="addProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ if (arguments.length != 1) {
+ Components.utils.reportError("gBrowser.addProgressListener was " +
+ "called with a second argument, " +
+ "which is not supported. See bug " +
+ "608628. Call stack: " + new Error().stack);
+ }
+
+ this.mProgressListeners.push(aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.mProgressListeners =
+ this.mProgressListeners.filter(l => l != aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="addTabsProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ this.mTabsProgressListeners.push(aListener);
+ </body>
+ </method>
+
+ <method name="removeTabsProgressListener">
+ <parameter name="aListener"/>
+ <body>
+ <![CDATA[
+ this.mTabsProgressListeners =
+ this.mTabsProgressListeners.filter(l => l != aListener);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getBrowserForTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ return aTab.linkedBrowser;
+ ]]>
+ </body>
+ </method>
+
+ <method name="showOnlyTheseTabs">
+ <parameter name="aTabs"/>
+ <body>
+ <![CDATA[
+ for (let tab of this.tabs) {
+ if (aTabs.indexOf(tab) == -1)
+ this.hideTab(tab);
+ else
+ this.showTab(tab);
+ }
+
+ this.tabContainer._handleTabSelect(false);
+ ]]>
+ </body>
+ </method>
+
+ <method name="showTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (aTab.hidden) {
+ aTab.removeAttribute("hidden");
+ this._visibleTabs = null; // invalidate cache
+
+ this.tabContainer.adjustTabstrip();
+
+ this.tabContainer._setPositionalAttributes();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabShow", true, false);
+ aTab.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="hideTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
+ !aTab.closing) {
+ aTab.setAttribute("hidden", "true");
+ this._visibleTabs = null; // invalidate cache
+
+ this.tabContainer.adjustTabstrip();
+
+ this.tabContainer._setPositionalAttributes();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabHide", true, false);
+ aTab.dispatchEvent(event);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="selectTabAtIndex">
+ <parameter name="aIndex"/>
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ let tabs = this.visibleTabs;
+
+ // count backwards for aIndex < 0
+ if (aIndex < 0) {
+ aIndex += tabs.length;
+ // clamp at index 0 if still negative.
+ if (aIndex < 0)
+ aIndex = 0;
+ } else if (aIndex >= tabs.length) {
+ // clamp at right-most tab if out of range.
+ aIndex = tabs.length - 1;
+ }
+
+ this.selectedTab = tabs[aIndex];
+
+ if (aEvent) {
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ }
+ ]]>
+ </body>
+ </method>
+
+ <property name="selectedTab">
+ <getter>
+ return this.mCurrentTab;
+ </getter>
+ <setter>
+ <![CDATA[
+ if (gNavToolbox.collapsed) {
+ return this.mTabBox.selectedTab;
+ }
+ // Update the tab
+ this.mTabBox.selectedTab = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="selectedBrowser"
+ onget="return this.mCurrentBrowser;"
+ readonly="true"/>
+
+ <field name="browsers" readonly="true">
+ <![CDATA[
+ // This defines a proxy which allows us to access browsers by
+ // index without actually creating a full array of browsers.
+ new Proxy([], {
+ has: (target, name) => {
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ return (name in this.tabs);
+ }
+ return false;
+ },
+ get: (target, name) => {
+ if (name == "length") {
+ return this.tabs.length;
+ }
+ if (typeof name == "string" && Number.isInteger(parseInt(name))) {
+ if (!(name in this.tabs)) {
+ return undefined;
+ }
+ return this.tabs[name].linkedBrowser;
+ }
+ return target[name];
+ }
+ });
+ ]]>
+ </field>
+
+ <!-- Moves a tab to a new browser window, unless it's already the only tab
+ in the current window, in which case this will do nothing. -->
+ <method name="replaceTabWithWindow">
+ <parameter name="aTab"/>
+ <parameter name="aOptions"/>
+ <body>
+ <![CDATA[
+ if (this.tabs.length == 1)
+ return null;
+
+ var options = "chrome,dialog=no,all";
+ for (var name in aOptions)
+ options += "," + name + "=" + aOptions[name];
+
+ // tell a new window to take the "dropped" tab
+ return window.openDialog(getBrowserURL(), "_blank", options, aTab);
+ ]]>
+ </body>
+ </method>
+
+ <!-- Opens a given tab to a non-remote window. -->
+ <method name="openNonRemoteWindow">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!this.AppConstants.E10S_TESTING_ONLY) {
+ throw "This method is intended only for e10s testing!";
+ }
+ let url = aTab.linkedBrowser.currentURI.spec;
+ return window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no,non-remote", url);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabTo">
+ <parameter name="aTab"/>
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ var oldPosition = aTab._tPos;
+ if (oldPosition == aIndex)
+ return;
+
+ // Don't allow mixing pinned and unpinned tabs.
+ if (aTab.pinned)
+ aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
+ else
+ aIndex = Math.max(aIndex, this._numPinnedTabs);
+ if (oldPosition == aIndex)
+ return;
+
+ this._lastRelatedTab = null;
+
+ let wasFocused = (document.activeElement == this.mCurrentTab);
+
+ aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
+
+ // invalidate cache
+ this._visibleTabs = null;
+
+ // use .item() instead of [] because dragging to the end of the strip goes out of
+ // bounds: .item() returns null (so it acts like appendChild), but [] throws
+ this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex));
+
+ for (let i = 0; i < this.tabs.length; i++) {
+ this.tabs[i]._tPos = i;
+ this.tabs[i]._selected = false;
+ }
+
+ // If we're in the midst of an async tab switch while calling
+ // moveTabTo, we can get into a case where _visuallySelected
+ // is set to true on two different tabs.
+ //
+ // What we want to do in moveTabTo is to remove logical selection
+ // from all tabs, and then re-add logical selection to mCurrentTab
+ // (and visual selection as well if we're not running with e10s, which
+ // setting _selected will do automatically).
+ //
+ // If we're running with e10s, then the visual selection will not
+ // be changed, which is fine, since if we weren't in the midst of a
+ // tab switch, the previously visually selected tab should still be
+ // correct, and if we are in the midst of a tab switch, then the async
+ // tab switcher will set the visually selected tab once the tab switch
+ // has completed.
+ this.mCurrentTab._selected = true;
+
+ if (wasFocused)
+ this.mCurrentTab.focus();
+
+ this.tabContainer._handleTabSelect(false);
+
+ if (aTab.pinned)
+ this.tabContainer._positionPinnedTabs();
+
+ this.tabContainer._setPositionalAttributes();
+
+ var evt = document.createEvent("UIEvents");
+ evt.initUIEvent("TabMove", true, false, window, oldPosition);
+ aTab.dispatchEvent(evt);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabForward">
+ <body>
+ <![CDATA[
+ let nextTab = this.mCurrentTab.nextSibling;
+ while (nextTab && nextTab.hidden)
+ nextTab = nextTab.nextSibling;
+
+ if (nextTab)
+ this.moveTabTo(this.mCurrentTab, nextTab._tPos);
+ else if (this.arrowKeysShouldWrap)
+ this.moveTabToStart();
+ ]]>
+ </body>
+ </method>
+
+ <!-- Adopts a tab from another browser window, and inserts it at aIndex -->
+ <method name="adoptTab">
+ <parameter name="aTab"/>
+ <parameter name="aIndex"/>
+ <parameter name="aSelectTab"/>
+ <body>
+ <![CDATA[
+ // Swap the dropped tab with a new one we create and then close
+ // it in the other window (making it seem to have moved between
+ // windows).
+ let params = { eventDetail: { adoptedTab: aTab } };
+ if (aTab.hasAttribute("usercontextid")) {
+ // new tab must have the same usercontextid as the old one
+ params.userContextId = aTab.getAttribute("usercontextid");
+ }
+ let newTab = this.addTab("about:blank", params);
+ let newBrowser = this.getBrowserForTab(newTab);
+ let newURL = aTab.linkedBrowser.currentURI.spec;
+
+ // If we're an e10s browser window, an exception will be thrown
+ // if we attempt to drag a non-remote browser in, so we need to
+ // ensure that the remoteness of the newly created browser is
+ // appropriate for the URL of the tab being dragged in.
+ this.updateBrowserRemotenessByURL(newBrowser, newURL);
+
+ // Stop the about:blank load.
+ newBrowser.stop();
+ // Make sure it has a docshell.
+ newBrowser.docShell;
+
+ let numPinned = this._numPinnedTabs;
+ if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
+ this.pinTab(newTab);
+ }
+
+ this.moveTabTo(newTab, aIndex);
+
+ // We need to select the tab before calling swapBrowsersAndCloseOther
+ // so that window.content in chrome windows points to the right tab
+ // when pagehide/show events are fired. This is no longer necessary
+ // for any exiting browser code, but it may be necessary for add-on
+ // compatibility.
+ if (aSelectTab) {
+ this.selectedTab = newTab;
+ }
+
+ aTab.parentNode._finishAnimateTabMove();
+ this.swapBrowsersAndCloseOther(newTab, aTab);
+
+ if (aSelectTab) {
+ // Call updateCurrentBrowser to make sure the URL bar is up to date
+ // for our new tab after we've done swapBrowsersAndCloseOther.
+ this.updateCurrentBrowser(true);
+ }
+
+ return newTab;
+ ]]>
+ </body>
+ </method>
+
+
+ <method name="moveTabBackward">
+ <body>
+ <![CDATA[
+ let previousTab = this.mCurrentTab.previousSibling;
+ while (previousTab && previousTab.hidden)
+ previousTab = previousTab.previousSibling;
+
+ if (previousTab)
+ this.moveTabTo(this.mCurrentTab, previousTab._tPos);
+ else if (this.arrowKeysShouldWrap)
+ this.moveTabToEnd();
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabToStart">
+ <body>
+ <![CDATA[
+ var tabPos = this.mCurrentTab._tPos;
+ if (tabPos > 0)
+ this.moveTabTo(this.mCurrentTab, 0);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabToEnd">
+ <body>
+ <![CDATA[
+ var tabPos = this.mCurrentTab._tPos;
+ if (tabPos < this.browsers.length - 1)
+ this.moveTabTo(this.mCurrentTab, this.browsers.length - 1);
+ ]]>
+ </body>
+ </method>
+
+ <method name="moveTabOver">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ var direction = window.getComputedStyle(this.parentNode, null).direction;
+ if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
+ (direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
+ this.moveTabForward();
+ else
+ this.moveTabBackward();
+ ]]>
+ </body>
+ </method>
+
+ <method name="duplicateTab">
+ <parameter name="aTab"/><!-- can be from a different window as well -->
+ <parameter name="aRestoreTabImmediately"/><!-- can defer loading of the tab contents -->
+ <body>
+ <![CDATA[
+ return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ List of browsers whose docshells must be active in order for print preview
+ to work.
+ -->
+ <field name="_printPreviewBrowsers">
+ new Set()
+ </field>
+
+ <method name="activateBrowserForPrintPreview">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ this._printPreviewBrowsers.add(aBrowser);
+ if (this._switcher) {
+ this._switcher.activateBrowserForPrintPreview(aBrowser);
+ }
+ aBrowser.docShellIsActive = true;
+ ]]>
+ </body>
+ </method>
+
+ <method name="deactivatePrintPreviewBrowsers">
+ <body>
+ <![CDATA[
+ let browsers = this._printPreviewBrowsers;
+ this._printPreviewBrowsers = new Set();
+ for (let browser of browsers) {
+ browser.docShellIsActive = this.shouldActivateDocShell(browser);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ Returns true if a given browser's docshell should be active.
+ -->
+ <method name="shouldActivateDocShell">
+ <parameter name="aBrowser"/>
+ <body>
+ <![CDATA[
+ if (this._switcher) {
+ return this._switcher.shouldActivateDocShell(aBrowser);
+ }
+ return (aBrowser == this.selectedBrowser &&
+ window.windowState != window.STATE_MINIMIZED) ||
+ this._printPreviewBrowsers.has(aBrowser);
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ The tab switcher is responsible for asynchronously switching
+ tabs in e10s. It waits until the new tab is ready (i.e., the
+ layer tree is available) before switching to it. Then it
+ unloads the layer tree for the old tab.
+
+ The tab switcher is a state machine. For each tab, it
+ maintains state about whether the layer tree for the tab is
+ available, being loaded, being unloaded, or unavailable. It
+ also keeps track of the tab currently being displayed, the tab
+ it's trying to load, and the tab the user has asked to switch
+ to. The switcher object is created upon tab switch. It is
+ released when there are no pending tabs to load or unload.
+
+ The following general principles have guided the design:
+
+ 1. We only request one layer tree at a time. If the user
+ switches to a different tab while waiting, we don't request
+ the new layer tree until the old tab has loaded or timed out.
+
+ 2. If loading the layers for a tab times out, we show the
+ spinner and possibly request the layer tree for another tab if
+ the user has requested one.
+
+ 3. We discard layer trees on a delay. This way, if the user is
+ switching among the same tabs frequently, we don't continually
+ load the same tabs.
+
+ It's important that we always show either the spinner or a tab
+ whose layers are available. Otherwise the compositor will draw
+ an entirely black frame, which is very jarring. To ensure this
+ never happens when switching away from a tab, we assume the
+ old tab might still be drawn until a MozAfterPaint event
+ occurs. Because layout and compositing happen asynchronously,
+ we don't have any other way of knowing when the switch
+ actually takes place. Therefore, we don't unload the old tab
+ until the next MozAfterPaint event.
+ -->
+ <field name="_switcher">null</field>
+ <method name="_getSwitcher">
+ <body><![CDATA[
+ if (this._switcher) {
+ return this._switcher;
+ }
+
+ let switcher = {
+ // How long to wait for a tab's layers to load. After this
+ // time elapses, we're free to put up the spinner and start
+ // trying to load a different tab.
+ TAB_SWITCH_TIMEOUT: 400 /* ms */,
+
+ // When the user hasn't switched tabs for this long, we unload
+ // layers for all tabs that aren't in use.
+ UNLOAD_DELAY: 300 /* ms */,
+
+ // The next three tabs form the principal state variables.
+ // See the assertions in postActions for their invariants.
+
+ // Tab the user requested most recently.
+ requestedTab: this.selectedTab,
+
+ // Tab we're currently trying to load.
+ loadingTab: null,
+
+ // We show this tab in case the requestedTab hasn't loaded yet.
+ lastVisibleTab: this.selectedTab,
+
+ // Auxilliary state variables:
+
+ visibleTab: this.selectedTab, // Tab that's on screen.
+ spinnerTab: null, // Tab showing a spinner.
+ originalTab: this.selectedTab, // Tab that we started on.
+
+ tabbrowser: this, // Reference to gBrowser.
+ loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance.
+ unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
+
+ // Map from tabs to STATE_* (below).
+ tabState: new Map(),
+
+ // True if we're in the midst of switching tabs.
+ switchInProgress: false,
+
+ // Keep an exact list of content processes (tabParent) in which
+ // we're actively suppressing the display port. This gives a robust
+ // way to make sure we don't forget to un-suppress.
+ activeSuppressDisplayport: new Set(),
+
+ // Set of tabs that might be visible right now. We maintain
+ // this set because we can't be sure when a tab is actually
+ // drawn. A tab is added to this set when we ask to make it
+ // visible. All tabs but the most recently shown tab are
+ // removed from the set upon MozAfterPaint.
+ maybeVisibleTabs: new Set([this.selectedTab]),
+
+ STATE_UNLOADED: 0,
+ STATE_LOADING: 1,
+ STATE_LOADED: 2,
+ STATE_UNLOADING: 3,
+
+ // re-entrancy guard:
+ _processing: false,
+
+ // Wraps nsITimer. Must not use the vanilla setTimeout and
+ // clearTimeout, because they will be blocked by nsIPromptService
+ // dialogs.
+ setTimer: function(callback, timeout) {
+ let event = {
+ notify: callback
+ };
+
+ var timer = Cc["@mozilla.org/timer;1"]
+ .createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ clearTimer: function(timer) {
+ timer.cancel();
+ },
+
+ getTabState: function(tab) {
+ let state = this.tabState.get(tab);
+ if (state === undefined) {
+ return this.STATE_UNLOADED;
+ }
+ return state;
+ },
+
+ setTabStateNoAction(tab, state) {
+ if (state == this.STATE_UNLOADED) {
+ this.tabState.delete(tab);
+ } else {
+ this.tabState.set(tab, state);
+ }
+ },
+
+ setTabState: function(tab, state) {
+ this.setTabStateNoAction(tab, state);
+
+ let browser = tab.linkedBrowser;
+ let {tabParent} = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (state == this.STATE_LOADING) {
+ this.assert(!this.minimized);
+ browser.docShellIsActive = true;
+ if (!tabParent) {
+ this.onLayersReady(browser);
+ }
+ } else if (state == this.STATE_UNLOADING) {
+ browser.docShellIsActive = false;
+ if (!tabParent) {
+ this.onLayersCleared(browser);
+ }
+ }
+ },
+
+ get minimized() {
+ return window.windowState == window.STATE_MINIMIZED;
+ },
+
+ init: function() {
+ this.log("START");
+
+ // If we minimized the window before the switcher was activated,
+ // we might have set the preserveLayers flag for the current
+ // browser. Let's clear it.
+ this.tabbrowser.mCurrentBrowser.preserveLayers(false);
+
+ window.addEventListener("MozAfterPaint", this);
+ window.addEventListener("MozLayerTreeReady", this);
+ window.addEventListener("MozLayerTreeCleared", this);
+ window.addEventListener("TabRemotenessChange", this);
+ window.addEventListener("sizemodechange", this);
+ window.addEventListener("SwapDocShells", this, true);
+ window.addEventListener("EndSwapDocShells", this, true);
+ if (!this.minimized) {
+ this.setTabState(this.requestedTab, this.STATE_LOADED);
+ }
+ },
+
+ destroy: function() {
+ if (this.unloadTimer) {
+ this.clearTimer(this.unloadTimer);
+ this.unloadTimer = null;
+ }
+ if (this.loadTimer) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+
+ window.removeEventListener("MozAfterPaint", this);
+ window.removeEventListener("MozLayerTreeReady", this);
+ window.removeEventListener("MozLayerTreeCleared", this);
+ window.removeEventListener("TabRemotenessChange", this);
+ window.removeEventListener("sizemodechange", this);
+ window.removeEventListener("SwapDocShells", this, true);
+ window.removeEventListener("EndSwapDocShells", this, true);
+
+ this.tabbrowser._switcher = null;
+
+ this.activeSuppressDisplayport.forEach(function(tabParent) {
+ tabParent.suppressDisplayport(false);
+ });
+ this.activeSuppressDisplayport.clear();
+ },
+
+ finish: function() {
+ this.log("FINISH");
+
+ this.assert(this.tabbrowser._switcher);
+ this.assert(this.tabbrowser._switcher === this);
+ this.assert(!this.spinnerTab);
+ this.assert(!this.loadTimer);
+ this.assert(!this.loadingTab);
+ this.assert(this.lastVisibleTab === this.requestedTab);
+ this.assert(this.minimized || this.getTabState(this.requestedTab) == this.STATE_LOADED);
+
+ this.destroy();
+
+ let toBrowser = this.requestedTab.linkedBrowser;
+ toBrowser.setAttribute("type", "content-primary");
+
+ this.tabbrowser._adjustFocusAfterTabSwitch(this.requestedTab);
+
+ let fromBrowser = this.originalTab.linkedBrowser;
+ // It's possible that the tab we're switching from closed
+ // before we were able to finalize, in which case, fromBrowser
+ // doesn't exist.
+ if (fromBrowser) {
+ fromBrowser.setAttribute("type", "content-targetable");
+ }
+
+ let event = new CustomEvent("TabSwitchDone", {
+ bubbles: true,
+ cancelable: true
+ });
+ this.tabbrowser.dispatchEvent(event);
+ },
+
+ // This function is called after all the main state changes to
+ // make sure we display the right tab.
+ updateDisplay: function() {
+ // Figure out which tab we actually want visible right now.
+ let showTab = null;
+ if (this.getTabState(this.requestedTab) != this.STATE_LOADED &&
+ this.lastVisibleTab && this.loadTimer) {
+ // If we can't show the requestedTab, and lastVisibleTab is
+ // available, show it.
+ showTab = this.lastVisibleTab;
+ } else {
+ // Show the requested tab. If it's not available, we'll show the spinner.
+ showTab = this.requestedTab;
+ }
+
+ // Show or hide the spinner as needed.
+ let needSpinner = this.getTabState(showTab) != this.STATE_LOADED && !this.minimized;
+ if (!needSpinner && this.spinnerTab) {
+ this.spinnerHidden();
+ this.tabbrowser.removeAttribute("pendingpaint");
+ this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+ this.spinnerTab = null;
+ } else if (needSpinner && this.spinnerTab !== showTab) {
+ if (this.spinnerTab) {
+ this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
+ } else {
+ this.spinnerDisplayed();
+ }
+ this.spinnerTab = showTab;
+ this.tabbrowser.setAttribute("pendingpaint", "true");
+ this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
+ }
+
+ // Switch to the tab we've decided to make visible.
+ if (this.visibleTab !== showTab) {
+ this.visibleTab = showTab;
+
+ this.maybeVisibleTabs.add(showTab);
+
+ let tabs = this.tabbrowser.mTabBox.tabs;
+ let tabPanel = this.tabbrowser.mPanelContainer;
+ let showPanel = tabs.getRelatedElement(showTab);
+ let index = Array.indexOf(tabPanel.childNodes, showPanel);
+ if (index != -1) {
+ this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
+ tabPanel.setAttribute("selectedIndex", index);
+ if (showTab === this.requestedTab) {
+ this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
+ }
+ }
+
+ // This doesn't necessarily exist if we're a new window and haven't switched tabs yet
+ if (this.lastVisibleTab)
+ this.lastVisibleTab._visuallySelected = false;
+
+ this.visibleTab._visuallySelected = true;
+ }
+
+ this.lastVisibleTab = this.visibleTab;
+ },
+
+ assert: function(cond) {
+ if (!cond) {
+ dump("Assertion failure\n" + Error().stack);
+
+ // Don't break a user's browser if an assertion fails.
+ if (this.tabbrowser.AppConstants.DEBUG) {
+ throw new Error("Assertion failure");
+ }
+ }
+ },
+
+ // We've decided to try to load requestedTab.
+ loadRequestedTab: function() {
+ this.assert(!this.loadTimer);
+ this.assert(!this.minimized);
+
+ // loadingTab can be non-null here if we timed out loading the current tab.
+ // In that case we just overwrite it with a different tab; it's had its chance.
+ this.loadingTab = this.requestedTab;
+ this.log("Loading tab " + this.tinfo(this.loadingTab));
+
+ this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
+ this.setTabState(this.requestedTab, this.STATE_LOADING);
+ },
+
+ // This function runs before every event. It fixes up the state
+ // to account for closed tabs.
+ preActions: function() {
+ this.assert(this.tabbrowser._switcher);
+ this.assert(this.tabbrowser._switcher === this);
+
+ for (let [tab, ] of this.tabState) {
+ if (!tab.linkedBrowser) {
+ this.tabState.delete(tab);
+ }
+ }
+
+ if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
+ this.lastVisibleTab = null;
+ }
+ if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
+ this.spinnerHidden();
+ this.spinnerTab = null;
+ }
+ if (this.loadingTab && !this.loadingTab.linkedBrowser) {
+ this.loadingTab = null;
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ },
+
+ // This code runs after we've responded to an event or requested a new
+ // tab. It's expected that we've already updated all the principal
+ // state variables. This function takes care of updating any auxilliary
+ // state.
+ postActions: function() {
+ // Once we finish loading loadingTab, we null it out. So the state should
+ // always be LOADING.
+ this.assert(!this.loadingTab ||
+ this.getTabState(this.loadingTab) == this.STATE_LOADING);
+
+ // We guarantee that loadingTab is non-null iff loadTimer is non-null. So
+ // the timer is set only when we're loading something.
+ this.assert(!this.loadTimer || this.loadingTab);
+ this.assert(!this.loadingTab || this.loadTimer);
+
+ // If we're not loading anything, try loading the requested tab.
+ let requestedState = this.getTabState(this.requestedTab);
+ if (!this.loadTimer && !this.minimized &&
+ (requestedState == this.STATE_UNLOADED ||
+ requestedState == this.STATE_UNLOADING)) {
+ this.loadRequestedTab();
+ }
+
+ // See how many tabs still have work to do.
+ let numPending = 0;
+ for (let [tab, state] of this.tabState) {
+ // Skip print preview browsers since they shouldn't affect tab switching.
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADED && tab !== this.requestedTab) {
+ numPending++;
+ }
+ if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
+ numPending++;
+ }
+ }
+
+ this.updateDisplay();
+
+ // It's possible for updateDisplay to trigger one of our own event
+ // handlers, which might cause finish() to already have been called.
+ // Check for that before calling finish() again.
+ if (!this.tabbrowser._switcher) {
+ return;
+ }
+
+ if (numPending == 0) {
+ this.finish();
+ }
+
+ this.logState("done");
+ },
+
+ // Fires when we're ready to unload unused tabs.
+ onUnloadTimeout: function() {
+ this.logState("onUnloadTimeout");
+ this.unloadTimer = null;
+ this.preActions();
+
+ let numPending = 0;
+
+ // Unload any tabs that can be unloaded.
+ for (let [tab, state] of this.tabState) {
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADED &&
+ !this.maybeVisibleTabs.has(tab) &&
+ tab !== this.lastVisibleTab &&
+ tab !== this.loadingTab &&
+ tab !== this.requestedTab)
+ {
+ this.setTabState(tab, this.STATE_UNLOADING);
+ }
+
+ if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
+ numPending++;
+ }
+ }
+
+ if (numPending) {
+ // Keep the timer going since there may be more tabs to unload.
+ this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+ }
+
+ this.postActions();
+ },
+
+ // Fires when an ongoing load has taken too long.
+ onLoadTimeout: function() {
+ this.logState("onLoadTimeout");
+ this.preActions();
+ this.loadTimer = null;
+ this.loadingTab = null;
+ this.postActions();
+ },
+
+ // Fires when the layers become available for a tab.
+ onLayersReady: function(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ this.logState(`onLayersReady(${tab._tPos})`);
+
+ this.assert(this.getTabState(tab) == this.STATE_LOADING ||
+ this.getTabState(tab) == this.STATE_LOADED);
+ this.setTabState(tab, this.STATE_LOADED);
+
+ this.maybeFinishTabSwitch();
+
+ if (this.loadingTab === tab) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ this.loadingTab = null;
+ }
+ },
+
+ // Fires when we paint the screen. Any tab switches we initiated
+ // previously are done, so there's no need to keep the old layers
+ // around.
+ onPaint: function() {
+ this.maybeVisibleTabs.clear();
+ this.maybeFinishTabSwitch();
+ },
+
+ // Called when we're done clearing the layers for a tab.
+ onLayersCleared: function(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ if (tab) {
+ this.logState(`onLayersCleared(${tab._tPos})`);
+ this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
+ this.getTabState(tab) == this.STATE_UNLOADED);
+ this.setTabState(tab, this.STATE_UNLOADED);
+ }
+ },
+
+ // Called when a tab switches from remote to non-remote. In this case
+ // a MozLayerTreeReady notification that we requested may never fire,
+ // so we need to simulate it.
+ onRemotenessChange: function(tab) {
+ this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
+ if (!tab.linkedBrowser.isRemoteBrowser) {
+ if (this.getTabState(tab) == this.STATE_LOADING) {
+ this.onLayersReady(tab.linkedBrowser);
+ } else if (this.getTabState(tab) == this.STATE_UNLOADING) {
+ this.onLayersCleared(tab.linkedBrowser);
+ }
+ }
+ },
+
+ // Called when a tab has been removed, and the browser node is
+ // about to be removed from the DOM.
+ onTabRemoved: function(tab) {
+ if (this.lastVisibleTab == tab) {
+ // The browser that was being presented to the user is
+ // going to be removed during this tick of the event loop.
+ // This will cause us to show a tab spinner instead.
+ this.preActions();
+ this.lastVisibleTab = null;
+ this.postActions();
+ }
+ },
+
+ onSizeModeChange() {
+ if (this.minimized) {
+ for (let [tab, state] of this.tabState) {
+ // Skip print preview browsers since they shouldn't affect tab switching.
+ if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
+ continue;
+ }
+
+ if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
+ this.setTabState(tab, this.STATE_UNLOADING);
+ }
+ }
+ if (this.loadTimer) {
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ this.loadingTab = null;
+ } else {
+ // Do nothing. We'll automatically start loading the requested tab in
+ // postActions.
+ }
+ },
+
+ onSwapDocShells(ourBrowser, otherBrowser) {
+ // This event fires before the swap. ourBrowser is from
+ // our window. We save the state of otherBrowser since ourBrowser
+ // needs to take on that state at the end of the swap.
+
+ let otherTabbrowser = otherBrowser.ownerDocument.defaultView.gBrowser;
+ let otherState;
+ if (otherTabbrowser && otherTabbrowser._switcher) {
+ let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
+ otherState = otherTabbrowser._switcher.getTabState(otherTab);
+ } else {
+ otherState = (otherBrowser.docShellIsActive
+ ? this.STATE_LOADED
+ : this.STATE_UNLOADED);
+ }
+
+ if (!this.swapMap) {
+ this.swapMap = new WeakMap();
+ }
+ this.swapMap.set(otherBrowser, otherState);
+ },
+
+ onEndSwapDocShells(ourBrowser, otherBrowser) {
+ // The swap has happened. We reset the loadingTab in
+ // case it has been swapped. We also set ourBrowser's state
+ // to whatever otherBrowser's state was before the swap.
+
+ if (this.loadTimer) {
+ // Clearing the load timer means that we will
+ // immediately display a spinner if ourBrowser isn't
+ // ready yet. Typically it will already be ready
+ // though. If it's not, we're probably in a new window,
+ // in which case we have no other tabs to display anyway.
+ this.clearTimer(this.loadTimer);
+ this.loadTimer = null;
+ }
+ this.loadingTab = null;
+
+ let otherState = this.swapMap.get(otherBrowser);
+ this.swapMap.delete(otherBrowser);
+
+ let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
+ if (ourTab) {
+ this.setTabStateNoAction(ourTab, otherState);
+ }
+ },
+
+ shouldActivateDocShell(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ let state = this.getTabState(tab);
+ return state == this.STATE_LOADING || state == this.STATE_LOADED;
+ },
+
+ activateBrowserForPrintPreview(browser) {
+ let tab = this.tabbrowser.getTabForBrowser(browser);
+ this.setTabState(tab, this.STATE_LOADING);
+ },
+
+ // Called when the user asks to switch to a given tab.
+ requestTab: function(tab) {
+ if (tab === this.requestedTab) {
+ return;
+ }
+
+ this.logState("requestTab " + this.tinfo(tab));
+ this.startTabSwitch();
+
+ this.requestedTab = tab;
+
+ let browser = this.requestedTab.linkedBrowser;
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
+ fl.tabParent.suppressDisplayport(true);
+ this.activeSuppressDisplayport.add(fl.tabParent);
+ }
+
+ this.preActions();
+
+ if (this.unloadTimer) {
+ this.clearTimer(this.unloadTimer);
+ }
+ this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
+
+ this.postActions();
+ },
+
+ handleEvent: function(event, delayed = false) {
+ if (this._processing) {
+ this.setTimer(() => this.handleEvent(event, true), 0);
+ return;
+ }
+ if (delayed && this.tabbrowser._switcher != this) {
+ // if we delayed processing this event, we might be out of date, in which
+ // case we drop the delayed events
+ return;
+ }
+ this._processing = true;
+ this.preActions();
+
+ if (event.type == "MozLayerTreeReady") {
+ this.onLayersReady(event.originalTarget);
+ } if (event.type == "MozAfterPaint") {
+ this.onPaint();
+ } else if (event.type == "MozLayerTreeCleared") {
+ this.onLayersCleared(event.originalTarget);
+ } else if (event.type == "TabRemotenessChange") {
+ this.onRemotenessChange(event.target);
+ } else if (event.type == "sizemodechange") {
+ this.onSizeModeChange();
+ } else if (event.type == "SwapDocShells") {
+ this.onSwapDocShells(event.originalTarget, event.detail);
+ } else if (event.type == "EndSwapDocShells") {
+ this.onEndSwapDocShells(event.originalTarget, event.detail);
+ }
+
+ this.postActions();
+ this._processing = false;
+ },
+
+ /*
+ * Telemetry and Profiler related helpers for recording tab switch
+ * timing.
+ */
+
+ startTabSwitch: function () {
+ TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ this.addMarker("AsyncTabSwitch:Start");
+ this.switchInProgress = true;
+ },
+
+ /**
+ * Something has occurred that might mean that we've completed
+ * the tab switch (layers are ready, paints are done, spinners
+ * are hidden). This checks to make sure all conditions are
+ * satisfied, and then records the tab switch as finished.
+ */
+ maybeFinishTabSwitch: function () {
+ if (this.switchInProgress && this.requestedTab &&
+ this.getTabState(this.requestedTab) == this.STATE_LOADED) {
+ // After this point the tab has switched from the content thread's point of view.
+ // The changes will be visible after the next refresh driver tick + composite.
+ let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ if (time != -1) {
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
+ this.log("DEBUG: tab switch time = " + time);
+ this.addMarker("AsyncTabSwitch:Finish");
+ }
+ this.switchInProgress = false;
+ }
+ },
+
+ spinnerDisplayed: function () {
+ this.assert(!this.spinnerTab);
+ TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
+ // We have a second, similar probe for capturing recordings of
+ // when the spinner is displayed for very long periods.
+ TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
+ this.addMarker("AsyncTabSwitch:SpinnerShown");
+ },
+
+ spinnerHidden: function () {
+ this.assert(this.spinnerTab);
+ this.log("DEBUG: spinner time = " +
+ TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
+ TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
+ this.addMarker("AsyncTabSwitch:SpinnerHidden");
+ // we do not get a onPaint after displaying the spinner
+ this.maybeFinishTabSwitch();
+ },
+
+ addMarker: function(marker) {
+ if (Services.profiler) {
+ Services.profiler.AddMarker(marker);
+ }
+ },
+
+ /*
+ * Debug related logging for switcher.
+ */
+
+ _useDumpForLogging: false,
+ _logInit: false,
+
+ logging: function () {
+ if (this._useDumpForLogging)
+ return true;
+ if (this._logInit)
+ return this._shouldLog;
+ let result = false;
+ try {
+ result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming");
+ } catch (ex) {
+ }
+ this._shouldLog = result;
+ this._logInit = true;
+ return this._shouldLog;
+ },
+
+ tinfo: function(tab) {
+ if (tab) {
+ return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
+ }
+ return "null";
+ },
+
+ log: function(s) {
+ if (!this.logging())
+ return;
+ if (this._useDumpForLogging) {
+ dump(s + "\n");
+ } else {
+ Services.console.logStringMessage(s);
+ }
+ },
+
+ logState: function(prefix) {
+ if (!this.logging())
+ return;
+
+ let accum = prefix + " ";
+ for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
+ let tab = this.tabbrowser.tabs[i];
+ let state = this.getTabState(tab);
+
+ accum += i + ":";
+ if (tab === this.lastVisibleTab) accum += "V";
+ if (tab === this.loadingTab) accum += "L";
+ if (tab === this.requestedTab) accum += "R";
+ if (state == this.STATE_LOADED) accum += "(+)";
+ if (state == this.STATE_LOADING) accum += "(+?)";
+ if (state == this.STATE_UNLOADED) accum += "(-)";
+ if (state == this.STATE_UNLOADING) accum += "(-?)";
+ accum += " ";
+ }
+ if (this._useDumpForLogging) {
+ dump(accum + "\n");
+ } else {
+ Services.console.logStringMessage(accum);
+ }
+ },
+ };
+ this._switcher = switcher;
+ switcher.init();
+ return switcher;
+ ]]></body>
+ </method>
+
+ <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
+ MAKE SURE TO ADD IT HERE AS WELL. -->
+ <property name="canGoBack"
+ onget="return this.mCurrentBrowser.canGoBack;"
+ readonly="true"/>
+
+ <property name="canGoForward"
+ onget="return this.mCurrentBrowser.canGoForward;"
+ readonly="true"/>
+
+ <method name="goBack">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goBack();
+ ]]>
+ </body>
+ </method>
+
+ <method name="goForward">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goForward();
+ ]]>
+ </body>
+ </method>
+
+ <method name="reload">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.reload();
+ ]]>
+ </body>
+ </method>
+
+ <method name="reloadWithFlags">
+ <parameter name="aFlags"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.reloadWithFlags(aFlags);
+ ]]>
+ </body>
+ </method>
+
+ <method name="stop">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.stop();
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURI">
+ <parameter name="aURI"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset);
+ ]]>
+ </body>
+ </method>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ // Note - the callee understands both:
+ // (a) loadURIWithFlags(aURI, aFlags, ...)
+ // (b) loadURIWithFlags(aURI, { flags: aFlags, ... })
+ // Forwarding it as (a) here actually supports both (a) and (b),
+ // so you can call us either way too.
+ return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData);
+ ]]>
+ </body>
+ </method>
+
+ <method name="goHome">
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.goHome();
+ ]]>
+ </body>
+ </method>
+
+ <property name="homePage">
+ <getter>
+ <![CDATA[
+ return this.mCurrentBrowser.homePage;
+ ]]>
+ </getter>
+ <setter>
+ <![CDATA[
+ this.mCurrentBrowser.homePage = val;
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <method name="gotoIndex">
+ <parameter name="aIndex"/>
+ <body>
+ <![CDATA[
+ return this.mCurrentBrowser.gotoIndex(aIndex);
+ ]]>
+ </body>
+ </method>
+
+ <property name="currentURI"
+ onget="return this.mCurrentBrowser.currentURI;"
+ readonly="true"/>
+
+ <property name="finder"
+ onget="return this.mCurrentBrowser.finder"
+ readonly="true"/>
+
+ <property name="docShell"
+ onget="return this.mCurrentBrowser.docShell"
+ readonly="true"/>
+
+ <property name="webNavigation"
+ onget="return this.mCurrentBrowser.webNavigation"
+ readonly="true"/>
+
+ <property name="webBrowserFind"
+ readonly="true"
+ onget="return this.mCurrentBrowser.webBrowserFind"/>
+
+ <property name="webProgress"
+ readonly="true"
+ onget="return this.mCurrentBrowser.webProgress"/>
+
+ <property name="contentWindow"
+ readonly="true"
+ onget="return this.mCurrentBrowser.contentWindow"/>
+
+ <property name="contentWindowAsCPOW"
+ readonly="true"
+ onget="return this.mCurrentBrowser.contentWindowAsCPOW"/>
+
+ <property name="sessionHistory"
+ onget="return this.mCurrentBrowser.sessionHistory;"
+ readonly="true"/>
+
+ <property name="markupDocumentViewer"
+ onget="return this.mCurrentBrowser.markupDocumentViewer;"
+ readonly="true"/>
+
+ <property name="contentViewerEdit"
+ onget="return this.mCurrentBrowser.contentViewerEdit;"
+ readonly="true"/>
+
+ <property name="contentViewerFile"
+ onget="return this.mCurrentBrowser.contentViewerFile;"
+ readonly="true"/>
+
+ <property name="contentDocument"
+ onget="return this.mCurrentBrowser.contentDocument;"
+ readonly="true"/>
+
+ <property name="contentDocumentAsCPOW"
+ onget="return this.mCurrentBrowser.contentDocumentAsCPOW;"
+ readonly="true"/>
+
+ <property name="contentTitle"
+ onget="return this.mCurrentBrowser.contentTitle;"
+ readonly="true"/>
+
+ <property name="contentPrincipal"
+ onget="return this.mCurrentBrowser.contentPrincipal;"
+ readonly="true"/>
+
+ <property name="securityUI"
+ onget="return this.mCurrentBrowser.securityUI;"
+ readonly="true"/>
+
+ <property name="fullZoom"
+ onget="return this.mCurrentBrowser.fullZoom;"
+ onset="this.mCurrentBrowser.fullZoom = val;"/>
+
+ <property name="textZoom"
+ onget="return this.mCurrentBrowser.textZoom;"
+ onset="this.mCurrentBrowser.textZoom = val;"/>
+
+ <property name="isSyntheticDocument"
+ onget="return this.mCurrentBrowser.isSyntheticDocument;"
+ readonly="true"/>
+
+ <method name="_handleKeyDownEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ if (aEvent.altKey)
+ return;
+
+ // Don't check if the event was already consumed because tab
+ // navigation should always work for better user experience.
+
+ if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) {
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_PAGE_UP:
+ this.moveTabBackward();
+ aEvent.preventDefault();
+ return;
+ case aEvent.DOM_VK_PAGE_DOWN:
+ this.moveTabForward();
+ aEvent.preventDefault();
+ return;
+ }
+ }
+
+ if (this.AppConstants.platform != "macosx") {
+ if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey &&
+ aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
+ !this.mCurrentTab.pinned) {
+ this.removeCurrentTab({animate: true});
+ aEvent.preventDefault();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <method name="_handleKeyPressEventMac">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ if (!aEvent.isTrusted) {
+ // Don't let untrusted events mess with tabs.
+ return;
+ }
+
+ if (aEvent.altKey)
+ return;
+
+ if (this.AppConstants.platform == "macosx") {
+ if (!aEvent.metaKey)
+ return;
+
+ var offset = 1;
+ switch (aEvent.charCode) {
+ case '}'.charCodeAt(0):
+ offset = -1;
+ case '{'.charCodeAt(0):
+ if (window.getComputedStyle(this, null).direction == "ltr")
+ offset *= -1;
+ this.tabContainer.advanceSelectedTab(offset, true);
+ aEvent.preventDefault();
+ }
+ }
+ ]]></body>
+ </method>
+
+ <property name="userTypedValue"
+ onget="return this.mCurrentBrowser.userTypedValue;"
+ onset="return this.mCurrentBrowser.userTypedValue = val;"/>
+
+ <method name="createTooltip">
+ <parameter name="event"/>
+ <body><![CDATA[
+ event.stopPropagation();
+ var tab = document.tooltipNode;
+ if (tab.localName != "tab") {
+ event.preventDefault();
+ return;
+ }
+
+ let stringWithShortcut = (stringId, keyElemId) => {
+ let keyElem = document.getElementById(keyElemId);
+ let shortcut = ShortcutUtils.prettifyShortcut(keyElem);
+ return this.mStringBundle.getFormattedString(stringId, [shortcut]);
+ };
+
+ var label;
+ if (tab.mOverCloseButton) {
+ label = tab.selected ?
+ stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") :
+ this.mStringBundle.getString("tabs.closeTab.tooltip");
+ } else if (tab._overPlayingIcon) {
+ let stringID;
+ if (tab.selected) {
+ stringID = tab.linkedBrowser.audioMuted ?
+ "tabs.unmuteAudio.tooltip" :
+ "tabs.muteAudio.tooltip";
+ label = stringWithShortcut(stringID, "key_toggleMute");
+ } else {
+ if (tab.linkedBrowser.audioBlocked) {
+ stringID = "tabs.unblockAudio.tooltip";
+ } else {
+ stringID = tab.linkedBrowser.audioMuted ?
+ "tabs.unmuteAudio.background.tooltip" :
+ "tabs.muteAudio.background.tooltip";
+ }
+
+ label = this.mStringBundle.getString(stringID);
+ }
+ } else {
+ label = tab.getAttribute("label") +
+ (this.AppConstants.E10S_TESTING_ONLY && tab.linkedBrowser && tab.linkedBrowser.isRemoteBrowser ? " - e10s" : "");
+ }
+ event.target.setAttribute("label", label);
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "keydown":
+ this._handleKeyDownEvent(aEvent);
+ break;
+ case "keypress":
+ this._handleKeyPressEventMac(aEvent);
+ break;
+ case "sizemodechange":
+ if (aEvent.target == window && !this._switcher) {
+ this.mCurrentBrowser.preserveLayers(window.windowState == window.STATE_MINIMIZED);
+ this.mCurrentBrowser.docShellIsActive = this.shouldActivateDocShell(this.mCurrentBrowser);
+ }
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="receiveMessage">
+ <parameter name="aMessage"/>
+ <body><![CDATA[
+ let data = aMessage.data;
+ let browser = aMessage.target;
+
+ switch (aMessage.name) {
+ case "DOMTitleChanged": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab || tab.hasAttribute("pending"))
+ return undefined;
+ let titleChanged = this.setTabTitle(tab);
+ if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
+ tab.setAttribute("titlechanged", "true");
+ break;
+ }
+ case "DOMWindowClose": {
+ if (this.tabs.length == 1) {
+ // We already did PermitUnload in the content process
+ // for this tab (the only one in the window). So we don't
+ // need to do it again for any tabs.
+ window.skipNextCanClose = true;
+ window.close();
+ return undefined;
+ }
+
+ let tab = this.getTabForBrowser(browser);
+ if (tab) {
+ // Skip running PermitUnload since it already happened in
+ // the content process.
+ this.removeTab(tab, {skipPermitUnload: true});
+ }
+ break;
+ }
+ case "contextmenu": {
+ let spellInfo = data.spellInfo;
+ if (spellInfo)
+ spellInfo.target = aMessage.target.messageManager;
+ let documentURIObject = makeURI(data.docLocation,
+ data.charSet,
+ makeURI(data.baseURI));
+ gContextMenuContentData = { isRemote: true,
+ event: aMessage.objects.event,
+ popupNode: aMessage.objects.popupNode,
+ browser: browser,
+ editFlags: data.editFlags,
+ spellInfo: spellInfo,
+ principal: data.principal,
+ customMenuItems: data.customMenuItems,
+ addonInfo: data.addonInfo,
+ documentURIObject: documentURIObject,
+ docLocation: data.docLocation,
+ charSet: data.charSet,
+ referrer: data.referrer,
+ referrerPolicy: data.referrerPolicy,
+ contentType: data.contentType,
+ contentDisposition: data.contentDisposition,
+ frameOuterWindowID: data.frameOuterWindowID,
+ selectionInfo: data.selectionInfo,
+ disableSetDesktopBackground: data.disableSetDesktopBg,
+ loginFillInfo: data.loginFillInfo,
+ parentAllowsMixedContent: data.parentAllowsMixedContent,
+ userContextId: data.userContextId,
+ };
+ let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
+ let event = gContextMenuContentData.event;
+ popup.openPopupAtScreen(event.screenX, event.screenY, true);
+ break;
+ }
+ case "DOMServiceWorkerFocusClient":
+ case "DOMWebNotificationClicked": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab)
+ return undefined;
+ this.selectedTab = tab;
+ window.focus();
+ break;
+ }
+ case "Browser:Init": {
+ let tab = this.getTabForBrowser(browser);
+ if (!tab)
+ return undefined;
+
+ this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
+ browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
+ break;
+ }
+ case "Browser:WindowCreated": {
+ let tab = this.getTabForBrowser(browser);
+ if (tab && data.userContextId) {
+ ContextualIdentityService.telemetry(data.userContextId);
+ tab.setUserContextId(data.userContextId);
+ }
+
+ // We don't want to update the container icon and identifier if
+ // this is not the selected browser.
+ if (browser == gBrowser.selectedBrowser) {
+ updateUserContextUIIndicator();
+ }
+
+ break;
+ }
+ case "Findbar:Keypress": {
+ let tab = this.getTabForBrowser(browser);
+ // If the find bar for this tab is not yet alive, only initialize
+ // it if there's a possibility FindAsYouType will be used.
+ // There's no point in doing it for most random keypresses.
+ if (!this.isFindBarInitialized(tab) &&
+ data.shouldFastFind) {
+ let shouldFastFind = this._findAsYouType;
+ if (!shouldFastFind) {
+ // Please keep in sync with toolkit/content/widgets/findbar.xml
+ const FAYT_LINKS_KEY = "'";
+ const FAYT_TEXT_KEY = "/";
+ let charCode = data.fakeEvent.charCode;
+ let key = charCode ? String.fromCharCode(charCode) : null;
+ shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY;
+ }
+ if (shouldFastFind) {
+ // Make sure we return the result.
+ return this.getFindBar(tab).receiveMessage(aMessage);
+ }
+ }
+ break;
+ }
+ case "RefreshBlocker:Blocked": {
+ let event = new CustomEvent("RefreshBlocked", {
+ bubbles: true,
+ cancelable: false,
+ detail: data,
+ });
+
+ browser.dispatchEvent(event);
+
+ break;
+ }
+
+ }
+ return undefined;
+ ]]></body>
+ </method>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ let browser;
+ switch (aTopic) {
+ case "live-resize-start":
+ browser = this.mCurrentTab.linkedBrowser;
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent && !this.mActiveResizeDisplayportSuppression) {
+ fl.tabParent.suppressDisplayport(true);
+ this.mActiveResizeDisplayportSuppression = browser;
+ }
+ break;
+ case "live-resize-end":
+ browser = this.mActiveResizeDisplayportSuppression;
+ if (browser) {
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ if (fl && fl.tabParent) {
+ fl.tabParent.suppressDisplayport(false);
+ this.mActiveResizeDisplayportSuppression = null;
+ }
+ }
+ break;
+ case "nsPref:changed":
+ // This is the only pref observed.
+ this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <constructor>
+ <![CDATA[
+ this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser");
+ this.mCurrentBrowser.permanentKey = {};
+
+ Services.obs.addObserver(this, "live-resize-start", false);
+ Services.obs.addObserver(this, "live-resize-end", false);
+
+ this.mCurrentTab = this.tabContainer.firstChild;
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.addSystemEventListener(document, "keydown", this, false);
+ if (this.AppConstants.platform == "macosx") {
+ els.addSystemEventListener(document, "keypress", this, false);
+ }
+ window.addEventListener("sizemodechange", this, false);
+
+ var uniqueId = this._generateUniquePanelID();
+ this.mPanelContainer.childNodes[0].id = uniqueId;
+ this.mCurrentTab.linkedPanel = uniqueId;
+ this.mCurrentTab.permanentKey = this.mCurrentBrowser.permanentKey;
+ this.mCurrentTab._tPos = 0;
+ this.mCurrentTab._fullyOpen = true;
+ this.mCurrentTab.cachePosition = 0;
+ this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
+ this.mCurrentTab.hasBrowser = true;
+ this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
+
+ // set up the shared autoscroll popup
+ this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
+ this._autoScrollPopup.id = "autoscroller";
+ this.appendChild(this._autoScrollPopup);
+ this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
+ this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
+ this.updateWindowResizers();
+
+ // Hook up the event listeners to the first browser
+ var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
+ const nsIWebProgress = Components.interfaces.nsIWebProgress;
+ const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
+ .createInstance(nsIWebProgress);
+ filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
+ this._tabListeners.set(this.mCurrentTab, tabListener);
+ this._tabFilters.set(this.mCurrentTab, filter);
+ this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
+
+ this.style.backgroundColor =
+ Services.prefs.getBoolPref("browser.display.use_system_colors") ?
+ "-moz-default-background-color" :
+ Services.prefs.getCharPref("browser.display.background_color");
+
+ let remote = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsILoadContext)
+ .useRemoteTabs;
+ if (remote) {
+ messageManager.addMessageListener("DOMTitleChanged", this);
+ messageManager.addMessageListener("DOMWindowClose", this);
+ messageManager.addMessageListener("contextmenu", this);
+ messageManager.addMessageListener("Browser:Init", this);
+
+ // If this window has remote tabs, switch to our tabpanels fork
+ // which does asynchronous tab switching.
+ this.mPanelContainer.classList.add("tabbrowser-tabpanels");
+ } else {
+ this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID,
+ this.mCurrentBrowser);
+ }
+ messageManager.addMessageListener("DOMWebNotificationClicked", this);
+ messageManager.addMessageListener("DOMServiceWorkerFocusClient", this);
+ messageManager.addMessageListener("RefreshBlocker:Blocked", this);
+ messageManager.addMessageListener("Browser:WindowCreated", this);
+
+ // To correctly handle keypresses for potential FindAsYouType, while
+ // the tab's find bar is not yet initialized.
+ this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
+ Services.prefs.addObserver("accessibility.typeaheadfind", this, false);
+ messageManager.addMessageListener("Findbar:Keypress", this);
+ ]]>
+ </constructor>
+
+ <method name="_generateUniquePanelID">
+ <body><![CDATA[
+ if (!this._uniquePanelIDCounter) {
+ this._uniquePanelIDCounter = 0;
+ }
+
+ let outerID = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .outerWindowID;
+
+ // We want panel IDs to be globally unique, that's why we include the
+ // window ID. We switched to a monotonic counter as Date.now() lead
+ // to random failures because of colliding IDs.
+ return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter);
+ ]]></body>
+ </method>
+
+ <destructor>
+ <![CDATA[
+ Services.obs.removeObserver(this, "live-resize-start", false);
+ Services.obs.removeObserver(this, "live-resize-end", false);
+
+ for (let tab of this.tabs) {
+ let browser = tab.linkedBrowser;
+ if (browser.registeredOpenURI) {
+ this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
+ browser.getAttribute("usercontextid") || 0);
+ delete browser.registeredOpenURI;
+ }
+ let filter = this._tabFilters.get(tab);
+ let listener = this._tabListeners.get(tab);
+
+ browser.webProgress.removeProgressListener(filter);
+ filter.removeProgressListener(listener);
+ listener.destroy();
+
+ this._tabFilters.delete(tab);
+ this._tabListeners.delete(tab);
+ }
+ const nsIEventListenerService =
+ Components.interfaces.nsIEventListenerService;
+ let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
+ .getService(nsIEventListenerService);
+ els.removeSystemEventListener(document, "keydown", this, false);
+ if (this.AppConstants.platform == "macosx") {
+ els.removeSystemEventListener(document, "keypress", this, false);
+ }
+ window.removeEventListener("sizemodechange", this, false);
+
+ if (gMultiProcessBrowser) {
+ messageManager.removeMessageListener("DOMTitleChanged", this);
+ messageManager.removeMessageListener("contextmenu", this);
+
+ if (this._switcher) {
+ this._switcher.destroy();
+ }
+ }
+
+ Services.prefs.removeObserver("accessibility.typeaheadfind", this);
+ ]]>
+ </destructor>
+
+ <!-- Deprecated stuff, implemented for backwards compatibility. -->
+ <method name="enterTabbedMode">
+ <body>
+ Services.console.logStringMessage("enterTabbedMode is an obsolete method and " +
+ "will be removed in a future release.");
+ </body>
+ </method>
+ <field name="mTabbedMode" readonly="true">true</field>
+ <method name="setStripVisibilityTo">
+ <parameter name="aShow"/>
+ <body>
+ this.tabContainer.visible = aShow;
+ </body>
+ </method>
+ <method name="getStripVisibility">
+ <body>
+ return this.tabContainer.visible;
+ </body>
+ </method>
+
+ <property name="mContextTab" readonly="true"
+ onget="return TabContextMenu.contextTab;"/>
+ <property name="mPrefs" readonly="true"
+ onget="return Services.prefs;"/>
+ <property name="mTabContainer" readonly="true"
+ onget="return this.tabContainer;"/>
+ <property name="mTabs" readonly="true"
+ onget="return this.tabs;"/>
+ <!--
+ - Compatibility hack: several extensions depend on this property to
+ - access the tab context menu or tab container, so keep that working for
+ - now. Ideally we can remove this once extensions are using
+ - tabbrowser.tabContextMenu and tabbrowser.tabContainer directly.
+ -->
+ <property name="mStrip" readonly="true">
+ <getter>
+ <![CDATA[
+ return ({
+ self: this,
+ childNodes: [null, this.tabContextMenu, this.tabContainer],
+ firstChild: { nextSibling: this.tabContextMenu },
+ getElementsByAttribute: function (attr, attrValue) {
+ if (attr == "anonid" && attrValue == "tabContextMenu")
+ return [this.self.tabContextMenu];
+ return [];
+ },
+ // Also support adding event listeners (forward to the tab container)
+ addEventListener: function (a, b, c) { this.self.tabContainer.addEventListener(a, b, c); },
+ removeEventListener: function (a, b, c) { this.self.tabContainer.removeEventListener(a, b, c); }
+ });
+ ]]>
+ </getter>
+ </property>
+ <field name="_soundPlayingAttrRemovalTimer">0</field>
+ </implementation>
+
+ <handlers>
+ <handler event="DOMWindowClose" phase="capturing">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ if (this.tabs.length == 1) {
+ // We already did PermitUnload in nsGlobalWindow::Close
+ // for this tab. There are no other tabs we need to do
+ // PermitUnload for.
+ window.skipNextCanClose = true;
+ return;
+ }
+
+ var tab = this._getTabForContentWindow(event.target);
+ if (tab) {
+ // Skip running PermitUnload since it already happened.
+ this.removeTab(tab, {skipPermitUnload: true});
+ event.preventDefault();
+ }
+ ]]>
+ </handler>
+ <handler event="DOMWillOpenModalDialog" phase="capturing">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ let targetIsWindow = event.target instanceof Window;
+
+ // We're about to open a modal dialog, so figure out for which tab:
+ // If this is a same-process modal dialog, then we're given its DOM
+ // window as the event's target. For remote dialogs, we're given the
+ // browser, but that's in the originalTarget and not the target,
+ // because it's across the tabbrowser's XBL boundary.
+ let tabForEvent = targetIsWindow ?
+ this._getTabForContentWindow(event.target.top) :
+ this.getTabForBrowser(event.originalTarget);
+
+ // Don't need to act if the tab is already selected:
+ if (tabForEvent.selected)
+ return;
+
+ // If this is a tabprompt, we won't switch tabs, unless:
+ // - this is a beforeunload prompt
+ // - this behaviour has been disabled entirely using the pref
+ if (event.detail && event.detail.tabPrompt &&
+ !event.detail.inPermitUnload &&
+ Services.prefs.getBoolPref("browser.tabs.dontfocusfordialogs")) {
+ let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
+ // At least one of these should/will be non-null:
+ let promptPrincipal = event.detail.promptPrincipal || docPrincipal ||
+ tabForEvent.linkedBrowser.contentPrincipal;
+ // For null principals, we bail immediately and don't show the checkbox:
+ if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
+ tabForEvent.setAttribute("attention", "true");
+ return;
+ }
+
+ // For non-system/expanded principals, we bail and show the checkbox
+ if (promptPrincipal.URI &&
+ !Services.scriptSecurityManager.isSystemPrincipal(promptPrincipal)) {
+ let permission = Services.perms.testPermissionFromPrincipal(promptPrincipal,
+ "focus-tab-by-prompt");
+ if (permission != Services.perms.ALLOW_ACTION) {
+ // Tell the prompt box we want to show the user a checkbox:
+ let tabPrompt = this.getTabModalPromptBox(tabForEvent.linkedBrowser);
+ tabPrompt.onNextPromptShowAllowFocusCheckboxFor(promptPrincipal);
+ tabForEvent.setAttribute("attention", "true");
+ return;
+ }
+ }
+ // ... so system and expanded principals, as well as permitted "normal"
+ // URI-based principals, always get to steal focus for the tab when prompting.
+ }
+
+ // if prefs/permissions/origins so dictate, bring tab to the front:
+ this.selectedTab = tabForEvent;
+ ]]>
+ </handler>
+ <handler event="DOMTitleChanged">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ var contentWin = event.target.defaultView;
+ if (contentWin != contentWin.top)
+ return;
+
+ var tab = this._getTabForContentWindow(contentWin);
+ if (!tab || tab.hasAttribute("pending"))
+ return;
+
+ var titleChanged = this.setTabTitle(tab);
+ if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
+ tab.setAttribute("titlechanged", "true");
+ ]]>
+ </handler>
+ <handler event="oop-browser-crashed">
+ <![CDATA[
+ if (!event.isTrusted)
+ return;
+
+ let browser = event.originalTarget;
+ let icon = browser.mIconURL;
+ let tab = this.getTabForBrowser(browser);
+
+ if (this.selectedBrowser == browser) {
+ TabCrashHandler.onSelectedBrowserCrash(browser);
+ } else {
+ this.updateBrowserRemoteness(browser, false);
+ SessionStore.reviveCrashedTab(tab);
+ }
+
+ tab.removeAttribute("soundplaying");
+ this.setIcon(tab, icon, browser.contentPrincipal);
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackStarted">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ clearTimeout(tab._soundPlayingAttrRemovalTimer);
+ tab._soundPlayingAttrRemovalTimer = 0;
+
+ let modifiedAttrs = [];
+ if (tab.hasAttribute("soundplaying-scheduledremoval")) {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ modifiedAttrs.push("soundplaying-scheduledremoval");
+ }
+
+ if (!tab.hasAttribute("soundplaying")) {
+ tab.setAttribute("soundplaying", true);
+ modifiedAttrs.push("soundplaying");
+ }
+
+ this._tabAttrModified(tab, modifiedAttrs);
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackStopped">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("soundplaying")) {
+ let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS");
+
+ tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`);
+ tab.setAttribute("soundplaying-scheduledremoval", "true");
+ this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
+
+ tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
+ tab.removeAttribute("soundplaying-scheduledremoval");
+ tab.removeAttribute("soundplaying");
+ this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]);
+ }, removalDelay);
+ }
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackBlockStarted">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (!tab.hasAttribute("blocked")) {
+ tab.setAttribute("blocked", true);
+ this._tabAttrModified(tab, ["blocked"]);
+ }
+ ]]>
+ </handler>
+ <handler event="DOMAudioPlaybackBlockStopped">
+ <![CDATA[
+ var tab = getTabFromAudioEvent(event)
+ if (!tab) {
+ return;
+ }
+
+ if (tab.hasAttribute("blocked")) {
+ tab.removeAttribute("blocked");
+ this._tabAttrModified(tab, ["blocked"]);
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tabbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tabbox">
+ <implementation>
+ <property name="tabs" readonly="true"
+ onget="return document.getBindingParent(this).tabContainer;"/>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll">
+ <implementation>
+ <!-- Override scrollbox.xml method, since our scrollbox's children are
+ inherited from the binding parent -->
+ <method name="_getScrollableElements">
+ <body><![CDATA[
+ return Array.filter(document.getBindingParent(this).childNodes,
+ this._canScrollToElement, this);
+ ]]></body>
+ </method>
+ <method name="_canScrollToElement">
+ <parameter name="tab"/>
+ <body><![CDATA[
+ return !tab.pinned && !tab.hidden;
+ ]]></body>
+ </method>
+ <field name="_tabMarginLeft">null</field>
+ <field name="_tabMarginRight">null</field>
+ <method name="_calcTabMargins">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (this._tabMarginLeft === null || this._tabMarginRight === null) {
+ let tabMiddle = document.getAnonymousElementByAttribute(aTab, "class", "tab-background-middle");
+ let tabMiddleStyle = window.getComputedStyle(tabMiddle, null);
+ this._tabMarginLeft = parseFloat(tabMiddleStyle.marginLeft);
+ this._tabMarginRight = parseFloat(tabMiddleStyle.marginRight);
+ }
+ ]]></body>
+ </method>
+ <method name="_adjustElementStartAndEnd">
+ <parameter name="aTab"/>
+ <parameter name="tabStart"/>
+ <parameter name="tabEnd"/>
+ <body><![CDATA[
+ this._calcTabMargins(aTab);
+ if (this._tabMarginLeft < 0) {
+ tabStart = tabStart + this._tabMarginLeft;
+ }
+ if (this._tabMarginRight < 0) {
+ tabEnd = tabEnd - this._tabMarginRight;
+ }
+ return [tabStart, tabEnd];
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="underflow" phase="capturing"><![CDATA[
+ if (event.detail == 0)
+ return; // Ignore vertical events
+
+ var tabs = document.getBindingParent(this);
+ tabs.removeAttribute("overflow");
+
+ if (tabs._lastTabClosedByMouse)
+ tabs._expandSpacerBy(this._scrollButtonDown.clientWidth);
+
+ for (let tab of Array.from(tabs.tabbrowser._removingTabs))
+ tabs.tabbrowser.removeTab(tab);
+
+ tabs._positionPinnedTabs();
+ ]]></handler>
+ <handler event="overflow"><![CDATA[
+ if (event.detail == 0)
+ return; // Ignore vertical events
+
+ var tabs = document.getBindingParent(this);
+ tabs.setAttribute("overflow", "true");
+ tabs._positionPinnedTabs();
+ tabs._handleTabSelect(false);
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tabs"
+ extends="chrome://global/content/bindings/tabbox.xml#tabs">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content>
+ <xul:hbox align="end">
+ <xul:image class="tab-drop-indicator" anonid="tab-drop-indicator" collapsed="true"/>
+ </xul:hbox>
+ <xul:arrowscrollbox anonid="arrowscrollbox" orient="horizontal" flex="1"
+ style="min-width: 1px;"
+ class="tabbrowser-arrowscrollbox">
+<!--
+ This is a hack to circumvent bug 472020, otherwise the tabs show up on the
+ right of the newtab button.
+-->
+ <children includes="tab"/>
+<!--
+ This is to ensure anything extensions put here will go before the newtab
+ button, necessary due to the previous hack.
+-->
+ <children/>
+ <xul:toolbarbutton class="tabs-newtab-button"
+ anonid="tabs-newtab-button"
+ command="cmd_newNavigatorTab"
+ onclick="checkForMiddleClick(this, event);"
+ onmouseover="document.getBindingParent(this)._enterNewTab();"
+ onmouseout="document.getBindingParent(this)._leaveNewTab();"
+ tooltip="dynamic-shortcut-tooltip"/>
+ <xul:spacer class="closing-tabs-spacer" anonid="closing-tabs-spacer"
+ style="width: 0;"/>
+ </xul:arrowscrollbox>
+ </content>
+
+ <implementation implements="nsIDOMEventListener, nsIObserver">
+ <constructor>
+ <![CDATA[
+ this.mTabClipWidth = Services.prefs.getIntPref("browser.tabs.tabClipWidth");
+
+ var tab = this.firstChild;
+ tab.label = this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle");
+ tab.setAttribute("crop", "end");
+ tab.setAttribute("onerror", "this.removeAttribute('image');");
+
+ window.addEventListener("resize", this, false);
+ window.addEventListener("load", this, false);
+
+ try {
+ this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled");
+ } catch (ex) {
+ this._tabAnimationLoggingEnabled = false;
+ }
+ this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled");
+ this.observe(null, "nsPref:changed", "privacy.userContext.enabled");
+ Services.prefs.addObserver("privacy.userContext.enabled", this, false);
+ ]]>
+ </constructor>
+
+ <destructor>
+ <![CDATA[
+ Services.prefs.removeObserver("privacy.userContext.enabled", this);
+ ]]>
+ </destructor>
+
+ <field name="tabbrowser" readonly="true">
+ document.getElementById(this.getAttribute("tabbrowser"));
+ </field>
+
+ <field name="tabbox" readonly="true">
+ this.tabbrowser.mTabBox;
+ </field>
+
+ <field name="contextMenu" readonly="true">
+ document.getElementById("tabContextMenu");
+ </field>
+
+ <field name="mTabstripWidth">0</field>
+
+ <field name="mTabstrip">
+ document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox");
+ </field>
+
+ <field name="_firstTab">null</field>
+ <field name="_lastTab">null</field>
+ <field name="_afterSelectedTab">null</field>
+ <field name="_beforeHoveredTab">null</field>
+ <field name="_afterHoveredTab">null</field>
+ <field name="_hoveredTab">null</field>
+
+ <method name="observe">
+ <parameter name="aSubject"/>
+ <parameter name="aTopic"/>
+ <parameter name="aData"/>
+ <body><![CDATA[
+ switch (aTopic) {
+ case "nsPref:changed":
+ // This is the only pref observed.
+ let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+
+ const newTab = document.getElementById("new-tab-button");
+ const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button")
+
+ if (containersEnabled) {
+ for (let parent of [newTab, newTab2]) {
+ if (!parent)
+ continue;
+ let popup = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "menupopup");
+ if (parent.id) {
+ popup.id = "newtab-popup";
+ } else {
+ popup.setAttribute("anonid", "newtab-popup");
+ }
+ popup.className = "new-tab-popup";
+ popup.setAttribute("position", "after_end");
+ parent.appendChild(popup);
+
+ gClickAndHoldListenersOnElement.add(parent);
+ parent.setAttribute("type", "menu");
+ }
+ } else {
+ for (let parent of [newTab, newTab2]) {
+ if (!parent)
+ continue;
+ gClickAndHoldListenersOnElement.remove(parent);
+ parent.removeAttribute("type");
+ if (!parent.firstChild)
+ continue;
+ parent.firstChild.remove();
+ }
+ }
+
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <property name="_isCustomizing" readonly="true">
+ <getter>
+ let root = document.documentElement;
+ return root.getAttribute("customizing") == "true" ||
+ root.getAttribute("customize-exiting") == "true";
+ </getter>
+ </property>
+
+ <method name="_setPositionalAttributes">
+ <body><![CDATA[
+ let visibleTabs = this.tabbrowser.visibleTabs;
+
+ if (!visibleTabs.length)
+ return;
+
+ let selectedIndex = visibleTabs.indexOf(this.selectedItem);
+
+ let lastVisible = visibleTabs.length - 1;
+
+ if (this._afterSelectedTab)
+ this._afterSelectedTab.removeAttribute("afterselected-visible");
+ if (this.selectedItem.closing || selectedIndex == lastVisible) {
+ this._afterSelectedTab = null;
+ } else {
+ this._afterSelectedTab = visibleTabs[selectedIndex + 1];
+ this._afterSelectedTab.setAttribute("afterselected-visible",
+ "true");
+ }
+
+ if (this._firstTab)
+ this._firstTab.removeAttribute("first-visible-tab");
+ this._firstTab = visibleTabs[0];
+ this._firstTab.setAttribute("first-visible-tab", "true");
+ if (this._lastTab)
+ this._lastTab.removeAttribute("last-visible-tab");
+ this._lastTab = visibleTabs[lastVisible];
+ this._lastTab.setAttribute("last-visible-tab", "true");
+
+ let hoveredTab = this._hoveredTab;
+ if (hoveredTab) {
+ hoveredTab._mouseleave();
+ }
+ hoveredTab = this.querySelector("tab:hover");
+ if (hoveredTab) {
+ hoveredTab._mouseenter();
+ }
+ ]]></body>
+ </method>
+
+ <field name="_blockDblClick">false</field>
+
+ <field name="_tabDropIndicator">
+ document.getAnonymousElementByAttribute(this, "anonid", "tab-drop-indicator");
+ </field>
+
+ <field name="_dragOverDelay">350</field>
+ <field name="_dragTime">0</field>
+
+ <field name="_container" readonly="true"><![CDATA[
+ this.parentNode && this.parentNode.localName == "toolbar" ? this.parentNode : this;
+ ]]></field>
+
+ <field name="_propagatedVisibilityOnce">false</field>
+
+ <property name="visible"
+ onget="return !this._container.collapsed;">
+ <setter><![CDATA[
+ if (val == this.visible &&
+ this._propagatedVisibilityOnce)
+ return val;
+
+ this._container.collapsed = !val;
+
+ this._propagateVisibility();
+ this._propagatedVisibilityOnce = true;
+
+ return val;
+ ]]></setter>
+ </property>
+
+ <method name="_enterNewTab">
+ <body><![CDATA[
+ let visibleTabs = this.tabbrowser.visibleTabs;
+ let candidate = visibleTabs[visibleTabs.length - 1];
+ if (!candidate.selected) {
+ this._beforeHoveredTab = candidate;
+ candidate.setAttribute("beforehovered", "true");
+ }
+ ]]></body>
+ </method>
+
+ <method name="_leaveNewTab">
+ <body><![CDATA[
+ if (this._beforeHoveredTab) {
+ this._beforeHoveredTab.removeAttribute("beforehovered");
+ this._beforeHoveredTab = null;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_propagateVisibility">
+ <body><![CDATA[
+ let visible = this.visible;
+
+ document.getElementById("menu_closeWindow").hidden = !visible;
+ document.getElementById("menu_close").setAttribute("label",
+ this.tabbrowser.mStringBundle.getString(visible ? "tabs.closeTab" : "tabs.close"));
+
+ TabsInTitlebar.allowedBy("tabs-visible", visible);
+ ]]></body>
+ </method>
+
+ <method name="updateVisibility">
+ <body><![CDATA[
+ if (this.childNodes.length - this.tabbrowser._removingTabs.length == 1)
+ this.visible = window.toolbar.visible;
+ else
+ this.visible = true;
+ ]]></body>
+ </method>
+
+ <method name="adjustTabstrip">
+ <body><![CDATA[
+ let numTabs = this.childNodes.length -
+ this.tabbrowser._removingTabs.length;
+ if (numTabs > 2) {
+ // This is an optimization to avoid layout flushes by calling
+ // getBoundingClientRect() when we just opened a second tab. In
+ // this case it's highly unlikely that the tab width is smaller
+ // than mTabClipWidth and the tab close button obscures too much
+ // of the tab's label. In the edge case of the window being too
+ // narrow (or if tabClipWidth has been set to a way higher value),
+ // we'll correct the 'closebuttons' attribute after the tabopen
+ // animation has finished.
+
+ let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs];
+ if (tab && tab.getBoundingClientRect().width <= this.mTabClipWidth) {
+ this.setAttribute("closebuttons", "activetab");
+ return;
+ }
+ }
+ this.removeAttribute("closebuttons");
+ ]]></body>
+ </method>
+
+ <method name="_handleTabSelect">
+ <parameter name="aSmoothScroll"/>
+ <body><![CDATA[
+ if (this.getAttribute("overflow") == "true")
+ this.mTabstrip.ensureElementIsVisible(this.selectedItem, aSmoothScroll);
+ ]]></body>
+ </method>
+
+ <method name="_fillTrailingGap">
+ <body><![CDATA[
+ try {
+ // if we're at the right side (and not the logical end,
+ // which is why this works for both LTR and RTL)
+ // of the tabstrip, we need to ensure that we stay
+ // completely scrolled to the right side
+ var tabStrip = this.mTabstrip;
+ if (tabStrip.scrollPosition + tabStrip.scrollClientSize >
+ tabStrip.scrollSize)
+ tabStrip.scrollByPixels(-1);
+ } catch (e) {}
+ ]]></body>
+ </method>
+
+ <field name="_closingTabsSpacer">
+ document.getAnonymousElementByAttribute(this, "anonid", "closing-tabs-spacer");
+ </field>
+
+ <field name="_tabDefaultMaxWidth">NaN</field>
+ <field name="_lastTabClosedByMouse">false</field>
+ <field name="_hasTabTempMaxWidth">false</field>
+
+ <!-- Try to keep the active tab's close button under the mouse cursor -->
+ <method name="_lockTabSizing">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ var tabs = this.tabbrowser.visibleTabs;
+ if (!tabs.length)
+ return;
+
+ var isEndTab = (aTab._tPos > tabs[tabs.length-1]._tPos);
+ var tabWidth = aTab.getBoundingClientRect().width;
+
+ if (!this._tabDefaultMaxWidth)
+ this._tabDefaultMaxWidth =
+ parseFloat(window.getComputedStyle(aTab).maxWidth);
+ this._lastTabClosedByMouse = true;
+
+ if (this.getAttribute("overflow") == "true") {
+ // Don't need to do anything if we're in overflow mode and aren't scrolled
+ // all the way to the right, or if we're closing the last tab.
+ if (isEndTab || !this.mTabstrip._scrollButtonDown.disabled)
+ return;
+
+ // If the tab has an owner that will become the active tab, the owner will
+ // be to the left of it, so we actually want the left tab to slide over.
+ // This can't be done as easily in non-overflow mode, so we don't bother.
+ if (aTab.owner)
+ return;
+
+ this._expandSpacerBy(tabWidth);
+ } else { // non-overflow mode
+ // Locking is neither in effect nor needed, so let tabs expand normally.
+ if (isEndTab && !this._hasTabTempMaxWidth)
+ return;
+
+ let numPinned = this.tabbrowser._numPinnedTabs;
+ // Force tabs to stay the same width, unless we're closing the last tab,
+ // which case we need to let them expand just enough so that the overall
+ // tabbar width is the same.
+ if (isEndTab) {
+ let numNormalTabs = tabs.length - numPinned;
+ tabWidth = tabWidth * (numNormalTabs + 1) / numNormalTabs;
+ if (tabWidth > this._tabDefaultMaxWidth)
+ tabWidth = this._tabDefaultMaxWidth;
+ }
+ tabWidth += "px";
+ for (let i = numPinned; i < tabs.length; i++) {
+ let tab = tabs[i];
+ tab.style.setProperty("max-width", tabWidth, "important");
+ if (!isEndTab) { // keep tabs the same width
+ tab.style.transition = "none";
+ tab.clientTop; // flush styles to skip animation; see bug 649247
+ tab.style.transition = "";
+ }
+ }
+ this._hasTabTempMaxWidth = true;
+ this.tabbrowser.addEventListener("mousemove", this, false);
+ window.addEventListener("mouseout", this, false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_expandSpacerBy">
+ <parameter name="pixels"/>
+ <body><![CDATA[
+ let spacer = this._closingTabsSpacer;
+ spacer.style.width = parseFloat(spacer.style.width) + pixels + "px";
+ this.setAttribute("using-closing-tabs-spacer", "true");
+ this.tabbrowser.addEventListener("mousemove", this, false);
+ window.addEventListener("mouseout", this, false);
+ ]]></body>
+ </method>
+
+ <method name="_unlockTabSizing">
+ <body><![CDATA[
+ this.tabbrowser.removeEventListener("mousemove", this, false);
+ window.removeEventListener("mouseout", this, false);
+
+ if (this._hasTabTempMaxWidth) {
+ this._hasTabTempMaxWidth = false;
+ let tabs = this.tabbrowser.visibleTabs;
+ for (let i = 0; i < tabs.length; i++)
+ tabs[i].style.maxWidth = "";
+ }
+
+ if (this.hasAttribute("using-closing-tabs-spacer")) {
+ this.removeAttribute("using-closing-tabs-spacer");
+ this._closingTabsSpacer.style.width = 0;
+ }
+ ]]></body>
+ </method>
+
+ <field name="_lastNumPinned">0</field>
+ <method name="_positionPinnedTabs">
+ <body><![CDATA[
+ var numPinned = this.tabbrowser._numPinnedTabs;
+ var doPosition = this.getAttribute("overflow") == "true" &&
+ numPinned > 0;
+
+ if (doPosition) {
+ this.setAttribute("positionpinnedtabs", "true");
+
+ let scrollButtonWidth = this.mTabstrip._scrollButtonDown.getBoundingClientRect().width;
+ let paddingStart = this.mTabstrip.scrollboxPaddingStart;
+ let width = 0;
+
+ for (let i = numPinned - 1; i >= 0; i--) {
+ let tab = this.childNodes[i];
+ width += tab.getBoundingClientRect().width;
+ tab.style.marginInlineStart = - (width + scrollButtonWidth + paddingStart) + "px";
+ }
+
+ this.style.paddingInlineStart = width + paddingStart + "px";
+
+ } else {
+ this.removeAttribute("positionpinnedtabs");
+
+ for (let i = 0; i < numPinned; i++) {
+ let tab = this.childNodes[i];
+ tab.style.marginInlineStart = "";
+ }
+
+ this.style.paddingInlineStart = "";
+ }
+
+ if (this._lastNumPinned != numPinned) {
+ this._lastNumPinned = numPinned;
+ this._handleTabSelect(false);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_animateTabMove">
+ <parameter name="event"/>
+ <body><![CDATA[
+ let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
+
+ if (this.getAttribute("movingtab") != "true") {
+ this.setAttribute("movingtab", "true");
+ this.selectedItem = draggedTab;
+ }
+
+ if (!("animLastScreenX" in draggedTab._dragData))
+ draggedTab._dragData.animLastScreenX = draggedTab._dragData.screenX;
+
+ let screenX = event.screenX;
+ if (screenX == draggedTab._dragData.animLastScreenX)
+ return;
+
+ draggedTab._dragData.animLastScreenX = screenX;
+
+ let rtl = (window.getComputedStyle(this).direction == "rtl");
+ let pinned = draggedTab.pinned;
+ let numPinned = this.tabbrowser._numPinnedTabs;
+ let tabs = this.tabbrowser.visibleTabs
+ .slice(pinned ? 0 : numPinned,
+ pinned ? numPinned : undefined);
+ if (rtl)
+ tabs.reverse();
+ let tabWidth = draggedTab.getBoundingClientRect().width;
+
+ // Move the dragged tab based on the mouse position.
+
+ let leftTab = tabs[0];
+ let rightTab = tabs[tabs.length - 1];
+ let tabScreenX = draggedTab.boxObject.screenX;
+ let translateX = screenX - draggedTab._dragData.screenX;
+ if (!pinned)
+ translateX += this.mTabstrip.scrollPosition - draggedTab._dragData.scrollX;
+ let leftBound = leftTab.boxObject.screenX - tabScreenX;
+ let rightBound = (rightTab.boxObject.screenX + rightTab.boxObject.width) -
+ (tabScreenX + tabWidth);
+ translateX = Math.max(translateX, leftBound);
+ translateX = Math.min(translateX, rightBound);
+ draggedTab.style.transform = "translateX(" + translateX + "px)";
+
+ // Determine what tab we're dragging over.
+ // * Point of reference is the center of the dragged tab. If that
+ // point touches a background tab, the dragged tab would take that
+ // tab's position when dropped.
+ // * We're doing a binary search in order to reduce the amount of
+ // tabs we need to check.
+
+ let tabCenter = tabScreenX + translateX + tabWidth / 2;
+ let newIndex = -1;
+ let oldIndex = "animDropIndex" in draggedTab._dragData ?
+ draggedTab._dragData.animDropIndex : draggedTab._tPos;
+ let low = 0;
+ let high = tabs.length - 1;
+ while (low <= high) {
+ let mid = Math.floor((low + high) / 2);
+ if (tabs[mid] == draggedTab &&
+ ++mid > high)
+ break;
+ let boxObject = tabs[mid].boxObject;
+ let screenX = boxObject.screenX + getTabShift(tabs[mid], oldIndex);
+ if (screenX > tabCenter) {
+ high = mid - 1;
+ } else if (screenX + boxObject.width < tabCenter) {
+ low = mid + 1;
+ } else {
+ newIndex = tabs[mid]._tPos;
+ break;
+ }
+ }
+ if (newIndex >= oldIndex)
+ newIndex++;
+ if (newIndex < 0 || newIndex == oldIndex)
+ return;
+ draggedTab._dragData.animDropIndex = newIndex;
+
+ // Shift background tabs to leave a gap where the dragged tab
+ // would currently be dropped.
+
+ for (let tab of tabs) {
+ if (tab != draggedTab) {
+ let shift = getTabShift(tab, newIndex);
+ tab.style.transform = shift ? "translateX(" + shift + "px)" : "";
+ }
+ }
+
+ function getTabShift(tab, dropIndex) {
+ if (tab._tPos < draggedTab._tPos && tab._tPos >= dropIndex)
+ return rtl ? -tabWidth : tabWidth;
+ if (tab._tPos > draggedTab._tPos && tab._tPos < dropIndex)
+ return rtl ? tabWidth : -tabWidth;
+ return 0;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_finishAnimateTabMove">
+ <body><![CDATA[
+ if (this.getAttribute("movingtab") != "true")
+ return;
+
+ for (let tab of this.tabbrowser.visibleTabs)
+ tab.style.transform = "";
+
+ this.removeAttribute("movingtab");
+
+ this._handleTabSelect();
+ ]]></body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="aEvent"/>
+ <body><![CDATA[
+ switch (aEvent.type) {
+ case "load":
+ this.updateVisibility();
+ TabsInTitlebar.init();
+ break;
+ case "resize":
+ if (aEvent.target != window)
+ break;
+
+ TabsInTitlebar.updateAppearance();
+
+ var width = this.mTabstrip.boxObject.width;
+ if (width != this.mTabstripWidth) {
+ this.adjustTabstrip();
+ this._fillTrailingGap();
+ this._handleTabSelect();
+ this.mTabstripWidth = width;
+ }
+
+ this.tabbrowser.updateWindowResizers();
+ break;
+ case "mouseout":
+ // If the "related target" (the node to which the pointer went) is not
+ // a child of the current document, the mouse just left the window.
+ let relatedTarget = aEvent.relatedTarget;
+ if (relatedTarget && relatedTarget.ownerDocument == document)
+ break;
+ case "mousemove":
+ if (document.getElementById("tabContextMenu").state != "open")
+ this._unlockTabSizing();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <field name="_animateElement">
+ this.mTabstrip._scrollButtonDown;
+ </field>
+
+ <method name="_notifyBackgroundTab">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ if (aTab.pinned || aTab.hidden)
+ return;
+
+ var scrollRect = this.mTabstrip.scrollClientRect;
+ var tab = aTab.getBoundingClientRect();
+ this.mTabstrip._calcTabMargins(aTab);
+
+ // DOMRect left/right properties are immutable.
+ tab = {left: tab.left, right: tab.right};
+
+ // Is the new tab already completely visible?
+ if (scrollRect.left <= tab.left && tab.right <= scrollRect.right)
+ return;
+
+ if (this.mTabstrip.smoothScroll) {
+ let selected = !this.selectedItem.pinned &&
+ this.selectedItem.getBoundingClientRect();
+ if (selected) {
+ selected = {left: selected.left, right: selected.right};
+ // Need to take in to account the width of the left/right margins on tabs.
+ selected.left = selected.left + this.mTabstrip._tabMarginLeft;
+ selected.right = selected.right - this.mTabstrip._tabMarginRight;
+ }
+
+ tab.left += this.mTabstrip._tabMarginLeft;
+ tab.right -= this.mTabstrip._tabMarginRight;
+
+ // Can we make both the new tab and the selected tab completely visible?
+ if (!selected ||
+ Math.max(tab.right - selected.left, selected.right - tab.left) <=
+ scrollRect.width) {
+ this.mTabstrip.ensureElementIsVisible(aTab);
+ return;
+ }
+
+ this.mTabstrip._smoothScrollByPixels(this.mTabstrip._isRTLScrollbox ?
+ selected.right - scrollRect.right :
+ selected.left - scrollRect.left);
+ }
+
+ if (!this._animateElement.hasAttribute("notifybgtab")) {
+ this._animateElement.setAttribute("notifybgtab", "true");
+ setTimeout(function (ele) {
+ ele.removeAttribute("notifybgtab");
+ }, 150, this._animateElement);
+ }
+ ]]></body>
+ </method>
+
+ <method name="_getDragTargetTab">
+ <parameter name="event"/>
+ <parameter name="isLink"/>
+ <body><![CDATA[
+ let tab = event.target.localName == "tab" ? event.target : null;
+ if (tab && isLink) {
+ let boxObject = tab.boxObject;
+ if (event.screenX < boxObject.screenX + boxObject.width * .25 ||
+ event.screenX > boxObject.screenX + boxObject.width * .75)
+ return null;
+ }
+ return tab;
+ ]]></body>
+ </method>
+
+ <method name="_getDropIndex">
+ <parameter name="event"/>
+ <parameter name="isLink"/>
+ <body><![CDATA[
+ var tabs = this.childNodes;
+ var tab = this._getDragTargetTab(event, isLink);
+ if (window.getComputedStyle(this, null).direction == "ltr") {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+ if (event.screenX < tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+ return i;
+ } else {
+ for (let i = tab ? tab._tPos : 0; i < tabs.length; i++)
+ if (event.screenX > tabs[i].boxObject.screenX + tabs[i].boxObject.width / 2)
+ return i;
+ }
+ return tabs.length;
+ ]]></body>
+ </method>
+
+ <method name="_getDropEffectForTabDrag">
+ <parameter name="event"/>
+ <body><![CDATA[
+ var dt = event.dataTransfer;
+ if (dt.mozItemCount == 1) {
+ var types = dt.mozTypesAt(0);
+ // tabs are always added as the first type
+ if (types[0] == TAB_DROP_TYPE) {
+ let sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (sourceNode instanceof XULElement &&
+ sourceNode.localName == "tab" &&
+ sourceNode.ownerDocument.defaultView instanceof ChromeWindow &&
+ sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
+ sourceNode.ownerDocument.defaultView.gBrowser.tabContainer == sourceNode.parentNode) {
+ // Do not allow transfering a private tab to a non-private window
+ // and vice versa.
+ if (PrivateBrowsingUtils.isWindowPrivate(window) !=
+ PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerDocument.defaultView))
+ return "none";
+
+ if (window.gMultiProcessBrowser !=
+ sourceNode.ownerDocument.defaultView.gMultiProcessBrowser)
+ return "none";
+
+ return dt.dropEffect == "copy" ? "copy" : "move";
+ }
+ }
+ }
+
+ if (browserDragAndDrop.canDropLink(event)) {
+ return "link";
+ }
+ return "none";
+ ]]></body>
+ </method>
+
+ <method name="_handleNewTab">
+ <parameter name="tab"/>
+ <body><![CDATA[
+ if (tab.parentNode != this)
+ return;
+ tab._fullyOpen = true;
+
+ this.adjustTabstrip();
+
+ if (tab.getAttribute("selected") == "true") {
+ this._fillTrailingGap();
+ this._handleTabSelect();
+ } else {
+ this._notifyBackgroundTab(tab);
+ }
+
+ // XXXmano: this is a temporary workaround for bug 345399
+ // We need to manually update the scroll buttons disabled state
+ // if a tab was inserted to the overflow area or removed from it
+ // without any scrolling and when the tabbar has already
+ // overflowed.
+ this.mTabstrip._updateScrollButtonsDisabledState();
+
+ // Preload the next about:newtab if there isn't one already.
+ this.tabbrowser._createPreloadBrowser();
+ ]]></body>
+ </method>
+
+ <method name="_canAdvanceToTab">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ return !aTab.closing;
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleTabTelemetryStart">
+ <parameter name="aTab"/>
+ <parameter name="aURI"/>
+ <body>
+ <![CDATA[
+ // Animation-smoothness telemetry/logging
+ if (Services.telemetry.canRecordExtended || this._tabAnimationLoggingEnabled) {
+ if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) {
+ // Indicate newtab page animation where other tabs are unaffected
+ // (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute)
+ aTab._recordingTabOpenPlain = true;
+ }
+ aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .startFrameTimeRecording();
+ }
+
+ // Overall animation duration
+ aTab._animStartTime = Date.now();
+ ]]>
+ </body>
+ </method>
+
+ <method name="_handleTabTelemetryEnd">
+ <parameter name="aTab"/>
+ <body>
+ <![CDATA[
+ if (!aTab._animStartTime) {
+ return;
+ }
+
+ aTab._animStartTime = 0;
+
+ // Handle tab animation smoothness telemetry/logging of frame intervals and paint times
+ if (!("_recordingHandle" in aTab)) {
+ return;
+ }
+
+ let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .stopFrameTimeRecording(aTab._recordingHandle);
+ delete aTab._recordingHandle;
+ let frameCount = intervals.length;
+
+ if (this._tabAnimationLoggingEnabled) {
+ let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval):\n";
+ for (let i = 0; i < frameCount; i++) {
+ msg += Math.round(intervals[i]) + "\n";
+ }
+ Services.console.logStringMessage(msg);
+ }
+
+ // For telemetry, the first frame interval is not useful since it may represent an interval
+ // to a relatively old frame (prior to recording start). So we'll ignore it for the average.
+ if (frameCount > 1) {
+ let averageInterval = 0;
+ for (let i = 1; i < frameCount; i++) {
+ averageInterval += intervals[i];
+ }
+ averageInterval = averageInterval / (frameCount - 1);
+
+ Services.telemetry.getHistogramById("FX_TAB_ANIM_ANY_FRAME_INTERVAL_MS").add(averageInterval);
+
+ if (aTab._recordingTabOpenPlain) {
+ delete aTab._recordingTabOpenPlain;
+ // While we do have a telemetry probe NEWTAB_PAGE_ENABLED to monitor newtab preview, it'll be
+ // easier to overview the data without slicing by it. Hence the additional histograms with _PREVIEW.
+ let preview = this._browserNewtabpageEnabled ? "_PREVIEW" : "";
+ Services.telemetry.getHistogramById("FX_TAB_ANIM_OPEN" + preview + "_FRAME_INTERVAL_MS").add(averageInterval);
+ }
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- Deprecated stuff, implemented for backwards compatibility. -->
+ <property name="mAllTabsPopup" readonly="true"
+ onget="return document.getElementById('alltabs-popup');"/>
+ </implementation>
+
+ <handlers>
+ <handler event="TabSelect" action="this._handleTabSelect();"/>
+
+ <handler event="transitionend"><![CDATA[
+ if (event.propertyName != "max-width")
+ return;
+
+ var tab = event.target;
+
+ this._handleTabTelemetryEnd(tab);
+
+ if (tab.getAttribute("fadein") == "true") {
+ if (tab._fullyOpen)
+ this.adjustTabstrip();
+ else
+ this._handleNewTab(tab);
+ } else if (tab.closing) {
+ this.tabbrowser._endRemoveTab(tab);
+ }
+ ]]></handler>
+
+ <handler event="dblclick"><![CDATA[
+ // When the tabbar has an unified appearance with the titlebar
+ // and menubar, a double-click in it should have the same behavior
+ // as double-clicking the titlebar
+ if (TabsInTitlebar.enabled || this.parentNode._dragBindingAlive)
+ return;
+
+ if (event.button != 0 ||
+ event.originalTarget.localName != "box")
+ return;
+
+ // See hack note in the tabbrowser-close-tab-button binding
+ if (!this._blockDblClick)
+ BrowserOpenTab();
+
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="click" button="0" phase="capturing"><![CDATA[
+ /* Catches extra clicks meant for the in-tab close button.
+ * Placed here to avoid leaking (a temporary handler added from the
+ * in-tab close button binding would close over the tab and leak it
+ * until the handler itself was removed). (bug 897751)
+ *
+ * The only sequence in which a second click event (i.e. dblclik)
+ * can be dispatched on an in-tab close button is when it is shown
+ * after the first click (i.e. the first click event was dispatched
+ * on the tab). This happens when we show the close button only on
+ * the active tab. (bug 352021)
+ * The only sequence in which a third click event can be dispatched
+ * on an in-tab close button is when the tab was opened with a
+ * double click on the tabbar. (bug 378344)
+ * In both cases, it is most likely that the close button area has
+ * been accidentally clicked, therefore we do not close the tab.
+ *
+ * We don't want to ignore processing of more than one click event,
+ * though, since the user might actually be repeatedly clicking to
+ * close many tabs at once.
+ */
+ let target = event.originalTarget;
+ if (target.classList.contains('tab-close-button')) {
+ // We preemptively set this to allow the closing-multiple-tabs-
+ // in-a-row case.
+ if (this._blockDblClick) {
+ target._ignoredCloseButtonClicks = true;
+ } else if (event.detail > 1 && !target._ignoredCloseButtonClicks) {
+ target._ignoredCloseButtonClicks = true;
+ event.stopPropagation();
+ return;
+ } else {
+ // Reset the "ignored click" flag
+ target._ignoredCloseButtonClicks = false;
+ }
+ }
+
+ /* Protects from close-tab-button errant doubleclick:
+ * Since we're removing the event target, if the user
+ * double-clicks the button, the dblclick event will be dispatched
+ * with the tabbar as its event target (and explicit/originalTarget),
+ * which treats that as a mouse gesture for opening a new tab.
+ * In this context, we're manually blocking the dblclick event
+ * (see tabbrowser-close-tab-button dblclick handler).
+ */
+ if (this._blockDblClick) {
+ if (!("_clickedTabBarOnce" in this)) {
+ this._clickedTabBarOnce = true;
+ return;
+ }
+ delete this._clickedTabBarOnce;
+ this._blockDblClick = false;
+ }
+ ]]></handler>
+
+ <handler event="click"><![CDATA[
+ if (event.button != 1)
+ return;
+
+ if (event.target.localName == "tab") {
+ this.tabbrowser.removeTab(event.target, {animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+ } else if (event.originalTarget.localName == "box") {
+ // The user middleclicked an open space on the tabstrip. This could
+ // be because they intend to open a new tab, but it could also be
+ // because they just removed a tab and they now middleclicked on the
+ // resulting space while that tab is closing. In that case, we don't
+ // want to open a tab. So if we're removing one or more tabs, and
+ // the tab click is before the end of the last visible tab, we do
+ // nothing.
+ if (this.tabbrowser._removingTabs.length) {
+ let visibleTabs = this.tabbrowser.visibleTabs;
+ let ltr = (window.getComputedStyle(this, null).direction == "ltr");
+ let lastTab = visibleTabs[visibleTabs.length - 1];
+ let endOfTab = lastTab.getBoundingClientRect()[ltr ? "right" : "left"];
+ if ((ltr && event.clientX > endOfTab) ||
+ (!ltr && event.clientX < endOfTab)) {
+ BrowserOpenTab();
+ }
+ } else {
+ BrowserOpenTab();
+ }
+ } else {
+ return;
+ }
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="keydown" group="system"><![CDATA[
+ if (event.altKey || event.shiftKey)
+ return;
+
+ let wrongModifiers;
+ if (this.tabbrowser.AppConstants.platform == "macosx") {
+ wrongModifiers = !event.metaKey;
+ } else {
+ wrongModifiers = !event.ctrlKey || event.metaKey;
+ }
+
+ if (wrongModifiers)
+ return;
+
+ // Don't check if the event was already consumed because tab navigation
+ // should work always for better user experience.
+
+ switch (event.keyCode) {
+ case KeyEvent.DOM_VK_UP:
+ this.tabbrowser.moveTabBackward();
+ break;
+ case KeyEvent.DOM_VK_DOWN:
+ this.tabbrowser.moveTabForward();
+ break;
+ case KeyEvent.DOM_VK_RIGHT:
+ case KeyEvent.DOM_VK_LEFT:
+ this.tabbrowser.moveTabOver(event);
+ break;
+ case KeyEvent.DOM_VK_HOME:
+ this.tabbrowser.moveTabToStart();
+ break;
+ case KeyEvent.DOM_VK_END:
+ this.tabbrowser.moveTabToEnd();
+ break;
+ default:
+ // Consume the keydown event for the above keyboard
+ // shortcuts only.
+ return;
+ }
+ event.preventDefault();
+ ]]></handler>
+
+ <handler event="dragstart"><![CDATA[
+ var tab = this._getDragTargetTab(event, false);
+ if (!tab || this._isCustomizing)
+ return;
+
+ let dt = event.dataTransfer;
+ dt.mozSetDataAt(TAB_DROP_TYPE, tab, 0);
+ let browser = tab.linkedBrowser;
+
+ // We must not set text/x-moz-url or text/plain data here,
+ // otherwise trying to deatch the tab by dropping it on the desktop
+ // may result in an "internet shortcut"
+ dt.mozSetDataAt("text/x-moz-text-internal", browser.currentURI.spec, 0);
+
+ // Set the cursor to an arrow during tab drags.
+ dt.mozCursor = "default";
+
+ // Create a canvas to which we capture the current tab.
+ // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
+ // canvas size (in CSS pixels) to the window's backing resolution in order
+ // to get a full-resolution drag image for use on HiDPI displays.
+ let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
+ let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
+ let canvas = this._dndCanvas ? this._dndCanvas
+ : document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.mozOpaque = true;
+ canvas.width = 160 * scale;
+ canvas.height = 90 * scale;
+ let toDrag;
+ let dragImageOffset = -16;
+ if (gMultiProcessBrowser) {
+ var context = canvas.getContext('2d');
+ context.fillStyle = "white";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+ // Create a panel to use it in setDragImage
+ // which will tell xul to render a panel that follows
+ // the pointer while a dnd session is on.
+ if (!this._dndPanel) {
+ this._dndCanvas = canvas;
+ this._dndPanel = document.createElement("panel");
+ this._dndPanel.className = "dragfeedback-tab";
+ this._dndPanel.setAttribute("type", "drag");
+ let wrapper = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ wrapper.style.width = "160px";
+ wrapper.style.height = "90px";
+ wrapper.appendChild(canvas);
+ canvas.style.width = "100%";
+ canvas.style.height = "100%";
+ this._dndPanel.appendChild(wrapper);
+ document.documentElement.appendChild(this._dndPanel);
+ }
+ // PageThumb is async with e10s but that's fine
+ // since we can update the panel during the dnd.
+ PageThumbs.captureToCanvas(browser, canvas);
+ toDrag = this._dndPanel;
+ } else {
+ // For the non e10s case we can just use PageThumbs
+ // sync. No need for xul magic, the native dnd will
+ // be fine, so let's use the canvas for setDragImage.
+ PageThumbs.captureToCanvas(browser, canvas);
+ toDrag = canvas;
+ dragImageOffset = dragImageOffset * scale;
+ }
+ dt.setDragImage(toDrag, dragImageOffset, dragImageOffset);
+
+ // _dragData.offsetX/Y give the coordinates that the mouse should be
+ // positioned relative to the corner of the new window created upon
+ // dragend such that the mouse appears to have the same position
+ // relative to the corner of the dragged tab.
+ function clientX(ele) {
+ return ele.getBoundingClientRect().left;
+ }
+ let tabOffsetX = clientX(tab) - clientX(this);
+ tab._dragData = {
+ offsetX: event.screenX - window.screenX - tabOffsetX,
+ offsetY: event.screenY - window.screenY,
+ scrollX: this.mTabstrip.scrollPosition,
+ screenX: event.screenX
+ };
+
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragover"><![CDATA[
+ var effects = this._getDropEffectForTabDrag(event);
+
+ var ind = this._tabDropIndicator;
+ if (effects == "" || effects == "none") {
+ ind.collapsed = true;
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+
+ var tabStrip = this.mTabstrip;
+ var ltr = (window.getComputedStyle(this, null).direction == "ltr");
+
+ // autoscroll the tab strip if we drag over the scroll
+ // buttons, even if we aren't dragging a tab, but then
+ // return to avoid drawing the drop indicator
+ var pixelsToScroll = 0;
+ if (this.getAttribute("overflow") == "true") {
+ var targetAnonid = event.originalTarget.getAttribute("anonid");
+ switch (targetAnonid) {
+ case "scrollbutton-up":
+ pixelsToScroll = tabStrip.scrollIncrement * -1;
+ break;
+ case "scrollbutton-down":
+ pixelsToScroll = tabStrip.scrollIncrement;
+ break;
+ }
+ if (pixelsToScroll)
+ tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll);
+ }
+
+ if (effects == "move" &&
+ this == event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0).parentNode) {
+ ind.collapsed = true;
+ this._animateTabMove(event);
+ return;
+ }
+
+ this._finishAnimateTabMove();
+
+ if (effects == "link") {
+ let tab = this._getDragTargetTab(event, true);
+ if (tab) {
+ if (!this._dragTime)
+ this._dragTime = Date.now();
+ if (Date.now() >= this._dragTime + this._dragOverDelay)
+ this.selectedItem = tab;
+ ind.collapsed = true;
+ return;
+ }
+ }
+
+ var rect = tabStrip.getBoundingClientRect();
+ var newMargin;
+ if (pixelsToScroll) {
+ // if we are scrolling, put the drop indicator at the edge
+ // so that it doesn't jump while scrolling
+ let scrollRect = tabStrip.scrollClientRect;
+ let minMargin = scrollRect.left - rect.left;
+ let maxMargin = Math.min(minMargin + scrollRect.width,
+ scrollRect.right);
+ if (!ltr)
+ [minMargin, maxMargin] = [this.clientWidth - maxMargin,
+ this.clientWidth - minMargin];
+ newMargin = (pixelsToScroll > 0) ? maxMargin : minMargin;
+ }
+ else {
+ let newIndex = this._getDropIndex(event, effects == "link");
+ if (newIndex == this.childNodes.length) {
+ let tabRect = this.childNodes[newIndex-1].getBoundingClientRect();
+ if (ltr)
+ newMargin = tabRect.right - rect.left;
+ else
+ newMargin = rect.right - tabRect.left;
+ }
+ else {
+ let tabRect = this.childNodes[newIndex].getBoundingClientRect();
+ if (ltr)
+ newMargin = tabRect.left - rect.left;
+ else
+ newMargin = rect.right - tabRect.right;
+ }
+ }
+
+ ind.collapsed = false;
+
+ newMargin += ind.clientWidth / 2;
+ if (!ltr)
+ newMargin *= -1;
+
+ ind.style.transform = "translate(" + Math.round(newMargin) + "px)";
+ ind.style.marginInlineStart = (-ind.clientWidth) + "px";
+ ]]></handler>
+
+ <handler event="drop"><![CDATA[
+ var dt = event.dataTransfer;
+ var dropEffect = dt.dropEffect;
+ var draggedTab;
+ if (dt.mozTypesAt(0)[0] == TAB_DROP_TYPE) { // tab copy or move
+ draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ // not our drop then
+ if (!draggedTab)
+ return;
+ }
+
+ this._tabDropIndicator.collapsed = true;
+ event.stopPropagation();
+ if (draggedTab && dropEffect == "copy") {
+ // copy the dropped tab (wherever it's from)
+ let newIndex = this._getDropIndex(event, false);
+ let newTab = this.tabbrowser.duplicateTab(draggedTab);
+ this.tabbrowser.moveTabTo(newTab, newIndex);
+ if (draggedTab.parentNode != this || event.shiftKey)
+ this.selectedItem = newTab;
+ } else if (draggedTab && draggedTab.parentNode == this) {
+ this._finishAnimateTabMove();
+
+ // actually move the dragged tab
+ if ("animDropIndex" in draggedTab._dragData) {
+ let newIndex = draggedTab._dragData.animDropIndex;
+ if (newIndex > draggedTab._tPos)
+ newIndex--;
+ this.tabbrowser.moveTabTo(draggedTab, newIndex);
+ }
+ } else if (draggedTab) {
+ let newIndex = this._getDropIndex(event, false);
+ this.tabbrowser.adoptTab(draggedTab, newIndex, true);
+ } else {
+ // Pass true to disallow dropping javascript: or data: urls
+ let links;
+ try {
+ links = browserDragAndDrop.dropLinks(event, true);
+ } catch (ex) {}
+
+ if (!links || links.length === 0)
+ return;
+
+ let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
+
+ if (event.shiftKey)
+ inBackground = !inBackground;
+
+ let targetTab = this._getDragTargetTab(event, true);
+ let userContextId = this.selectedItem.getAttribute("usercontextid");
+ let replace = !!targetTab;
+ let newIndex = this._getDropIndex(event, true);
+ let urls = links.map(link => link.url);
+ this.tabbrowser.loadTabs(urls, {
+ inBackground,
+ replace,
+ allowThirdPartyFixup: true,
+ targetTab,
+ newIndex,
+ userContextId,
+ });
+ }
+
+ if (draggedTab) {
+ delete draggedTab._dragData;
+ }
+ ]]></handler>
+
+ <handler event="dragend"><![CDATA[
+ // Note: while this case is correctly handled here, this event
+ // isn't dispatched when the tab is moved within the tabstrip,
+ // see bug 460801.
+
+ this._finishAnimateTabMove();
+
+ var dt = event.dataTransfer;
+ var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
+ if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
+ delete draggedTab._dragData;
+ return;
+ }
+
+ // Disable detach within the browser toolbox
+ var eX = event.screenX;
+ var eY = event.screenY;
+ var wX = window.screenX;
+ // check if the drop point is horizontally within the window
+ if (eX > wX && eX < (wX + window.outerWidth)) {
+ let bo = this.mTabstrip.boxObject;
+ // also avoid detaching if the the tab was dropped too close to
+ // the tabbar (half a tab)
+ let endScreenY = bo.screenY + 1.5 * bo.height;
+ if (eY < endScreenY && eY > window.screenY)
+ return;
+ }
+
+ // screen.availLeft et. al. only check the screen that this window is on,
+ // but we want to look at the screen the tab is being dropped onto.
+ var screen = Cc["@mozilla.org/gfx/screenmanager;1"]
+ .getService(Ci.nsIScreenManager)
+ .screenForRect(eX, eY, 1, 1);
+ var fullX = {}, fullY = {}, fullWidth = {}, fullHeight = {};
+ var availX = {}, availY = {}, availWidth = {}, availHeight = {};
+ // get full screen rect and available rect, both in desktop pix
+ screen.GetRectDisplayPix(fullX, fullY, fullWidth, fullHeight);
+ screen.GetAvailRectDisplayPix(availX, availY, availWidth, availHeight);
+
+ // scale factor to convert desktop pixels to CSS px
+ var scaleFactor =
+ screen.contentsScaleFactor / screen.defaultCSSScaleFactor;
+ // synchronize CSS-px top-left coordinates with the screen's desktop-px
+ // coordinates, to ensure uniqueness across multiple screens
+ // (compare the equivalent adjustments in nsGlobalWindow::GetScreenXY()
+ // and related methods)
+ availX.value = (availX.value - fullX.value) * scaleFactor + fullX.value;
+ availY.value = (availY.value - fullY.value) * scaleFactor + fullY.value;
+ availWidth.value *= scaleFactor;
+ availHeight.value *= scaleFactor;
+
+ // ensure new window entirely within screen
+ var winWidth = Math.min(window.outerWidth, availWidth.value);
+ var winHeight = Math.min(window.outerHeight, availHeight.value);
+ var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, availX.value),
+ availX.value + availWidth.value - winWidth);
+ var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, availY.value),
+ availY.value + availHeight.value - winHeight);
+
+ delete draggedTab._dragData;
+
+ if (this.tabbrowser.tabs.length == 1) {
+ // resize _before_ move to ensure the window fits the new screen. if
+ // the window is too large for its screen, the window manager may do
+ // automatic repositioning.
+ window.resizeTo(winWidth, winHeight);
+ window.moveTo(left, top);
+ window.focus();
+ } else {
+ let props = { screenX: left, screenY: top };
+ if (this.tabbrowser.AppConstants.platform != "win") {
+ props.outerWidth = winWidth;
+ props.outerHeight = winHeight;
+ }
+ this.tabbrowser.replaceTabWithWindow(draggedTab, props);
+ }
+ event.stopPropagation();
+ ]]></handler>
+
+ <handler event="dragexit"><![CDATA[
+ this._dragTime = 0;
+
+ // This does not work at all (see bug 458613)
+ var target = event.relatedTarget;
+ while (target && target != this)
+ target = target.parentNode;
+ if (target)
+ return;
+
+ this._tabDropIndicator.collapsed = true;
+ event.stopPropagation();
+ ]]></handler>
+ </handlers>
+ </binding>
+
+ <!-- close-tab-button binding
+ This binding relies on the structure of the tabbrowser binding.
+ Therefore it should only be used as a child of the tab or the tabs
+ element (in both cases, when they are anonymous nodes of <tabbrowser>).
+ -->
+ <binding id="tabbrowser-close-tab-button"
+ extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton-image">
+ <handlers>
+ <handler event="click" button="0"><![CDATA[
+ var bindingParent = document.getBindingParent(this);
+ var tabContainer = bindingParent.parentNode;
+ tabContainer.tabbrowser.removeTab(bindingParent, {animate: true,
+ byMouse: event.mozInputSource == MouseEvent.MOZ_SOURCE_MOUSE});
+ // This enables double-click protection for the tab container
+ // (see tabbrowser-tabs 'click' handler).
+ tabContainer._blockDblClick = true;
+ ]]></handler>
+
+ <handler event="dblclick" button="0" phase="capturing">
+ // for the one-close-button case
+ event.stopPropagation();
+ </handler>
+
+ <handler event="dragstart">
+ event.stopPropagation();
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-tab" display="xul:hbox"
+ extends="chrome://global/content/bindings/tabbox.xml#tab">
+ <resources>
+ <stylesheet src="chrome://browser/content/tabbrowser.css"/>
+ </resources>
+
+ <content context="tabContextMenu">
+ <xul:stack class="tab-stack" flex="1">
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected,fadein"
+ class="tab-background">
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-start"/>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-middle"/>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected"
+ class="tab-background-end"/>
+ </xul:hbox>
+ <xul:hbox xbl:inherits="pinned,selected=visuallyselected,titlechanged,attention"
+ class="tab-content" align="center">
+ <xul:image xbl:inherits="fadein,pinned,busy,progress,selected=visuallyselected"
+ class="tab-throbber"
+ role="presentation"
+ layer="true" />
+ <xul:image xbl:inherits="src=image,loadingprincipal=iconLoadingPrincipal,fadein,pinned,selected=visuallyselected,busy,crashed,sharing"
+ anonid="tab-icon-image"
+ class="tab-icon-image"
+ validate="never"
+ role="presentation"/>
+ <xul:image xbl:inherits="sharing,selected=visuallyselected"
+ anonid="sharing-icon"
+ class="tab-sharing-icon-overlay"
+ role="presentation"/>
+ <xul:image xbl:inherits="crashed,busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected"
+ anonid="overlay-icon"
+ class="tab-icon-overlay"
+ role="presentation"/>
+ <xul:label flex="1"
+ xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected=visuallyselected,attention"
+ class="tab-text tab-label"
+ role="presentation"/>
+ <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected=visuallyselected"
+ anonid="soundplaying-icon"
+ class="tab-icon-sound"
+ role="presentation"/>
+ <xul:toolbarbutton anonid="close-button"
+ xbl:inherits="fadein,pinned,selected=visuallyselected"
+ class="tab-close-button close-icon"/>
+ </xul:hbox>
+ </xul:stack>
+ </content>
+
+ <implementation>
+ <constructor><![CDATA[
+ if (!("_lastAccessed" in this)) {
+ this.updateLastAccessed();
+ }
+ ]]></constructor>
+
+ <property name="_visuallySelected">
+ <setter>
+ <![CDATA[
+ if (val)
+ this.setAttribute("visuallyselected", "true");
+ else
+ this.removeAttribute("visuallyselected");
+ this.parentNode.tabbrowser._tabAttrModified(this, ["visuallyselected"]);
+
+ this._setPositionAttributes(val);
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="_selected">
+ <setter>
+ <![CDATA[
+ // in e10s we want to only pseudo-select a tab before its rendering is done, so that
+ // the rest of the system knows that the tab is selected, but we don't want to update its
+ // visual status to selected until after we receive confirmation that its content has painted.
+ if (val)
+ this.setAttribute("selected", "true");
+ else
+ this.removeAttribute("selected");
+
+ // If we're non-e10s we should update the visual selection as well at the same time,
+ // *or* if we're e10s and the visually selected tab isn't changing, in which case the
+ // tab switcher code won't run and update anything else (like the before- and after-
+ // selected attributes).
+ if (!gMultiProcessBrowser || (val && this.hasAttribute("visuallyselected"))) {
+ this._visuallySelected = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+
+ <property name="pinned" readonly="true">
+ <getter>
+ return this.getAttribute("pinned") == "true";
+ </getter>
+ </property>
+ <property name="hidden" readonly="true">
+ <getter>
+ return this.getAttribute("hidden") == "true";
+ </getter>
+ </property>
+ <property name="muted" readonly="true">
+ <getter>
+ return this.getAttribute("muted") == "true";
+ </getter>
+ </property>
+ <property name="blocked" readonly="true">
+ <getter>
+ return this.getAttribute("blocked") == "true";
+ </getter>
+ </property>
+ <!--
+ Describes how the tab ended up in this mute state. May be any of:
+
+ - undefined: The tabs mute state has never changed.
+ - null: The mute state was last changed through the UI.
+ - Any string: The ID was changed through an extension API. The string
+ must be the ID of the extension which changed it.
+ -->
+ <field name="muteReason">undefined</field>
+
+ <property name="userContextId" readonly="true">
+ <getter>
+ return this.hasAttribute("usercontextid")
+ ? parseInt(this.getAttribute("usercontextid"))
+ : 0;
+ </getter>
+ </property>
+
+ <property name="soundPlaying" readonly="true">
+ <getter>
+ return this.getAttribute("soundplaying") == "true";
+ </getter>
+ </property>
+
+ <property name="lastAccessed">
+ <getter>
+ return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
+ </getter>
+ </property>
+ <method name="updateLastAccessed">
+ <parameter name="aDate"/>
+ <body><![CDATA[
+ this._lastAccessed = this.selected ? Infinity : (aDate || Date.now());
+ ]]></body>
+ </method>
+
+ <field name="cachePosition">Infinity</field>
+
+ <field name="mOverCloseButton">false</field>
+ <property name="_overPlayingIcon" readonly="true">
+ <getter><![CDATA[
+ let iconVisible = this.hasAttribute("soundplaying") ||
+ this.hasAttribute("muted") ||
+ this.hasAttribute("blocked");
+ let soundPlayingIcon =
+ document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon");
+ let overlayIcon =
+ document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon");
+
+ return soundPlayingIcon && soundPlayingIcon.matches(":hover") ||
+ (overlayIcon && overlayIcon.matches(":hover") && iconVisible);
+ ]]></getter>
+ </property>
+ <field name="mCorrespondingMenuitem">null</field>
+
+ <!--
+ While it would make sense to track this in a field, the field will get nuked
+ once the node is gone from the DOM, which causes us to think the tab is not
+ closed, which causes us to make wrong decisions. So we use an expando instead.
+ <field name="closing">false</field>
+ -->
+
+ <method name="_mouseenter">
+ <body><![CDATA[
+ if (this.hidden || this.closing)
+ return;
+
+ let tabContainer = this.parentNode;
+ let visibleTabs = tabContainer.tabbrowser.visibleTabs;
+ let tabIndex = visibleTabs.indexOf(this);
+ if (tabIndex == 0) {
+ tabContainer._beforeHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex - 1];
+ if (!candidate.selected) {
+ tabContainer._beforeHoveredTab = candidate;
+ candidate.setAttribute("beforehovered", "true");
+ }
+ }
+
+ if (tabIndex == visibleTabs.length - 1) {
+ tabContainer._afterHoveredTab = null;
+ } else {
+ let candidate = visibleTabs[tabIndex + 1];
+ if (!candidate.selected) {
+ tabContainer._afterHoveredTab = candidate;
+ candidate.setAttribute("afterhovered", "true");
+ }
+ }
+
+ tabContainer._hoveredTab = this;
+ ]]></body>
+ </method>
+
+ <method name="_mouseleave">
+ <body><![CDATA[
+ let tabContainer = this.parentNode;
+ if (tabContainer._beforeHoveredTab) {
+ tabContainer._beforeHoveredTab.removeAttribute("beforehovered");
+ tabContainer._beforeHoveredTab = null;
+ }
+ if (tabContainer._afterHoveredTab) {
+ tabContainer._afterHoveredTab.removeAttribute("afterhovered");
+ tabContainer._afterHoveredTab = null;
+ }
+
+ tabContainer._hoveredTab = null;
+ ]]></body>
+ </method>
+
+ <method name="toggleMuteAudio">
+ <parameter name="aMuteReason"/>
+ <body>
+ <![CDATA[
+ let tabContainer = this.parentNode;
+ let browser = this.linkedBrowser;
+ let modifiedAttrs = [];
+ if (browser.audioBlocked) {
+ this.removeAttribute("blocked");
+ modifiedAttrs.push("blocked");
+
+ // We don't want sound icon flickering between "blocked", "none" and
+ // "sound-playing", here adding the "soundplaying" is to keep the
+ // transition smoothly.
+ if (!this.hasAttribute("soundplaying")) {
+ this.setAttribute("soundplaying", true);
+ modifiedAttrs.push("soundplaying");
+ }
+
+ browser.resumeMedia();
+ } else {
+ if (browser.audioMuted) {
+ browser.unmute();
+ this.removeAttribute("muted");
+ BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason);
+ } else {
+ browser.mute();
+ this.setAttribute("muted", "true");
+ BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason);
+ }
+ this.muteReason = aMuteReason || null;
+ modifiedAttrs.push("muted");
+ }
+ tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs);
+ ]]>
+ </body>
+ </method>
+
+ <method name="setUserContextId">
+ <parameter name="aUserContextId"/>
+ <body>
+ <![CDATA[
+ if (aUserContextId) {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.setAttribute("usercontextid", aUserContextId);
+ }
+ this.setAttribute("usercontextid", aUserContextId);
+ } else {
+ if (this.linkedBrowser) {
+ this.linkedBrowser.removeAttribute("usercontextid");
+ }
+ this.removeAttribute("usercontextid");
+ }
+
+ ContextualIdentityService.setTabStyle(this);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="mouseover"><![CDATA[
+ let anonid = event.originalTarget.getAttribute("anonid");
+ if (anonid == "close-button")
+ this.mOverCloseButton = true;
+
+ this._mouseenter();
+ ]]></handler>
+ <handler event="mouseout"><![CDATA[
+ let anonid = event.originalTarget.getAttribute("anonid");
+ if (anonid == "close-button")
+ this.mOverCloseButton = false;
+
+ this._mouseleave();
+ ]]></handler>
+ <handler event="dragstart" phase="capturing">
+ this.style.MozUserFocus = '';
+ </handler>
+ <handler event="mousedown" phase="capturing">
+ <![CDATA[
+ if (this.selected) {
+ this.style.MozUserFocus = 'ignore';
+ this.clientTop; // just using this to flush style updates
+ } else if (this.mOverCloseButton ||
+ this._overPlayingIcon) {
+ // Prevent tabbox.xml from selecting the tab.
+ event.stopPropagation();
+ }
+ ]]>
+ </handler>
+ <handler event="mouseup">
+ this.style.MozUserFocus = '';
+ </handler>
+ <handler event="click">
+ <![CDATA[
+ if (event.button != 0) {
+ return;
+ }
+
+ if (this._overPlayingIcon) {
+ this.toggleMuteAudio();
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <binding id="tabbrowser-alltabs-popup"
+ extends="chrome://global/content/bindings/popup.xml#popup">
+ <implementation implements="nsIDOMEventListener">
+ <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;
+ case "scroll":
+ this._updateTabsVisibilityStatus();
+ 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;
+
+ var tabstripBO = tabContainer.mTabstrip.scrollBoxObject;
+ for (var i = 0; i < this.childNodes.length; i++) {
+ let curTab = this.childNodes[i].tab;
+ if (!curTab) // "Undo close tab", menuseparator, or entries put here by addons.
+ continue;
+ let curTabBO = curTab.boxObject;
+ if (curTabBO.screenX >= tabstripBO.screenX &&
+ curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width)
+ this.childNodes[i].setAttribute("tabIsVisible", "true");
+ else
+ this.childNodes[i].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;
+
+ this.appendChild(menuItem);
+ ]]></body>
+ </method>
+
+ <method name="_setMenuitemAttributes">
+ <parameter name="aMenuitem"/>
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ aMenuitem.setAttribute("label", aTab.label);
+ aMenuitem.setAttribute("crop", aTab.getAttribute("crop"));
+
+ if (aTab.hasAttribute("busy")) {
+ aMenuitem.setAttribute("busy", aTab.getAttribute("busy"));
+ aMenuitem.removeAttribute("image");
+ } else {
+ aMenuitem.setAttribute("image", aTab.getAttribute("image"));
+ aMenuitem.removeAttribute("busy");
+ }
+
+ 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");
+ ]]></body>
+ </method>
+ </implementation>
+
+ <handlers>
+ <handler event="popupshowing">
+ <![CDATA[
+ if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
+ createUserContextMenu(event);
+ return;
+ }
+
+ let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+
+ if (event.target.getAttribute("anonid") == "newtab-popup" ||
+ event.target.id == "newtab-popup") {
+ createUserContextMenu(event);
+ } else {
+ document.getElementById("alltabs-popup-separator-1").hidden = !containersEnabled;
+ let containersTab = document.getElementById("alltabs_containersTab");
+
+ containersTab.hidden = !containersEnabled;
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ containersTab.setAttribute("disabled", "true");
+ }
+
+ document.getElementById("alltabs_undoCloseTab").disabled =
+ SessionStore.getClosedTabCount(window) == 0;
+
+ var tabcontainer = gBrowser.tabContainer;
+
+ // Listen for changes in the tab bar.
+ tabcontainer.addEventListener("TabAttrModified", this, false);
+ tabcontainer.addEventListener("TabClose", this, false);
+ tabcontainer.mTabstrip.addEventListener("scroll", this, false);
+
+ let tabs = gBrowser.visibleTabs;
+ for (var i = 0; i < tabs.length; i++) {
+ if (!tabs[i].pinned)
+ this._createTabMenuItem(tabs[i]);
+ }
+ this._updateTabsVisibilityStatus();
+ }
+ ]]></handler>
+
+ <handler event="popuphidden">
+ <![CDATA[
+ if (event.target.getAttribute("id") == "alltabs_containersMenuTab") {
+ return;
+ }
+
+ // clear out the menu popup and remove the listeners
+ for (let i = this.childNodes.length - 1; i > 0; i--) {
+ let menuItem = this.childNodes[i];
+ if (menuItem.tab) {
+ menuItem.tab.mCorrespondingMenuitem = null;
+ this.removeChild(menuItem);
+ }
+ if (menuItem.hasAttribute("usercontextid")) {
+ this.removeChild(menuItem);
+ }
+ }
+ var tabcontainer = gBrowser.tabContainer;
+ tabcontainer.mTabstrip.removeEventListener("scroll", this, false);
+ tabcontainer.removeEventListener("TabAttrModified", this, false);
+ tabcontainer.removeEventListener("TabClose", this, false);
+ ]]></handler>
+
+ <handler event="DOMMenuItemActive">
+ <![CDATA[
+ var tab = event.target.tab;
+ if (tab) {
+ let overLink = tab.linkedBrowser.currentURI.spec;
+ if (overLink == "about:blank")
+ overLink = "";
+ XULBrowserWindow.setOverLink(overLink, null);
+ }
+ ]]></handler>
+
+ <handler event="DOMMenuItemInactive">
+ <![CDATA[
+ XULBrowserWindow.setOverLink("", null);
+ ]]></handler>
+
+ <handler event="command"><![CDATA[
+ if (event.target.tab)
+ gBrowser.selectedTab = event.target.tab;
+ ]]></handler>
+
+ </handlers>
+ </binding>
+
+ <binding id="statuspanel" display="xul:hbox">
+ <content>
+ <xul:hbox class="statuspanel-inner">
+ <xul:label class="statuspanel-label"
+ role="status"
+ aria-live="off"
+ xbl:inherits="value=label,crop,mirror"
+ flex="1"
+ crop="end"/>
+ </xul:hbox>
+ </content>
+
+ <implementation implements="nsIDOMEventListener">
+ <constructor><![CDATA[
+ window.addEventListener("resize", this, false);
+ ]]></constructor>
+
+ <destructor><![CDATA[
+ window.removeEventListener("resize", this, false);
+ MousePosTracker.removeListener(this);
+ ]]></destructor>
+
+ <property name="label">
+ <setter><![CDATA[
+ if (!this.label) {
+ this.removeAttribute("mirror");
+ this.removeAttribute("sizelimit");
+ }
+
+ this.style.minWidth = this.getAttribute("type") == "status" &&
+ this.getAttribute("previoustype") == "status"
+ ? getComputedStyle(this).width : "";
+
+ if (val) {
+ this.setAttribute("label", val);
+ this.removeAttribute("inactive");
+ this._calcMouseTargetRect();
+ MousePosTracker.addListener(this);
+ } else {
+ this.setAttribute("inactive", "true");
+ MousePosTracker.removeListener(this);
+ }
+
+ return val;
+ ]]></setter>
+ <getter>
+ return this.hasAttribute("inactive") ? "" : this.getAttribute("label");
+ </getter>
+ </property>
+
+ <method name="getMouseTargetRect">
+ <body><![CDATA[
+ return this._mouseTargetRect;
+ ]]></body>
+ </method>
+
+ <method name="onMouseEnter">
+ <body>
+ this._mirror();
+ </body>
+ </method>
+
+ <method name="onMouseLeave">
+ <body>
+ this._mirror();
+ </body>
+ </method>
+
+ <method name="handleEvent">
+ <parameter name="event"/>
+ <body><![CDATA[
+ if (!this.label)
+ return;
+
+ switch (event.type) {
+ case "resize":
+ this._calcMouseTargetRect();
+ break;
+ }
+ ]]></body>
+ </method>
+
+ <method name="_calcMouseTargetRect">
+ <body><![CDATA[
+ let container = this.parentNode;
+ let alignRight = (getComputedStyle(container).direction == "rtl");
+ let panelRect = this.getBoundingClientRect();
+ let containerRect = container.getBoundingClientRect();
+
+ this._mouseTargetRect = {
+ top: panelRect.top,
+ bottom: panelRect.bottom,
+ left: alignRight ? containerRect.right - panelRect.width : containerRect.left,
+ right: alignRight ? containerRect.right : containerRect.left + panelRect.width
+ };
+ ]]></body>
+ </method>
+
+ <method name="_mirror">
+ <body>
+ if (this.hasAttribute("mirror"))
+ this.removeAttribute("mirror");
+ else
+ this.setAttribute("mirror", "true");
+
+ if (!this.hasAttribute("sizelimit")) {
+ this.setAttribute("sizelimit", "true");
+ this._calcMouseTargetRect();
+ }
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-tabpanels"
+ extends="chrome://global/content/bindings/tabbox.xml#tabpanels">
+ <implementation>
+ <field name="_selectedIndex">0</field>
+
+ <property name="selectedIndex">
+ <getter>
+ <![CDATA[
+ return this._selectedIndex;
+ ]]>
+ </getter>
+
+ <setter>
+ <![CDATA[
+ if (val < 0 || val >= this.childNodes.length)
+ return val;
+
+ let toTab = this.getRelatedElement(this.childNodes[val]);
+
+ gBrowser._getSwitcher().requestTab(toTab);
+
+ var panel = this._selectedPanel;
+ var newPanel = this.childNodes[val];
+ this._selectedPanel = newPanel;
+ if (this._selectedPanel != panel) {
+ var event = document.createEvent("Events");
+ event.initEvent("select", true, true);
+ this.dispatchEvent(event);
+
+ this._selectedIndex = val;
+ }
+
+ return val;
+ ]]>
+ </setter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-browser"
+ extends="chrome://global/content/bindings/browser.xml#browser">
+ <implementation>
+ <field name="tabModalPromptBox">null</field>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ var params = arguments[1];
+ if (typeof(params) == "number") {
+ params = {
+ flags: aFlags,
+ referrerURI: aReferrerURI,
+ charset: aCharset,
+ postData: aPostData,
+ };
+ }
+ _loadURIWithFlags(this, aURI, params);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="tabbrowser-remote-browser"
+ extends="chrome://global/content/bindings/remote-browser.xml#remote-browser">
+ <implementation>
+ <field name="tabModalPromptBox">null</field>
+
+ <!-- throws exception for unknown schemes -->
+ <method name="loadURIWithFlags">
+ <parameter name="aURI"/>
+ <parameter name="aFlags"/>
+ <parameter name="aReferrerURI"/>
+ <parameter name="aCharset"/>
+ <parameter name="aPostData"/>
+ <body>
+ <![CDATA[
+ var params = arguments[1];
+ if (typeof(params) == "number") {
+ params = {
+ flags: aFlags,
+ referrerURI: aReferrerURI,
+ charset: aCharset,
+ postData: aPostData,
+ };
+ }
+ _loadURIWithFlags(this, aURI, params);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>