Mypal/application/basilisk/base/content/tabbrowser.xml

7179 lines
270 KiB
XML

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