diff options
Diffstat (limited to 'application/basilisk/base/content/tabbrowser.xml')
-rw-r--r-- | application/basilisk/base/content/tabbrowser.xml | 7631 |
1 files changed, 7631 insertions, 0 deletions
diff --git a/application/basilisk/base/content/tabbrowser.xml b/application/basilisk/base/content/tabbrowser.xml new file mode 100644 index 000000000..695e14c16 --- /dev/null +++ b/application/basilisk/base/content/tabbrowser.xml @@ -0,0 +1,7631 @@ +<?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" message="true" messagemanagergroup="browsers" + primary="true" + 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="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="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"> +#ifdef XP_MACOSX + true +#else + false +#endif + </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="_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() { + delete this.mTab; + delete this.mBrowser; + delete this.mTabBrowser; + }, + + _callProgressListeners() { + Array.unshift(arguments, this.mBrowser); + return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments); + }, + + _shouldShowProgress(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(aWebProgress, aLocation) { + if (!this.mBlank || !aWebProgress.isTopLevel) { + return false; + } + + let location = aLocation ? aLocation.spec : ""; + return location == "about:blank"; + }, + + onProgressChange(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(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + return this.onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, + aMaxTotalProgress); + }, + + onStateChange(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(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(aWebProgress, aRequest, aStatus, aMessage) { + if (this.mBlank) + return; + + this._callProgressListeners("onStatusChange", + [aWebProgress, aRequest, aStatus, aMessage]); + + this.mMessage = aMessage; + }, + + onSecurityChange(aWebProgress, aRequest, aState) { + this._callProgressListeners("onSecurityChange", + [aWebProgress, aRequest, aState]); + }, + + onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { + return this._callProgressListeners("onRefreshAttempted", + [aWebProgress, aURI, aDelay, aSameURI]); + }, + + QueryInterface(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.removeAttribute("primary"); + oldBrowser.docShellIsActive = false; + newBrowser.setAttribute("primary", "true"); + 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"); + this._tabAttrModified(aTab, ["label"]); + ]]> + </body> + </method> + + <method name="setTabTitle"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + 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. */ } + } 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) + return false; + + aTab.label = title; + this._tabAttrModified(aTab, ["label"]); + + 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"/> + <parameter name="aIsPrerendered"/> + <body> + <![CDATA[ + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aAllowMixedContent; + var aSkipAnimation; + var aForceNotRemote; + var aPreferredRemoteType; + 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]; + aTriggeringPrincipal = params.triggeringPrincipal + 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; + aPreferredRemoteType = params.preferredRemoteType; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aRelatedBrowser = params.relatedBrowser; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + aIsPrerendered = params.isPrerendered; + } + + var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + var owner = bgLoad ? null : this.selectedTab; + + var tab = this.addTab(aURI, { + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + fromExternal: aFromExternal, + relatedToCurrent: aRelatedToCurrent, + skipAnimation: aSkipAnimation, + allowMixedContent: aAllowMixedContent, + forceNotRemote: aForceNotRemote, + preferredRemoteType: aPreferredRemoteType, + noReferrer: aNoReferrer, + userContextId: aUserContextId, + originPrincipal: aOriginPrincipal, + relatedBrowser: aRelatedBrowser, + opener: aOpener, + isPrerendered: aIsPrerendered }); + 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="aOptions"/> + <body> + <![CDATA[ + aOptions = aOptions || {}; + let isRemote = aBrowser.getAttribute("remote") == "true"; + + if (!gMultiProcessBrowser && aShouldBeRemote) { + throw new Error("Cannot switch to remote browser in a window " + + "without the remote tabs load context."); + } + + // Default values for remoteType + if (!aOptions.remoteType) { + aOptions.remoteType = aShouldBeRemote ? E10SUtils.DEFAULT_REMOTE_TYPE : E10SUtils.NOT_REMOTE; + } + + // 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 (aOptions.opener) { + if (aShouldBeRemote) { + throw new Error("Cannot set an opener on a browser which should be remote!"); + } + if (!isRemote && aBrowser.contentWindow.opener != aOptions.opener) { + throw new Error("Cannot change opener on an already non-remote browser!"); + } + } + + // Abort if we're not going to change anything + if (isRemote == aShouldBeRemote && !aOptions.newFrameloader && !aOptions.freshProcess && + (!isRemote || aBrowser.getAttribute("remoteType") == aOptions.remoteType) && + !aBrowser.frameLoader.isFreshProcess) { + 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); + if (aShouldBeRemote) { + aBrowser.setAttribute("remote", "true"); + aBrowser.setAttribute("remoteType", aOptions.remoteType); + } else { + aBrowser.setAttribute("remote", "false"); + aBrowser.removeAttribute("remoteType"); + } + + // NB: This works with the hack in the browser constructor that + // turns this normal property into a field. + aBrowser.relatedBrowser = relatedBrowser; + + if (aOptions.opener) { + // Set the opener window on the browser, such that when the frame + // loader is created the opener is set correctly. + aBrowser.presetOpenerWindow(aOptions.opener); + } + + // Set the freshProcess attribute so that the frameloader knows to + // create a new process + if (aOptions.freshProcess) { + 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="updateBrowserRemotenessByURL"> + <parameter name="aBrowser"/> + <parameter name="aURL"/> + <parameter name="aOptions"/> + <body> + <![CDATA[ + aOptions = aOptions || {}; + + if (!gMultiProcessBrowser) + return this.updateBrowserRemoteness(aBrowser, false); + + // If this URL can't load in the current browser then flip it to the + // correct type. + let currentRemoteType = aBrowser.remoteType; + aOptions.remoteType = + E10SUtils.getRemoteTypeForURI(aURL, gMultiProcessBrowser, + currentRemoteType); + if (currentRemoteType != aOptions.remoteType || + aOptions.freshProcess || aOptions.newFrameloader || + aBrowser.frameLoader.isFreshProcess) { + let remote = aOptions.remoteType != E10SUtils.NOT_REMOTE; + return this.updateBrowserRemoteness(aBrowser, remote, aOptions); + } + + 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 remoteType = + E10SUtils.getRemoteTypeForURI(BROWSER_NEW_TAB_URL, + gMultiProcessBrowser); + let browser = this._createBrowser({isPreloadBrowser: true, remoteType}); + this._preloadedBrowser = browser; + + let notificationbox = this.getNotificationBox(browser); + this.mPanelContainer.appendChild(notificationbox); + + if (remoteType != E10SUtils.NOT_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, remoteType, isPreloadBrowser, + // uriIsAboutBlank, permanentKey, isPrerendered + + 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"); + b.setAttribute("message", "true"); + b.setAttribute("messagemanagergroup", "browsers"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + + if (aParams.isPrerendered) { + b.setAttribute("prerendered", "true"); + } + + if (aParams.userContextId) { + b.setAttribute("usercontextid", aParams.userContextId); + } + + // remote parameter used by some addons, use default in this case. + if (aParams.remote && !aParams.remoteType) { + aParams.remoteType = E10SUtils.DEFAULT_REMOTE_TYPE; + } + + if (aParams.remoteType) { + b.setAttribute("remoteType", aParams.remoteType); + b.setAttribute("remote", "true"); + } + + if (aParams.opener) { + if (aParams.remoteType) { + throw new Exception("Cannot set opener window on a remote browser!"); + } + b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aParams.opener); + } + + 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, preferredRemoteType, userContextId, isPrerendered + + let uriIsAboutBlank = !aURI || aURI == "about:blank"; + + let remoteType = + aParams.forceNotRemote ? E10SUtils.NOT_REMOTE + : E10SUtils.getRemoteTypeForURI(aURI, gMultiProcessBrowser, + aParams.preferredRemoteType); + + 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, + remoteType, + uriIsAboutBlank, + userContextId: aParams.userContextId, + relatedBrowser: aParams.relatedBrowser, + opener: aParams.opener, + isPrerendered: aParams.isPrerendered}); + } + + 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 (remoteType == E10SUtils.NOT_REMOTE) { + this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser); + } + + var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: {} }); + aTab.dispatchEvent(evt); + + return { usingPreloadedContent }; + ]]> + </body> + </method> + + <method name="addTab"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <parameter name="aOwner"/> + <parameter name="aAllowThirdPartyFixup"/> + <parameter name="aIsPrerendered"/> + <body> + <![CDATA[ + "use strict"; + + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aSkipAnimation; + var aAllowMixedContent; + var aForceNotRemote; + var aPreferredRemoteType; + var aNoReferrer; + var aUserContextId; + var aEventDetail; + var aRelatedBrowser; + var aOriginPrincipal; + var aDisallowInheritPrincipal; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + 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; + aPreferredRemoteType = params.preferredRemoteType; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aEventDetail = params.eventDetail; + aRelatedBrowser = params.relatedBrowser; + aOriginPrincipal = params.originPrincipal; + aDisallowInheritPrincipal = params.disallowInheritPrincipal; + aOpener = params.opener; + aIsPrerendered = params.isPrerendered; + } + + // 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"; + + 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 (aIsPrerendered) { + t.setAttribute("hidden", "true"); + } + + // Related tab inherits current tab's user context unless a different + // usercontextid is specified + if (aUserContextId == null && + (aRelatedToCurrent == null ? aReferrerURI : aRelatedToCurrent)) { + aUserContextId = this.mCurrentTab.getAttribute("usercontextid") || 0; + } + + if (aUserContextId) { + t.setAttribute("usercontextid", aUserContextId); + ContextualIdentityService.setTabStyle(t); + } + + 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(); + + // If URI is about:blank and we don't have a preferred remote type, + // then we need to use the referrer, if we have one, to get the + // correct remote type for the new tab. + if (uriIsAboutBlank && !aPreferredRemoteType && aReferrerURI) { + aPreferredRemoteType = + E10SUtils.getRemoteTypeForURI(aReferrerURI.spec, + gMultiProcessBrowser); + } + + // 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, + preferredRemoteType: aPreferredRemoteType, + userContextId: aUserContextId, + relatedBrowser: aRelatedBrowser, + opener: aOpener, + isPrerendered: aIsPrerendered, + }; + 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) { + 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 || aDisallowInheritPrincipal)) { + // 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; + if (aDisallowInheritPrincipal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + try { + b.loadURIWithFlags(aURI, { + flags, + triggeringPrincipal: aTriggeringPrincipal, + 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.removeAttribute("primary"); + + // 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); + } + + SitePermissions.copyTemporaryPermissions(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, Ci.nsIBrowser.SWAP_DEFAULT, stateFlags); + } + + // Unregister the previously opened URI + if (otherBrowser.registeredOpenURI) { + this._unifiedComplete.unregisterOpenPage(otherBrowser.registeredOpenURI, + otherBrowser.getAttribute("usercontextid") || 0); + delete otherBrowser.registeredOpenURI; + } + + // 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="swapBrowsers"> + <parameter name="aOurTab"/> + <parameter name="aOtherTab"/> + <parameter name="aFlags"/> + <body> + <![CDATA[ + let otherBrowser = aOtherTab.linkedBrowser; + let otherTabBrowser = otherBrowser.getTabBrowser(); + + // We aren't closing the other tab so, we also need to swap its tablisteners. + let filter = otherTabBrowser._tabFilters.get(aOtherTab); + let tabListener = otherTabBrowser._tabListeners.get(aOtherTab); + otherBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(tabListener); + + // Perform the docshell swap through the common mechanism. + this._swapBrowserDocShells(aOurTab, otherBrowser, aFlags); + + // Restore the listeners for the swapped in tab. + tabListener = otherTabBrowser.mTabProgressListener(aOtherTab, otherBrowser, false, false); + otherTabBrowser._tabListeners.set(aOtherTab, tabListener); + + const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL; + filter.addProgressListener(tabListener, notifyAll); + otherBrowser.webProgress.addProgressListener(filter, notifyAll); + ]]> + </body> + </method> + + <method name="_swapBrowserDocShells"> + <parameter name="aOurTab"/> + <parameter name="aOtherBrowser"/> + <parameter name="aFlags"/> + <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); + } + + if (!(aFlags & Ci.nsIBrowser.SWAP_KEEP_PERMANENT_KEY)) { + // 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[ + // Swap the registeredOpenURI properties of the two browsers + let tmp = aOurBrowser.registeredOpenURI; + delete aOurBrowser.registeredOpenURI; + if (aOtherBrowser.registeredOpenURI) { + aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; + delete aOtherBrowser.registeredOpenURI; + } + if (tmp) { + aOtherBrowser.registeredOpenURI = tmp; + } + ]]> + </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[ + let browser = this.getBrowserForTab(aTab); + // Reset temporary permissions on the current tab. This is done here + // because we only want to reset permissions on user reload. + SitePermissions.clearTemporaryPermissions(browser); + browser.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[ +#ifndef E10S_TESTING_ONLY + throw "This method is intended only for e10s testing!"; +#endif + 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(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(timer) { + timer.cancel(); + }, + + getTabState(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(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() { + 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() { + 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() { + 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("primary", "true"); + + 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.removeAttribute("primary"); + } + + 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() { + // 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(cond) { + if (!cond) { + dump("Assertion failure\n" + Error().stack); + + // Don't break a user's browser if an assertion fails. +#ifdef DEBUG + throw new Error("Assertion failure"); +#endif + } + }, + + // We've decided to try to load requestedTab. + loadRequestedTab() { + 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() { + 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() { + // 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() { + 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() { + this.logState("onLoadTimeout"); + this.preActions(); + this.loadTimer = null; + this.loadingTab = null; + this.postActions(); + }, + + // Fires when the layers become available for a tab. + onLayersReady(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() { + this.maybeVisibleTabs.clear(); + this.maybeFinishTabSwitch(); + }, + + // Called when we're done clearing the layers for a tab. + onLayersCleared(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(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(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(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(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() { + 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() { + 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() { + 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() { + 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(marker) { + if (Services.profiler) { + Services.profiler.AddMarker(marker); + } + }, + + /* + * Debug related logging for switcher. + */ + + _useDumpForLogging: false, + _logInit: false, + + logging() { + 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(tab) { + if (tab) { + return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")"; + } + return "null"; + }, + + log(s) { + if (!this.logging()) + return; + if (this._useDumpForLogging) { + dump(s + "\n"); + } else { + Services.console.logStringMessage(s); + } + }, + + logState(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; + } + } + +#ifndef XP_MACOSX + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + !this.mCurrentTab.pinned) { + this.removeCurrentTab({animate: true}); + aEvent.preventDefault(); + } +#endif + ]]></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; + +#ifdef XP_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(); + } +#endif + ]]></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 { +#ifdef E10S_TESTING_ONLY + label = tab.getAttribute("label") + (tab.linkedBrowser && tab.linkedBrowser.isRemoteBrowser ? " - e10s" : ""); +#else + label = tab.getAttribute("label"); +#endif + if (tab.userContextId) { + label = this.mStringBundle.getFormattedString("tabs.containers.tooltip", [label, ContextualIdentityService.getUserContextLabel(tab.userContextId)]); + } + } + + 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, + editFlags: data.editFlags, + spellInfo, + principal: data.principal, + customMenuItems: data.customMenuItems, + addonInfo: data.addonInfo, + 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; + } + + case "Prerender:Request": { + let sendCancelPrerendering = () => { + browser.frameloader.messageManager. + sendAsyncMessage("Prerender:Canceled", { id: data.id }); + }; + + let tab = this.getTabForBrowser(browser); + if (!tab) { + // No tab? + sendCancelPrerendering(); + break; + } + + if (tab.hidden) { + // Skip prerender on hidden tab. + sendCancelPrerendering(); + break; + } + + if (browser.canGoForward) { + // Skip prerender on history navigation as we don't support it + // yet. Remove this check once bug 1323650 is implemented. + sendCancelPrerendering(); + break; + } + + if (!data.href) { + // If we don't have data.href, loadOneTab will load about:blank + // which is meaningless for prerendering. + sendCancelPrerendering(); + break; + } + + let groupedSHistory = browser.frameLoader.ensureGroupedSHistory(); + + let newTab = this.loadOneTab(data.href, { + referrerURI: (data.referrer ? makeURI(data.referrer) : null), + referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_UNSET, + postData: null, + allowThirdPartyFixup: true, + relatedToCurrent: true, + isPrerendered: true, + }); + let partialSHistory = newTab.linkedBrowser.frameLoader.partialSHistory; + groupedSHistory.addPrerenderingPartialSHistory(partialSHistory, data.id); + break; + } + + case "Prerender:Cancel": { + let groupedSHistory = browser.frameLoader.groupedSHistory; + if (groupedSHistory) { + groupedSHistory.cancelPrerendering(data.id); + } + break; + } + + case "Prerender:Swap": { + let frameloader = browser.frameLoader; + let groupedSHistory = browser.frameLoader.groupedSHistory; + if (groupedSHistory) { + groupedSHistory.activatePrerendering(data.id).then( + () => frameloader.messageManager.sendAsyncMessage("Prerender:Swapped", data), + () => frameloader.messageManager.sendAsyncMessage("Prerender:Canceled", data), + ); + } + break; + } + + } + return undefined; + ]]></body> + </method> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body><![CDATA[ + switch (aTopic) { + case "contextual-identity-updated": { + for (let tab of this.tabs) { + if (tab.getAttribute("usercontextid") == aData) { + ContextualIdentityService.setTabStyle(tab); + } + } + 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, "contextual-identity-updated", 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); +#ifdef XP_MACOSX + els.addSystemEventListener(document, "keypress", this, false); +#endif + window.addEventListener("sizemodechange", this); + + 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; + + // 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 messageManager = window.getGroupMessageManager("browsers"); + + let remote = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .useRemoteTabs; + if (remote) { + messageManager.addMessageListener("DOMTitleChanged", this); + messageManager.addMessageListener("DOMWindowClose", this); + window.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); + + // Add listeners for prerender messages + messageManager.addMessageListener("Prerender:Request", this); + messageManager.addMessageListener("Prerender:Cancel", this); + messageManager.addMessageListener("Prerender:Swap", 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, "contextual-identity-updated"); + + 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); +#ifdef XP_MACOSX + els.removeSystemEventListener(document, "keypress", this, false); +#endif + window.removeEventListener("sizemodechange", this); + + if (gMultiProcessBrowser) { + let messageManager = window.getGroupMessageManager("browsers"); + messageManager.removeMessageListener("DOMTitleChanged", this); + window.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(attr, attrValue) { + if (attr == "anonid" && attrValue == "tabContextMenu") + return [this.self.tabContextMenu]; + return []; + }, + // Also support adding event listeners (forward to the tab container) + addEventListener(a, b, c) { this.self.tabContainer.addEventListener(a, b, c); }, + removeEventListener(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); + + // Focus window for beforeunload dialog so it is seen but don't + // steal focus from other applications. + if (event.detail && + event.detail.tabPrompt && + event.detail.inPermitUnload && + Services.focus.activeWindow) + window.focus(); + + // Don't need to act if the tab is already selected: + if (tabForEvent.selected) + return; + + // We always switch tabs for beforeunload tab-modal prompts. + if (event.detail && + event.detail.tabPrompt && + !event.detail.inPermitUnload) { + 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 permissions/origins dictate so, 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"]); + tab.startMediaBlockTimer(); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("blocked")) { + tab.removeAttribute("blocked"); + this._tabAttrModified(tab, ["blocked"]); + let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED"); + hist.add(2 /* unblockByVisitingTab */); + tab.finishMediaBlockTimer(); + } + ]]> + </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.target != this) + return; + + 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.target != this) + return; + + 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("onerror", "this.removeAttribute('image');"); + + window.addEventListener("resize", this); + window.addEventListener("load", this); + + try { + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled"); + } catch (ex) { + this._tabAnimationLoggingEnabled = false; + } + this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); + Services.prefs.addObserver("privacy.userContext", this, false); + this.observe(null, "nsPref:changed", "privacy.userContext.enabled"); + ]]> + </constructor> + + <destructor> + <![CDATA[ + Services.prefs.removeObserver("privacy.userContext", 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 has to deal with changes in + // privacy.userContext.enabled and + // privacy.userContext.longPressBehavior. + let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled") + && !PrivateBrowsingUtils.isWindowPrivate(window); + + // This pref won't change so often, so just recreate the menu. + let longPressBehavior = Services.prefs.getIntPref("privacy.userContext.longPressBehavior"); + + // If longPressBehavior pref is set to 0 (or any invalid value) + // long press menu is disabled. + if (containersEnabled && (longPressBehavior <= 0 || longPressBehavior > 2)) { + containersEnabled = false; + } + + const newTab = document.getElementById("new-tab-button"); + const newTab2 = document.getAnonymousElementByAttribute(this, "anonid", "tabs-newtab-button") + + for (let parent of [newTab, newTab2]) { + if (!parent) + continue; + + gClickAndHoldListenersOnElement.remove(parent); + parent.removeAttribute("type"); + if (parent.firstChild) { + parent.firstChild.remove(); + } + + if (containersEnabled) { + 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); + + // longPressBehavior == 2 means that the menu is shown after X + // millisecs. Otherwise, with 1, the menu is open immediatelly. + if (longPressBehavior == 2) { + gClickAndHoldListenersOnElement.add(parent); + } + + parent.setAttribute("type", "menu"); + } + } + + 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); + window.addEventListener("mouseout", this); + } + ]]></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); + window.addEventListener("mouseout", this); + ]]></body> + </method> + + <method name="_unlockTabSizing"> + <body><![CDATA[ + this.tabbrowser.removeEventListener("mousemove", this); + window.removeEventListener("mouseout", this); + + 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; + 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; + } + 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; +#ifdef XP_MACOSX + wrongModifiers = !event.metaKey; +#else + wrongModifiers = !event.ctrlKey || event.metaKey; +#endif + + 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"; + + // Set the tab as the source of the drag, which ensures we have a stable + // node to deliver the `dragend` event. See bug 1345473. + dt.addElement(tab); + + // 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; + if (!canvas) { + this._dndCanvas = canvas = + document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.style.width = "100%"; + canvas.style.height = "100%"; + canvas.mozOpaque = true; + } + + canvas.width = 160 * scale; + canvas.height = 90 * scale; + let toDrag = canvas; + let dragImageOffset = -16; + if (gMultiProcessBrowser) { + var context = canvas.getContext("2d"); + context.fillStyle = "white"; + context.fillRect(0, 0, canvas.width, canvas.height); + + let captureListener; + // On Windows and Mac we can update the drag image during a drag + // using updateDragImage. On Linux, we can use a panel. +#if defined(XP_WIN) || defined(XP_MACOSX) + captureListener = function() { + dt.updateDragImage(canvas, dragImageOffset, dragImageOffset); + } +#else + // 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); + this._dndPanel.appendChild(wrapper); + document.documentElement.appendChild(this._dndPanel); + } + toDrag = this._dndPanel; +#endif + // PageThumb is async with e10s but that's fine + // since we can update the image during the dnd. + PageThumbs.captureToCanvas(browser, canvas, captureListener); + } else { + // For the non e10s case we can just use PageThumbs + // sync, so let's use the canvas for setDragImage. + PageThumbs.captureToCanvas(browser, 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 }; +#ifndef XP_WIN + props.outerWidth = winWidth; + props.outerHeight = winHeight; +#endif + 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:hbox class="tab-label-container" + xbl:inherits="pinned,selected=visuallyselected" + onoverflow="this.setAttribute('textoverflow', 'true');" + onunderflow="this.removeAttribute('textoverflow');" + flex="1"> + <xul:label class="tab-text tab-label" + xbl:inherits="xbl:text=label,accesskey,fadein,pinned,selected=visuallyselected,attention" + role="presentation"/> + </xul:hbox> + <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="soundBlocked" readonly="true"> + <getter> + return this.getAttribute("blocked") == "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 (this.selected) + tabContainer._handleTabSelect(); + + 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="startMediaBlockTimer"> + <body><![CDATA[ + TelemetryStopwatch.start("TAB_MEDIA_BLOCKING_TIME_MS", this); + ]]></body> + </method> + + <method name="finishMediaBlockTimer"> + <body><![CDATA[ + TelemetryStopwatch.finish("TAB_MEDIA_BLOCKING_TIME_MS", this); + ]]></body> + </method> + + <method name="toggleMuteAudio"> + <parameter name="aMuteReason"/> + <body> + <![CDATA[ + let tabContainer = this.parentNode; + let browser = this.linkedBrowser; + let modifiedAttrs = []; + let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED"); + + 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(); + hist.add(3 /* unblockByClickingIcon */); + this.finishMediaBlockTimer(); + } else { + if (browser.audioMuted) { + browser.unmute(); + this.removeAttribute("muted"); + BrowserUITelemetry.countTabMutingEvent("unmute", aMuteReason); + hist.add(1 /* unmute */); + } else { + browser.mute(); + this.setAttribute("muted", "true"); + BrowserUITelemetry.countTabMutingEvent("mute", aMuteReason); + hist.add(0 /* mute */); + } + 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", "end"); + + 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, {useAccessKeys: false}); + return; + } + + let containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled"); + + if (event.target.getAttribute("anonid") == "newtab-popup" || + event.target.id == "newtab-popup") { + createUserContextMenu(event, {useAccessKeys: false}); + } 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); + tabcontainer.addEventListener("TabClose", this); + tabcontainer.mTabstrip.addEventListener("scroll", this); + + 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); + tabcontainer.removeEventListener("TabAttrModified", this); + tabcontainer.removeEventListener("TabClose", this); + ]]></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); + ]]></constructor> + + <destructor><![CDATA[ + window.removeEventListener("resize", this); + 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> |