mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 07:05:24 +00:00
6568 lines
246 KiB
XML
6568 lines
246 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"/>
|
|
</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="mFaviconService" readonly="true">
|
|
Components.classes["@mozilla.org/browser/favicon-service;1"]
|
|
.getService(Components.interfaces.nsIFaviconService);
|
|
</field>
|
|
<field name="_placesAutocomplete" readonly="true">
|
|
Components.classes["@mozilla.org/autocomplete/search;1?name=history"]
|
|
.getService(Components.interfaces.mozIPlacesAutoComplete);
|
|
</field>
|
|
<field name="_unifiedComplete" readonly="true">
|
|
Components.classes["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"]
|
|
.getService(Components.interfaces.mozIPlacesAutoComplete);
|
|
</field>
|
|
<field name="PlacesUtils" readonly="true">
|
|
(Components.utils.import("resource://gre/modules/PlacesUtils.jsm", {})).PlacesUtils;
|
|
</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="mTabListeners">
|
|
[]
|
|
</field>
|
|
<field name="mTabFilters">
|
|
[]
|
|
</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="formValidationAnchor" readonly="true">
|
|
<getter><![CDATA[
|
|
if (this.mCurrentTab._formValidationAnchor) {
|
|
return this.mCurrentTab._formValidationAnchor;
|
|
}
|
|
let stack = this.mCurrentBrowser.parentNode;
|
|
// Create an anchor for the form validation popup
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
let formValidationAnchor = document.createElementNS(NS_XUL, "hbox");
|
|
formValidationAnchor.className = "form-validation-anchor";
|
|
formValidationAnchor.hidden = true;
|
|
stack.appendChild(formValidationAnchor);
|
|
return this.mCurrentTab._formValidationAnchor = formValidationAnchor;
|
|
]]></getter>
|
|
</property>
|
|
|
|
<method name="isFindBarInitialized">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
return (aTab || this.selectedTab)._findBar != undefined;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="getFindBar">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
if (!aTab)
|
|
aTab = this.selectedTab;
|
|
|
|
if (aTab._findBar)
|
|
return aTab._findBar;
|
|
|
|
let findBar = document.createElementNS(this.namespaceURI, "findbar");
|
|
let browser = this.getBrowserForTab(aTab);
|
|
let browserContainer = this.getBrowserContainer(browser);
|
|
browserContainer.appendChild(findBar);
|
|
|
|
// Force a style flush to ensure that our binding is attached.
|
|
findBar.clientTop;
|
|
|
|
findBar.browser = browser;
|
|
findBar._findField.value = this._lastFindValue;
|
|
|
|
aTab._findBar = findBar;
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TabFindInitialized", true, false);
|
|
aTab.dispatchEvent(event);
|
|
|
|
return findBar;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="getStatusPanel">
|
|
<body><![CDATA[
|
|
if (!this._statusPanel) {
|
|
this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
|
|
this._statusPanel.setAttribute("inactive", "true");
|
|
this._statusPanel.setAttribute("layer", "true");
|
|
this._appendStatusPanel();
|
|
}
|
|
return this._statusPanel;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_appendStatusPanel">
|
|
<body><![CDATA[
|
|
if (this._statusPanel) {
|
|
let browser = this.selectedBrowser;
|
|
let browserContainer = this.getBrowserContainer(browser);
|
|
browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling);
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="updateWindowResizers">
|
|
<body><![CDATA[
|
|
if (!window.gShowPageResizers)
|
|
return;
|
|
|
|
var show = window.windowState == window.STATE_NORMAL;
|
|
for (let i = 0; i < this.browsers.length; i++) {
|
|
this.browsers[i].showWindowResizer = show;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_setCloseKeyState">
|
|
<parameter name="aEnabled"/>
|
|
<body><![CDATA[
|
|
let keyClose = document.getElementById("key_close");
|
|
let closeKeyEnabled = keyClose.getAttribute("disabled") != "true";
|
|
if (closeKeyEnabled == aEnabled)
|
|
return;
|
|
|
|
if (aEnabled)
|
|
keyClose.removeAttribute("disabled");
|
|
else
|
|
keyClose.setAttribute("disabled", "true");
|
|
|
|
// We also want to remove the keyboard shortcut from the file menu
|
|
// when the shortcut is disabled, and bring it back when it's
|
|
// renabled.
|
|
//
|
|
// Fixing bug 630826 could make that happen automatically.
|
|
// Fixing bug 630830 could avoid the ugly hack below.
|
|
|
|
let closeMenuItem = document.getElementById("menu_close");
|
|
let parentPopup = closeMenuItem.parentNode;
|
|
let nextItem = closeMenuItem.nextSibling;
|
|
let clonedItem = closeMenuItem.cloneNode(true);
|
|
|
|
parentPopup.removeChild(closeMenuItem);
|
|
|
|
if (aEnabled)
|
|
clonedItem.setAttribute("key", "key_close");
|
|
else
|
|
clonedItem.removeAttribute("key");
|
|
|
|
parentPopup.insertBefore(clonedItem, nextItem);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="pinTab">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
if (aTab.pinned)
|
|
return;
|
|
|
|
if (aTab.hidden)
|
|
this.showTab(aTab);
|
|
|
|
this.moveTabTo(aTab, this._numPinnedTabs);
|
|
aTab.setAttribute("pinned", "true");
|
|
this.tabContainer._unlockTabSizing();
|
|
this.tabContainer._positionPinnedTabs();
|
|
this.tabContainer.adjustTabstrip();
|
|
|
|
this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true })
|
|
|
|
if (aTab.selected)
|
|
this._setCloseKeyState(false);
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TabPinned", true, false);
|
|
aTab.dispatchEvent(event);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="unpinTab">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
if (!aTab.pinned)
|
|
return;
|
|
|
|
this.moveTabTo(aTab, this._numPinnedTabs - 1);
|
|
aTab.removeAttribute("pinned");
|
|
aTab.style.MozMarginStart = "";
|
|
this.tabContainer._unlockTabSizing();
|
|
this.tabContainer._positionPinnedTabs();
|
|
this.tabContainer.adjustTabstrip();
|
|
|
|
this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false })
|
|
|
|
if (aTab.selected)
|
|
this._setCloseKeyState(true);
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TabUnpinned", true, false);
|
|
aTab.dispatchEvent(event);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="previewTab">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aCallback"/>
|
|
<body>
|
|
<![CDATA[
|
|
let currentTab = this.selectedTab;
|
|
try {
|
|
// Suppress focus, ownership and selected tab changes
|
|
this._previewMode = true;
|
|
this.selectedTab = aTab;
|
|
aCallback();
|
|
} finally {
|
|
this.selectedTab = currentTab;
|
|
this._previewMode = false;
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserAtIndex">
|
|
<parameter name="aIndex"/>
|
|
<body>
|
|
<![CDATA[
|
|
return this.browsers[aIndex];
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserIndexForDocument">
|
|
<parameter name="aDocument"/>
|
|
<body>
|
|
<![CDATA[
|
|
var tab = this._getTabForContentWindow(aDocument.defaultView);
|
|
return tab ? tab._tPos : -1;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserForDocument">
|
|
<parameter name="aDocument"/>
|
|
<body>
|
|
<![CDATA[
|
|
var tab = this._getTabForContentWindow(aDocument.defaultView);
|
|
return tab ? tab.linkedBrowser : null;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserForContentWindow">
|
|
<parameter name="aWindow"/>
|
|
<body>
|
|
<![CDATA[
|
|
var tab = this._getTabForContentWindow(aWindow);
|
|
return tab ? tab.linkedBrowser : null;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserForOuterWindowID">
|
|
<parameter name="aID"/>
|
|
<body>
|
|
<![CDATA[
|
|
return this._outerWindowIDBrowserMap.get(aID);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_getTabForContentWindow">
|
|
<parameter name="aWindow"/>
|
|
<body>
|
|
<![CDATA[
|
|
// When not using remote browsers, we can take a fast path by getting
|
|
// directly from the content window to the browser without looping
|
|
// over all browsers.
|
|
if (!gMultiProcessBrowser) {
|
|
let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell)
|
|
.chromeEventHandler;
|
|
return this.getTabForBrowser(browser);
|
|
}
|
|
|
|
for (let i = 0; i < this.browsers.length; i++) {
|
|
// NB: We use contentWindowAsCPOW so that this code works both
|
|
// for remote browsers as well. aWindow may be a CPOW.
|
|
if (this.browsers[i].contentWindowAsCPOW == aWindow)
|
|
return this.tabs[i];
|
|
}
|
|
return null;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<!-- Binding from browser to tab -->
|
|
<field name="_tabForBrowser" readonly="true">
|
|
<![CDATA[
|
|
new WeakMap();
|
|
]]>
|
|
</field>
|
|
|
|
<method name="_getTabForBrowser">
|
|
<parameter name="aBrowser" />
|
|
<body>
|
|
<![CDATA[
|
|
let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
|
|
let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser";
|
|
let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser";
|
|
Deprecated.warning(text, url);
|
|
return this.getTabForBrowser(aBrowser);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getTabForBrowser">
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
return this._tabForBrowser.get(aBrowser);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getNotificationBox">
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
return this.getSidebarContainer(aBrowser).parentNode;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getSidebarContainer">
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
return this.getBrowserContainer(aBrowser).parentNode;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserContainer">
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
return (aBrowser || this.mCurrentBrowser).parentNode.parentNode;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getTabModalPromptBox">
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
let browser = (aBrowser || this.mCurrentBrowser);
|
|
if (!browser.tabModalPromptBox) {
|
|
browser.tabModalPromptBox = new TabModalPromptBox(browser);
|
|
}
|
|
return browser.tabModalPromptBox;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_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"/>
|
|
<body>
|
|
<![CDATA[
|
|
let stateFlags = 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;
|
|
},
|
|
|
|
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;
|
|
|
|
var oldBlank = this.mBlank;
|
|
|
|
const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
|
|
const nsIChannel = Components.interfaces.nsIChannel;
|
|
|
|
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) {
|
|
// It's okay to clear what the user typed when we start
|
|
// loading a document. If the user types, this counter gets
|
|
// set to zero, if the document load ends without an
|
|
// onLocationChange, this counter gets decremented
|
|
// (so we keep it while switching tabs after failed loads)
|
|
// We need to add 2 because loadURIWithFlags may have
|
|
// cancelled a pending load which would have cleared
|
|
// its anchor scroll detection temporary increment.
|
|
if (aWebProgress.isTopLevel) {
|
|
this.mBrowser.userTypedClear += 2;
|
|
|
|
// If the browser is loading it must not be crashed anymore
|
|
this.mTab.removeAttribute("crashed");
|
|
|
|
// If the browser was playing audio, we should remove the playing state.
|
|
if (this.mTab.hasAttribute("soundplaying")) {
|
|
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._shouldShowProgress(aRequest)) {
|
|
if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) {
|
|
this.mTab.setAttribute("busy", "true");
|
|
if (!(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) {
|
|
if (!Components.isSuccessCode(aStatus) &&
|
|
!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;
|
|
|
|
if (this.mTab.selected && gURLBar)
|
|
URLBarSetURI();
|
|
} else {
|
|
// The document is done loading, we no longer want the
|
|
// value cleared.
|
|
|
|
if (this.mBrowser.userTypedClear > 1)
|
|
this.mBrowser.userTypedClear -= 2;
|
|
else if (this.mBrowser.userTypedClear > 0)
|
|
this.mBrowser.userTypedClear--;
|
|
}
|
|
|
|
if (!this.mBrowser.mIconURL)
|
|
this.mTabBrowser.useDefaultIcon(this.mTab);
|
|
}
|
|
|
|
if (this.mBlank)
|
|
this.mBlank = false;
|
|
|
|
var location = aRequest.QueryInterface(nsIChannel).URI;
|
|
|
|
// 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 (oldBlank) {
|
|
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) {
|
|
// If userTypedClear > 0, the document loaded correctly and we should be
|
|
// clearing the user typed value. We also 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.
|
|
if (this.mBrowser.userTypedClear > 0 ||
|
|
((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
|
|
aLocation.spec != "about:blank") ||
|
|
aFlags && Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)
|
|
this.mBrowser.userTypedValue = null;
|
|
|
|
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();
|
|
|
|
// fix bug 253793 - turn off highlight when page changes
|
|
findBar.getElement("highlight").checked = false;
|
|
}
|
|
|
|
// Don't clear the favicon if this onLocationChange was
|
|
// triggered by a pushState or a replaceState. See bug 550565.
|
|
if (aWebProgress.isLoadingDocument &&
|
|
!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) {
|
|
this.mBrowser.mIconURL = null;
|
|
}
|
|
|
|
let autocomplete = this.mTabBrowser._placesAutocomplete;
|
|
let unifiedComplete = this.mTabBrowser._unifiedComplete;
|
|
if (this.mBrowser.registeredOpenURI) {
|
|
autocomplete.unregisterOpenPage(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)) {
|
|
autocomplete.registerOpenPage(aLocation);
|
|
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>
|
|
|
|
<method name="setIcon">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aURI"/>
|
|
<body>
|
|
<![CDATA[
|
|
var browser = this.getBrowserForTab(aTab);
|
|
browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
|
|
if (aURI && this.mFaviconService) {
|
|
if (!(aURI instanceof Ci.nsIURI))
|
|
aURI = makeURI(aURI);
|
|
this.mFaviconService.setAndFetchFaviconForPage(browser.currentURI,
|
|
aURI, false,
|
|
PrivateBrowsingUtils.isWindowPrivate(window) ?
|
|
this.mFaviconService.FAVICON_LOAD_PRIVATE :
|
|
this.mFaviconService.FAVICON_LOAD_NON_PRIVATE);
|
|
}
|
|
|
|
let sizedIconUrl = browser.mIconURL || "";
|
|
if (sizedIconUrl != aTab.getAttribute("image")) {
|
|
if (sizedIconUrl)
|
|
aTab.setAttribute("image", sizedIconUrl);
|
|
else
|
|
aTab.removeAttribute("image");
|
|
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);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isFailedIcon">
|
|
<parameter name="aURI"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.mFaviconService) {
|
|
if (!(aURI instanceof Ci.nsIURI))
|
|
aURI = makeURI(aURI);
|
|
return this.mFaviconService.isFailedFavicon(aURI);
|
|
}
|
|
return null;
|
|
]]>
|
|
</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[
|
|
if ("TabView" in window && TabView.isVisible()) {
|
|
// ToDo: this will be removed when we gain ability to draw to the menu bar.
|
|
// Bug 586175
|
|
this.ownerDocument.title = TabView.windowTitle;
|
|
}
|
|
else {
|
|
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;
|
|
|
|
if (!aForceUpdate) {
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
|
|
if (!Services.appinfo.browserTabsRemoteAutostart) {
|
|
// old way of measuring tab paint which is not
|
|
// valid with e10s.
|
|
window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
|
|
.beginTabSwitch();
|
|
}
|
|
}
|
|
|
|
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.mTabListeners[this.tabContainer.selectedIndex] || null;
|
|
if (listener && listener.mStateFlags) {
|
|
this._callProgressListeners(null, "onUpdateCurrentBrowser",
|
|
[listener.mStateFlags, listener.mStatus,
|
|
listener.mMessage, listener.mTotalProgress],
|
|
true, false);
|
|
}
|
|
|
|
if (!this._previewMode) {
|
|
this.mCurrentTab.lastAccessed = Infinity;
|
|
this.mCurrentTab.removeAttribute("unread");
|
|
oldTab.lastAccessed = Date.now();
|
|
|
|
let oldFindBar = oldTab._findBar;
|
|
if (oldFindBar &&
|
|
oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
|
|
!oldFindBar.hidden)
|
|
this._lastFindValue = oldFindBar._findField.value;
|
|
|
|
this.updateTitlebar();
|
|
|
|
this.mCurrentTab.removeAttribute("titlechanged");
|
|
this.mCurrentTab.removeAttribute("attention");
|
|
}
|
|
|
|
// If the new tab is busy, and our current state is not busy, then
|
|
// we need to fire a start to all progress listeners.
|
|
const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
|
|
if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) {
|
|
this.mIsBusy = true;
|
|
this._callProgressListeners(null, "onStateChange",
|
|
[webProgress, null,
|
|
nsIWebProgressListener.STATE_START |
|
|
nsIWebProgressListener.STATE_IS_NETWORK, 0],
|
|
true, false);
|
|
}
|
|
|
|
// If the new tab is not busy, and our current state is busy, then
|
|
// we need to fire a stop to all progress listeners.
|
|
if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) {
|
|
this.mIsBusy = false;
|
|
this._callProgressListeners(null, "onStateChange",
|
|
[webProgress, null,
|
|
nsIWebProgressListener.STATE_STOP |
|
|
nsIWebProgressListener.STATE_IS_NETWORK, 0],
|
|
true, false);
|
|
}
|
|
|
|
this._setCloseKeyState(!this.mCurrentTab.pinned);
|
|
|
|
// TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
|
|
// that might rely upon the other changes suppressed.
|
|
// Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
|
|
if (!this._previewMode) {
|
|
// We've selected the new tab, so go ahead and notify listeners.
|
|
let event = new CustomEvent("TabSelect", {
|
|
bubbles: true,
|
|
cancelable: false,
|
|
detail: {
|
|
previousTab: oldTab
|
|
}
|
|
});
|
|
this.mCurrentTab.dispatchEvent(event);
|
|
|
|
this._tabAttrModified(oldTab, ["selected"]);
|
|
this._tabAttrModified(this.mCurrentTab, ["selected"]);
|
|
|
|
if (oldBrowser != newBrowser &&
|
|
oldBrowser.getInPermitUnload) {
|
|
oldBrowser.getInPermitUnload(inPermitUnload => {
|
|
if (!inPermitUnload) {
|
|
return;
|
|
}
|
|
// Since the user is switching away from a tab that has
|
|
// a beforeunload prompt active, we remove the prompt.
|
|
// This prevents confusing user flows like the following:
|
|
// 1. User attempts to close Firefox
|
|
// 2. User switches tabs (ingoring a beforeunload prompt)
|
|
// 3. User returns to tab, presses "Leave page"
|
|
let promptBox = this.getTabModalPromptBox(oldBrowser);
|
|
let prompts = promptBox.listPrompts();
|
|
// There might not be any prompts here if the tab was closed
|
|
// while in an onbeforeunload prompt, which will have
|
|
// destroyed aforementioned prompt already, so check there's
|
|
// something to remove, first:
|
|
if (prompts.length) {
|
|
// NB: This code assumes that the beforeunload prompt
|
|
// is the top-most prompt on the tab.
|
|
prompts[prompts.length - 1].abortPrompt();
|
|
}
|
|
});
|
|
}
|
|
|
|
oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
|
|
if (this.isFindBarInitialized(oldTab)) {
|
|
let findBar = this.getFindBar(oldTab);
|
|
oldTab._findBarFocused = (!findBar.hidden &&
|
|
findBar._findField.getAttribute("focused") == "true");
|
|
}
|
|
|
|
// If focus is in the tab bar, retain it there.
|
|
if (document.activeElement == oldTab) {
|
|
// We need to explicitly focus the new tab, because
|
|
// tabbox.xml does this only in some cases.
|
|
this.mCurrentTab.focus();
|
|
} else if (gMultiProcessBrowser && document.activeElement !== newBrowser) {
|
|
// Clear focus so that _adjustFocusAfterTabSwitch can detect if
|
|
// some element has been focused and respect that.
|
|
document.activeElement.blur();
|
|
}
|
|
|
|
if (!gMultiProcessBrowser)
|
|
this._adjustFocusAfterTabSwitch(this.mCurrentTab);
|
|
}
|
|
|
|
updateUserContextUIIndicator(gBrowser.selectedBrowser);
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
let event = new CustomEvent("TabSwitchDone", {
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
this.dispatchEvent(event);
|
|
}
|
|
|
|
if (!aForceUpdate)
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_adjustFocusAfterTabSwitch">
|
|
<parameter name="newTab"/>
|
|
<body><![CDATA[
|
|
// Don't steal focus from the tab bar.
|
|
if (document.activeElement == newTab)
|
|
return;
|
|
|
|
let newBrowser = this.getBrowserForTab(newTab);
|
|
|
|
// If there's a tabmodal prompt showing, focus it.
|
|
if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
|
|
let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
|
|
let prompt = prompts[prompts.length - 1];
|
|
prompt.Dialog.setDefaultFocus();
|
|
return;
|
|
}
|
|
|
|
// Focus the location bar if it was previously focused for that tab.
|
|
// In full screen mode, only bother making the location bar visible
|
|
// if the tab is a blank one.
|
|
if (newBrowser._urlbarFocused && gURLBar) {
|
|
// Explicitly close the popup if the URL bar retains focus
|
|
gURLBar.closePopup();
|
|
|
|
if (!window.fullScreen) {
|
|
gURLBar.focus();
|
|
return;
|
|
}
|
|
|
|
if (isTabEmpty(this.mCurrentTab)) {
|
|
focusAndSelectUrlBar();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Focus the find bar if it was previously focused for that tab.
|
|
if (gFindBarInitialized && !gFindBar.hidden &&
|
|
this.selectedTab._findBarFocused) {
|
|
gFindBar._findField.focus();
|
|
return;
|
|
}
|
|
|
|
// Don't focus the content area if something has been focused after the
|
|
// tab switch was initiated.
|
|
if (gMultiProcessBrowser &&
|
|
document.activeElement != document.documentElement)
|
|
return;
|
|
|
|
// We're now committed to focusing the content area.
|
|
let fm = Services.focus;
|
|
let focusFlags = fm.FLAG_NOSCROLL;
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {});
|
|
|
|
// for anchors, use FLAG_SHOWRING so that it is clear what link was
|
|
// last clicked when switching back to that tab
|
|
if (newFocusedElement &&
|
|
(newFocusedElement instanceof HTMLAnchorElement ||
|
|
newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple"))
|
|
focusFlags |= fm.FLAG_SHOWRING;
|
|
}
|
|
|
|
fm.setFocus(newBrowser, focusFlags);
|
|
]]></body>
|
|
</method>
|
|
|
|
<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="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 // Still no title? Fall back to our untitled string.
|
|
title = this.mStringBundle.getString("tabs.emptyTabTitle");
|
|
}
|
|
|
|
if (aTab.label == title &&
|
|
aTab.crop == crop)
|
|
return false;
|
|
|
|
aTab.label = title;
|
|
aTab.crop = crop;
|
|
this._tabAttrModified(aTab, ["label", "crop"]);
|
|
|
|
if (aTab.selected)
|
|
this.updateTitlebar();
|
|
|
|
return true;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="loadOneTab">
|
|
<parameter name="aURI"/>
|
|
<parameter name="aReferrerURI"/>
|
|
<parameter name="aCharset"/>
|
|
<parameter name="aPostData"/>
|
|
<parameter name="aLoadInBackground"/>
|
|
<parameter name="aAllowThirdPartyFixup"/>
|
|
<body>
|
|
<![CDATA[
|
|
var aReferrerPolicy;
|
|
var aFromExternal;
|
|
var aRelatedToCurrent;
|
|
var aAllowMixedContent;
|
|
var aSkipAnimation;
|
|
var aForceNotRemote;
|
|
var aNoReferrer;
|
|
var aUserContextId;
|
|
if (arguments.length == 2 &&
|
|
typeof arguments[1] == "object" &&
|
|
!(arguments[1] instanceof Ci.nsIURI)) {
|
|
let params = arguments[1];
|
|
aReferrerURI = params.referrerURI;
|
|
aReferrerPolicy = params.referrerPolicy;
|
|
aCharset = params.charset;
|
|
aPostData = params.postData;
|
|
aLoadInBackground = params.inBackground;
|
|
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
|
|
aFromExternal = params.fromExternal;
|
|
aRelatedToCurrent = params.relatedToCurrent;
|
|
aAllowMixedContent = params.allowMixedContent;
|
|
aSkipAnimation = params.skipAnimation;
|
|
aForceNotRemote = params.forceNotRemote;
|
|
aNoReferrer = params.noReferrer;
|
|
aUserContextId = params.userContextId;
|
|
}
|
|
|
|
var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
|
|
Services.prefs.getBoolPref("browser.tabs.loadInBackground");
|
|
var owner = bgLoad ? null : this.selectedTab;
|
|
var tab = this.addTab(aURI, {
|
|
referrerURI: aReferrerURI,
|
|
referrerPolicy: aReferrerPolicy,
|
|
charset: aCharset,
|
|
postData: aPostData,
|
|
ownerTab: owner,
|
|
allowThirdPartyFixup: aAllowThirdPartyFixup,
|
|
fromExternal: aFromExternal,
|
|
relatedToCurrent: aRelatedToCurrent,
|
|
skipAnimation: aSkipAnimation,
|
|
allowMixedContent: aAllowMixedContent,
|
|
forceNotRemote: aForceNotRemote,
|
|
noReferrer: aNoReferrer,
|
|
userContextId: aUserContextId });
|
|
if (!bgLoad)
|
|
this.selectedTab = tab;
|
|
|
|
return tab;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="loadTabs">
|
|
<parameter name="aURIs"/>
|
|
<parameter name="aLoadInBackground"/>
|
|
<parameter name="aReplace"/>
|
|
<body><![CDATA[
|
|
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;
|
|
|
|
if (aReplace) {
|
|
try {
|
|
this.loadURI(aURIs[0], null, null);
|
|
} 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});
|
|
|
|
var tabNum = this.tabContainer.selectedIndex;
|
|
for (let i = 1; i < aURIs.length; ++i) {
|
|
let tab = this.addTab(aURIs[i], {skipAnimation: true});
|
|
if (aReplace)
|
|
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"/>
|
|
<body>
|
|
<![CDATA[
|
|
let isRemote = aBrowser.getAttribute("remote") == "true";
|
|
if (isRemote == aShouldBeRemote)
|
|
return false;
|
|
|
|
let wasActive = document.activeElement == aBrowser;
|
|
|
|
// Unmap the old outerWindowID.
|
|
this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
|
|
|
|
// Unhook our progress listener.
|
|
let tab = this.getTabForBrowser(aBrowser);
|
|
let index = tab._tPos;
|
|
let filter = this.mTabFilters[index];
|
|
aBrowser.webProgress.removeProgressListener(filter);
|
|
// Make sure the browser is destroyed so it unregisters from observer notifications
|
|
aBrowser.destroy();
|
|
|
|
// Make sure to restore the original droppedLinkHandler.
|
|
let droppedLinkHandler = aBrowser.droppedLinkHandler;
|
|
|
|
// Change the "remote" attribute.
|
|
let parent = aBrowser.parentNode;
|
|
parent.removeChild(aBrowser);
|
|
aBrowser.setAttribute("remote", aShouldBeRemote ? "true" : "false");
|
|
parent.appendChild(aBrowser);
|
|
|
|
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 = (aBrowser == this.selectedBrowser &&
|
|
window.windowState != window.STATE_MINIMIZED);
|
|
|
|
// 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, securityUI.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;
|
|
}
|
|
|
|
let evt = document.createEvent("Events");
|
|
evt.initEvent("TabRemotenessChange", true, false);
|
|
tab.dispatchEvent(evt);
|
|
|
|
return true;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="updateBrowserRemotenessByURL">
|
|
<parameter name="aBrowser"/>
|
|
<parameter name="aURL"/>
|
|
<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.isRemoteBrowser) {
|
|
browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
|
|
browser.attachFormFill();
|
|
}
|
|
|
|
return browser;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_isPreloadingEnabled">
|
|
<body>
|
|
<![CDATA[
|
|
// Preloading for the newtab page is enabled when the pref is true
|
|
// and the URL is "about:newtab". We do not support preloading for
|
|
// custom newtab URLs.
|
|
return Services.prefs.getBoolPref("browser.newtab.preload") &&
|
|
!aboutNewTabService.overridden;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_createPreloadBrowser">
|
|
<body>
|
|
<![CDATA[
|
|
// Do nothing if we have a preloaded browser already
|
|
// or preloading of newtab pages is disabled.
|
|
if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
|
|
return;
|
|
}
|
|
|
|
let remote = gMultiProcessBrowser &&
|
|
E10SUtils.canLoadURIInProcess(BROWSER_NEW_TAB_URL, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
|
|
let browser = this._createBrowser({isPreloadBrowser: true, remote: remote});
|
|
this._preloadedBrowser = browser;
|
|
|
|
let notificationbox = this.getNotificationBox(browser);
|
|
this.mPanelContainer.appendChild(notificationbox);
|
|
|
|
if (remote) {
|
|
// For remote browsers, we need to make sure that the webProgress is
|
|
// instantiated, otherwise the parent won't get informed about the state
|
|
// of the preloaded browser until it gets attached to a tab.
|
|
browser.webProgress;
|
|
}
|
|
|
|
browser.loadURI(BROWSER_NEW_TAB_URL);
|
|
browser.docShellIsActive = false;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_createBrowser">
|
|
<parameter name="aParams"/>
|
|
<body>
|
|
<![CDATA[
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
let remote = aParams && aParams.remote;
|
|
let uriIsAboutBlank = aParams && aParams.uriIsAboutBlank;
|
|
let isPreloadBrowser = aParams && aParams.isPreloadBrowser;
|
|
let userContextId = aParams && aParams.userContextId;
|
|
|
|
let b = document.createElementNS(NS_XUL, "browser");
|
|
b.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 (userContextId)
|
|
b.setAttribute("usercontextid", userContextId);
|
|
|
|
if (remote)
|
|
b.setAttribute("remote", "true");
|
|
|
|
if (window.gShowPageResizers && window.windowState == window.STATE_NORMAL) {
|
|
b.setAttribute("showresizer", "true");
|
|
}
|
|
|
|
if (!isPreloadBrowser && this.hasAttribute("autocompletepopup"))
|
|
b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
|
|
|
|
if (this.hasAttribute("selectmenulist"))
|
|
b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
|
|
|
|
b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
|
|
|
|
// 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 (!uriIsAboutBlank) {
|
|
b.setAttribute("nodefaultsrc", "true");
|
|
}
|
|
|
|
return b;
|
|
]]>
|
|
</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[
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
var aReferrerPolicy;
|
|
var aFromExternal;
|
|
var aRelatedToCurrent;
|
|
var aSkipAnimation;
|
|
var aAllowMixedContent;
|
|
var aForceNotRemote;
|
|
var aNoReferrer;
|
|
var aUserContextId;
|
|
if (arguments.length == 2 &&
|
|
typeof arguments[1] == "object" &&
|
|
!(arguments[1] instanceof Ci.nsIURI)) {
|
|
let params = arguments[1];
|
|
aReferrerURI = params.referrerURI;
|
|
aReferrerPolicy = params.referrerPolicy;
|
|
aCharset = params.charset;
|
|
aPostData = params.postData;
|
|
aOwner = params.ownerTab;
|
|
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
|
|
aFromExternal = params.fromExternal;
|
|
aRelatedToCurrent = params.relatedToCurrent;
|
|
aSkipAnimation = params.skipAnimation;
|
|
aAllowMixedContent = params.allowMixedContent;
|
|
aForceNotRemote = params.forceNotRemote;
|
|
aNoReferrer = params.noReferrer;
|
|
aUserContextId = params.userContextId;
|
|
}
|
|
|
|
// if we're adding tabs, we're past interrupt mode, ditch the owner
|
|
if (this.mCurrentTab.owner)
|
|
this.mCurrentTab.owner = null;
|
|
|
|
var t = document.createElementNS(NS_XUL, "tab");
|
|
|
|
var uriIsAboutBlank = !aURI || aURI == "about:blank";
|
|
|
|
if (aUserContextId)
|
|
t.setAttribute("usercontextid", aUserContextId);
|
|
t.setAttribute("crop", "end");
|
|
t.setAttribute("onerror", "this.removeAttribute('image');");
|
|
t.className = "tabbrowser-tab";
|
|
|
|
// The new browser should be remote if this is an e10s window and
|
|
// the uri to load can be loaded remotely.
|
|
let remote = gMultiProcessBrowser &&
|
|
!aForceNotRemote &&
|
|
E10SUtils.canLoadURIInProcess(aURI, Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT);
|
|
|
|
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 caches
|
|
this._browsers = null;
|
|
this._visibleTabs = null;
|
|
|
|
this.tabContainer.appendChild(t);
|
|
|
|
// If this new tab is owned by another, assert that relationship
|
|
if (aOwner)
|
|
t.owner = aOwner;
|
|
|
|
let b;
|
|
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 && !aUserContextId &&
|
|
!PrivateBrowsingUtils.isWindowPrivate(window)) {
|
|
b = this._getPreloadedBrowser();
|
|
usingPreloadedContent = !!b;
|
|
}
|
|
|
|
if (!b) {
|
|
// No preloaded browser found, create one.
|
|
b = this._createBrowser({remote: remote,
|
|
uriIsAboutBlank: uriIsAboutBlank,
|
|
userContextId: aUserContextId});
|
|
}
|
|
|
|
let notificationbox = this.getNotificationBox(b);
|
|
var position = this.tabs.length - 1;
|
|
var uniqueId = this._generateUniquePanelID();
|
|
notificationbox.id = uniqueId;
|
|
t.linkedPanel = uniqueId;
|
|
t.linkedBrowser = b;
|
|
this._tabForBrowser.set(b, t);
|
|
t._tPos = position;
|
|
t.lastAccessed = Date.now();
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
// 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);
|
|
}
|
|
|
|
// We've waited until the tab is in the DOM to set the label. This
|
|
// allows the TabLabelModified event to be properly dispatched.
|
|
if (!aURI || isBlankPageURL(aURI)) {
|
|
t.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.label = aURI;
|
|
}
|
|
|
|
this.tabContainer.updateVisibility();
|
|
|
|
// wire up a progress listener for the new browser object.
|
|
var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank, usingPreloadedContent);
|
|
const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
|
|
.createInstance(Components.interfaces.nsIWebProgress);
|
|
filter.addProgressListener(tabListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
|
|
b.webProgress.addProgressListener(filter, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
|
|
this.mTabListeners[position] = tabListener;
|
|
this.mTabFilters[position] = filter;
|
|
|
|
b.droppedLinkHandler = handleDroppedLink;
|
|
|
|
// Swap in a preloaded customize tab, if available.
|
|
if (aURI == "about:customizing") {
|
|
usingPreloadedContent = gCustomizationTabPreloader.newTab(t);
|
|
}
|
|
|
|
// 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 evt = document.createEvent("Events");
|
|
evt.initEvent("TabOpen", true, false);
|
|
t.dispatchEvent(evt);
|
|
|
|
// 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: flags,
|
|
referrerURI: aNoReferrer ? null: aReferrerURI,
|
|
referrerPolicy: aReferrerPolicy,
|
|
charset: aCharset,
|
|
postData: aPostData,
|
|
});
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
// We start our browsers out as inactive, and then maintain
|
|
// activeness in the tab switcher.
|
|
b.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().
|
|
//
|
|
// Note: Only do this of we still have a docShell. The TabOpen
|
|
// event was dispatched above and a gBrowser.removeTab() call from
|
|
// one of its listeners could cause us to fail here.
|
|
if (!remote && b.docShell) {
|
|
this._outerWindowIDBrowserMap.set(b.outerWindowID, b);
|
|
}
|
|
|
|
// 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, false, 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="aTabWillBeMoved"/>
|
|
<parameter name="aCloseWindowWithLastTab"/>
|
|
<parameter name="aCloseWindowFastpath"/>
|
|
<parameter name="aSkipPermitUnload"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (aTab.closing ||
|
|
this._windowIsClosing)
|
|
return false;
|
|
|
|
var browser = this.getBrowserForTab(aTab);
|
|
|
|
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");
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (!aTab._pendingPermitUnload && !aTabWillBeMoved && !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} = 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 || !permitUnload) {
|
|
// NB: deliberately keep the _closedDuringPermitUnload set to
|
|
// true so we keep exiting early in case of multiple calls.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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 = document.createEvent("UIEvent");
|
|
evt.initUIEvent("TabClose", true, false, window, aTabWillBeMoved ? 1 : 0);
|
|
aTab.dispatchEvent(evt);
|
|
|
|
if (!aTabWillBeMoved && !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.mTabFilters[aTab._tPos];
|
|
|
|
browser.webProgress.removeProgressListener(filter);
|
|
|
|
filter.removeProgressListener(this.mTabListeners[aTab._tPos]);
|
|
this.mTabListeners[aTab._tPos].destroy();
|
|
|
|
if (browser.registeredOpenURI && !aTabWillBeMoved) {
|
|
this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
|
|
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.
|
|
// Clean up mTabFilters and mTabListeners now rather than in
|
|
// _beginRemoveTab, so that their size is always in sync with the
|
|
// number of tabs and browsers (the xbl destructor depends on this).
|
|
this.mTabFilters.splice(aTab._tPos, 1);
|
|
this.mTabListeners.splice(aTab._tPos, 1);
|
|
|
|
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;
|
|
|
|
// Invalidate browsers cache, as the tab is removed from the
|
|
// tab container.
|
|
this._browsers = null;
|
|
|
|
// 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);
|
|
|
|
// 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="swapNewTabWithBrowser">
|
|
<parameter name="aNewTab"/>
|
|
<parameter name="aBrowser"/>
|
|
<body>
|
|
<![CDATA[
|
|
// The browser must be standalone.
|
|
if (aBrowser.getTabBrowser())
|
|
throw Cr.NS_ERROR_INVALID_ARG;
|
|
|
|
// The tab is definitely not loading.
|
|
aNewTab.removeAttribute("busy");
|
|
if (aNewTab.selected) {
|
|
this.mIsBusy = false;
|
|
}
|
|
|
|
this._swapBrowserDocShells(aNewTab, aBrowser);
|
|
|
|
// Update the new tab's title.
|
|
this.setTabTitle(aNewTab);
|
|
|
|
if (aNewTab.selected) {
|
|
this.updateCurrentBrowser(true);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="swapBrowsersAndCloseOther">
|
|
<parameter name="aOurTab"/>
|
|
<parameter name="aOtherTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
// Do not allow transfering a private tab to a non-private window
|
|
// and vice versa.
|
|
if (PrivateBrowsingUtils.isWindowPrivate(window) !=
|
|
PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerDocument.defaultView))
|
|
return;
|
|
|
|
let ourBrowser = this.getBrowserForTab(aOurTab);
|
|
let otherBrowser = aOtherTab.linkedBrowser;
|
|
|
|
// Can't swap between chrome and content processes.
|
|
if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
|
|
return;
|
|
|
|
// Keep the userContextId if set on other browser
|
|
if (otherBrowser.hasAttribute("usercontextid")) {
|
|
ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
|
|
}
|
|
|
|
// That's gBrowser for the other window, not the tab's browser!
|
|
var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser;
|
|
var isPending = aOtherTab.hasAttribute("pending");
|
|
|
|
// 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, true, true))
|
|
return;
|
|
|
|
let modifiedAttrs = [];
|
|
if (aOtherTab.hasAttribute("muted")) {
|
|
aOurTab.setAttribute("muted", "true");
|
|
ourBrowser.mute();
|
|
modifiedAttrs.push("muted");
|
|
}
|
|
if (aOtherTab.hasAttribute("soundplaying")) {
|
|
aOurTab.setAttribute("soundplaying", "true");
|
|
modifiedAttrs.push("soundplaying");
|
|
}
|
|
if (aOtherTab.hasAttribute("usercontextid")) {
|
|
aOurTab.setAttribute("usercontextid", aOtherTab.getAttribute("usercontextid"));
|
|
modifiedAttrs.push("usercontextid");
|
|
}
|
|
|
|
// 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);
|
|
var isBusy = aOtherTab.hasAttribute("busy");
|
|
if (isBusy) {
|
|
aOurTab.setAttribute("busy", "true");
|
|
modifiedAttrs.push("busy");
|
|
if (aOurTab.selected)
|
|
this.mIsBusy = true;
|
|
}
|
|
|
|
this._swapBrowserDocShells(aOurTab, otherBrowser);
|
|
}
|
|
|
|
// 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"/>
|
|
<body>
|
|
<![CDATA[
|
|
// Unhook our progress listener
|
|
let index = aOurTab._tPos;
|
|
const filter = this.mTabFilters[index];
|
|
let tabListener = this.mTabListeners[index];
|
|
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);
|
|
}
|
|
|
|
aOtherBrowser.docShellIsActive = (ourBrowser == this.selectedBrowser &&
|
|
window.windowState != window.STATE_MINIMIZED);
|
|
|
|
// 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;
|
|
|
|
// Restore the progress listener
|
|
this.mTabListeners[index] = tabListener =
|
|
this.mTabProgressListener(aOurTab, ourBrowser, false, false);
|
|
|
|
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._placesAutocomplete.unregisterOpenPage(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[
|
|
this.getBrowserForTab(aTab).reload();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="addProgressListener">
|
|
<parameter name="aListener"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (arguments.length != 1) {
|
|
Components.utils.reportError("gBrowser.addProgressListener was " +
|
|
"called with a second argument, " +
|
|
"which is not supported. See bug " +
|
|
"608628. Call stack: " + new Error().stack);
|
|
}
|
|
|
|
this.mProgressListeners.push(aListener);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="removeProgressListener">
|
|
<parameter name="aListener"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.mProgressListeners =
|
|
this.mProgressListeners.filter(l => l != aListener);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="addTabsProgressListener">
|
|
<parameter name="aListener"/>
|
|
<body>
|
|
this.mTabsProgressListeners.push(aListener);
|
|
</body>
|
|
</method>
|
|
|
|
<method name="removeTabsProgressListener">
|
|
<parameter name="aListener"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.mTabsProgressListeners =
|
|
this.mTabsProgressListeners.filter(l => l != aListener);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getBrowserForTab">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
return aTab.linkedBrowser;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="showOnlyTheseTabs">
|
|
<parameter name="aTabs"/>
|
|
<body>
|
|
<![CDATA[
|
|
for (let tab of this.tabs) {
|
|
if (aTabs.indexOf(tab) == -1)
|
|
this.hideTab(tab);
|
|
else
|
|
this.showTab(tab);
|
|
}
|
|
|
|
this.tabContainer._handleTabSelect(false);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="showTab">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (aTab.hidden) {
|
|
aTab.removeAttribute("hidden");
|
|
this._visibleTabs = null; // invalidate cache
|
|
|
|
this.tabContainer.adjustTabstrip();
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TabShow", true, false);
|
|
aTab.dispatchEvent(event);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="hideTab">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
|
|
!aTab.closing) {
|
|
aTab.setAttribute("hidden", "true");
|
|
this._visibleTabs = null; // invalidate cache
|
|
|
|
this.tabContainer.adjustTabstrip();
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
let event = document.createEvent("Events");
|
|
event.initEvent("TabHide", true, false);
|
|
aTab.dispatchEvent(event);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="selectTabAtIndex">
|
|
<parameter name="aIndex"/>
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
let tabs = this.visibleTabs;
|
|
|
|
// count backwards for aIndex < 0
|
|
if (aIndex < 0)
|
|
aIndex += tabs.length;
|
|
|
|
if (aIndex >= 0 && aIndex < tabs.length)
|
|
this.selectedTab = tabs[aIndex];
|
|
|
|
if (aEvent) {
|
|
aEvent.preventDefault();
|
|
aEvent.stopPropagation();
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<property name="selectedTab">
|
|
<getter>
|
|
return this.mCurrentTab;
|
|
</getter>
|
|
<setter>
|
|
<![CDATA[
|
|
// Update the tab
|
|
this.mTabBox.selectedTab = val;
|
|
return val;
|
|
]]>
|
|
</setter>
|
|
</property>
|
|
|
|
<property name="selectedBrowser"
|
|
onget="return this.mCurrentBrowser;"
|
|
readonly="true"/>
|
|
|
|
<property name="browsers" readonly="true">
|
|
<getter>
|
|
<![CDATA[
|
|
return this._browsers ||
|
|
(this._browsers = Array.map(this.tabs, tab => tab.linkedBrowser));
|
|
]]>
|
|
</getter>
|
|
</property>
|
|
<field name="_browsers">null</field>
|
|
|
|
<!-- Moves a tab to a new browser window, unless it's already the only tab
|
|
in the current window, in which case this will do nothing. -->
|
|
<method name="replaceTabWithWindow">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aOptions"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.tabs.length == 1)
|
|
return null;
|
|
|
|
var options = "chrome,dialog=no,all";
|
|
for (var name in aOptions)
|
|
options += "," + name + "=" + aOptions[name];
|
|
|
|
// tell a new window to take the "dropped" tab
|
|
return window.openDialog(getBrowserURL(), "_blank", options, aTab);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<!-- Opens a given tab to a non-remote window. -->
|
|
<method name="openNonRemoteWindow">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!this.AppConstants.E10S_TESTING_ONLY) {
|
|
throw "This method is intended only for e10s testing!";
|
|
}
|
|
let url = aTab.linkedBrowser.currentURI.spec;
|
|
return window.openDialog("chrome://browser/content/", "_blank", "chrome,all,dialog=no,non-remote", url);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="moveTabTo">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aIndex"/>
|
|
<body>
|
|
<![CDATA[
|
|
var oldPosition = aTab._tPos;
|
|
if (oldPosition == aIndex)
|
|
return;
|
|
|
|
// Don't allow mixing pinned and unpinned tabs.
|
|
if (aTab.pinned)
|
|
aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
|
|
else
|
|
aIndex = Math.max(aIndex, this._numPinnedTabs);
|
|
if (oldPosition == aIndex)
|
|
return;
|
|
|
|
this._lastRelatedTab = null;
|
|
|
|
this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(aTab._tPos, 1)[0]);
|
|
this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(aTab._tPos, 1)[0]);
|
|
|
|
let wasFocused = (document.activeElement == this.mCurrentTab);
|
|
|
|
aIndex = aIndex < aTab._tPos ? aIndex: aIndex+1;
|
|
this.mCurrentTab._logicallySelected = false;
|
|
this.mCurrentTab._visuallySelected = false;
|
|
|
|
// invalidate caches
|
|
this._browsers = null;
|
|
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]._logicallySelected = false;
|
|
this.tabs[i]._visuallySelected = false;
|
|
}
|
|
this.mCurrentTab._logicallySelected = true;
|
|
this.mCurrentTab._visuallySelected = 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>
|
|
|
|
<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 -->
|
|
<body>
|
|
<![CDATA[
|
|
return SessionStore.duplicateTab(window, aTab);
|
|
]]>
|
|
</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, we do the following:
|
|
|
|
1. 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.
|
|
|
|
2. Suppose that the user switches from tab A to B and then
|
|
back to A. Suppose that we ask for tab A's layers to be
|
|
unloaded via message M1 after the first switch and then
|
|
request them again via message M2 once the second switch
|
|
happens. Both loading and unloading of layers happens
|
|
asynchronously, and this can cause problems. It's possible
|
|
that the content process publishes one last layer tree before
|
|
M1 is received. The parent process doesn't know that this
|
|
layer tree was published before M1 and not after M2, so it
|
|
will display the tab. However, once M1 arrives, the content
|
|
process will destroy the layer tree for A and now we will
|
|
display black for it.
|
|
|
|
To counter this problem, we keep tab A in a separate
|
|
"unloading" state until the layer tree is actually dropped in
|
|
the compositor thread. While the tab is in the "unloading"
|
|
state, we're not allowed to request layers for it. Once the
|
|
layers are dropped in the compositor, an event will fire and
|
|
we will transition the tab to the "unloaded" state. Then we
|
|
are free to request the tab's layers again.
|
|
-->
|
|
<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 timer.
|
|
unloadTimer: null, // UNLOAD_DELAY timer.
|
|
|
|
// Map from tabs to STATE_* (below).
|
|
tabState: new Map(),
|
|
|
|
// 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,
|
|
|
|
getTabState: function(tab) {
|
|
let state = this.tabState.get(tab);
|
|
if (state === undefined) {
|
|
return this.STATE_UNLOADED;
|
|
}
|
|
return state;
|
|
},
|
|
|
|
setTabState: function(tab, state) {
|
|
if (state == this.STATE_UNLOADED) {
|
|
this.tabState.delete(tab);
|
|
} else {
|
|
this.tabState.set(tab, state);
|
|
}
|
|
|
|
let browser = tab.linkedBrowser;
|
|
let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
|
|
if (state == this.STATE_LOADING) {
|
|
// Ask for a MozLayerTreeReady event.
|
|
fl.requestNotifyLayerTreeReady();
|
|
browser.docShellIsActive = true;
|
|
} else if (state == this.STATE_UNLOADING) {
|
|
// Ask for MozLayerTreeCleared event.
|
|
fl.requestNotifyLayerTreeCleared();
|
|
browser.docShellIsActive = false;
|
|
}
|
|
},
|
|
|
|
init: function() {
|
|
this.log("START");
|
|
|
|
window.addEventListener("MozAfterPaint", this);
|
|
window.addEventListener("MozLayerTreeReady", this);
|
|
window.addEventListener("MozLayerTreeCleared", this);
|
|
window.addEventListener("TabRemotenessChange", this);
|
|
this.setTabState(this.requestedTab, this.STATE_LOADED);
|
|
},
|
|
|
|
destroy: function() {
|
|
clearTimeout(this.unloadTimer);
|
|
clearTimeout(this.loadTimer);
|
|
|
|
window.removeEventListener("MozAfterPaint", this);
|
|
window.removeEventListener("MozLayerTreeReady", this);
|
|
window.removeEventListener("MozLayerTreeCleared", this);
|
|
window.removeEventListener("TabRemotenessChange", this);
|
|
|
|
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.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;
|
|
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);
|
|
throw new Error("Assertion failure");
|
|
}
|
|
},
|
|
|
|
// We've decided to try to load requestedTab.
|
|
loadRequestedTab: function() {
|
|
this.assert(!this.loadTimer);
|
|
|
|
// 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.setTabState(this.requestedTab, this.STATE_LOADING);
|
|
this.loadTimer = setTimeout(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
|
|
},
|
|
|
|
// 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, state] 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;
|
|
clearTimeout(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.
|
|
if (!this.loadTimer && this.getTabState(this.requestedTab) == this.STATE_UNLOADED) {
|
|
this.loadRequestedTab();
|
|
}
|
|
|
|
// See how many tabs still have work to do.
|
|
let numPending = 0;
|
|
for (let [tab, state] of this.tabState) {
|
|
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.preActions();
|
|
|
|
let numPending = 0;
|
|
|
|
// Unload any tabs that can be unloaded.
|
|
for (let [tab, state] of this.tabState) {
|
|
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 = setTimeout(() => 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) {
|
|
this.logState("onLayersReady");
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
this.setTabState(tab, this.STATE_LOADED);
|
|
|
|
this.finishTabSwitch();
|
|
|
|
if (this.loadingTab === tab) {
|
|
clearTimeout(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.finishTabSwitch();
|
|
},
|
|
|
|
// Called when we're done clearing the layers for a tab.
|
|
onLayersCleared: function(browser) {
|
|
this.logState("onLayersCleared");
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
if (tab) {
|
|
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");
|
|
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 the user asks to switch to a given tab.
|
|
requestTab: function(tab) {
|
|
if (tab === this.requestedTab) {
|
|
return;
|
|
}
|
|
|
|
// Instrumentation to figure out bug 1166351 - if the binding
|
|
// on the browser we're switching to has gone away, try to find out
|
|
// why. We should remove this and the checkBrowserBindingAlive
|
|
// method once bug 1166351 has been closed.
|
|
if (this.tabbrowser.AppConstants.E10S_TESTING_ONLY &&
|
|
!this.checkBrowserBindingAlive(tab)) {
|
|
Cu.reportError("Please report the above errors in bug 1166351.");
|
|
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();
|
|
|
|
clearTimeout(this.unloadTimer);
|
|
this.unloadTimer = setTimeout(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
|
|
|
|
this.postActions();
|
|
},
|
|
|
|
handleEvent: function(event, delayed = false) {
|
|
if (this._processing) {
|
|
setTimeout(() => 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);
|
|
}
|
|
|
|
this.postActions();
|
|
this._processing = false;
|
|
},
|
|
|
|
/*
|
|
* Telemetry and Profiler related helpers for recording tab switch
|
|
* timing.
|
|
*/
|
|
|
|
startTabSwitch: function () {
|
|
TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
this.addMarker("AsyncTabSwitch:Start");
|
|
},
|
|
|
|
finishTabSwitch: function () {
|
|
if (this.requestedTab && this.getTabState(this.requestedTab) == this.STATE_LOADED) {
|
|
// After this point the tab has switched from the content thread's point of view.
|
|
// The changes will be visible after the next refresh driver tick + composite.
|
|
let event = new CustomEvent("TabSwitched", {
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
this.tabbrowser.dispatchEvent(event);
|
|
let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
if (time != -1) {
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
this.log("DEBUG: tab switch time = " + time);
|
|
this.addMarker("AsyncTabSwitch:Finish");
|
|
}
|
|
}
|
|
},
|
|
|
|
spinnerDisplayed: function () {
|
|
this.assert(!this.spinnerTab);
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
|
|
this.addMarker("AsyncTabSwitch:SpinnerShown");
|
|
},
|
|
|
|
spinnerHidden: function () {
|
|
this.assert(this.spinnerTab);
|
|
this.log("DEBUG: spinner time = " +
|
|
TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
|
|
this.addMarker("AsyncTabSwitch:SpinnerHidden");
|
|
// we do not get a onPaint after displaying the spinner
|
|
this.finishTabSwitch();
|
|
},
|
|
|
|
addMarker: function(marker) {
|
|
if (Services.profiler) {
|
|
Services.profiler.AddMarker(marker);
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Debug related logging for switcher.
|
|
*/
|
|
|
|
_useDumpForLogging: false,
|
|
_logInit: false,
|
|
|
|
logging: function () {
|
|
if (this._useDumpForLogging)
|
|
return true;
|
|
if (this._logInit)
|
|
return this._shouldLog;
|
|
let result = false;
|
|
try {
|
|
result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming");
|
|
} catch (ex) {
|
|
}
|
|
this._shouldLog = result;
|
|
this._logInit = true;
|
|
return this._shouldLog;
|
|
},
|
|
|
|
// Instrumentation for bug 1166351
|
|
checkBrowserBindingAlive: function(tab) {
|
|
let err = Cu.reportError;
|
|
|
|
if (!tab.linkedBrowser) {
|
|
err("Attempting to switch to tab that has no linkedBrowser.");
|
|
return false;
|
|
}
|
|
|
|
let b = tab.linkedBrowser;
|
|
|
|
if (!b._alive) {
|
|
// The browser binding has been removed. Dump a bunch of
|
|
// diagnostic information to the browser console.
|
|
let utils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
|
let results = utils.getBindingURLs(b);
|
|
let urls = [];
|
|
|
|
for (let i = 0; i < results.length; ++i) {
|
|
urls.push(results.queryElementAt(i, Ci.nsIURI).spec);
|
|
}
|
|
err("The browser has the following bindings:");
|
|
err(urls);
|
|
err("MozBinding is currently: " +
|
|
window.getComputedStyle(b).MozBinding);
|
|
if (!b.parentNode) {
|
|
err("Browser was removed from the DOM.");
|
|
} else {
|
|
err("Parent is: " + b.parentNode.outerHTML);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
tinfo: function(tab) {
|
|
if (tab) {
|
|
return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
|
|
} else {
|
|
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) dump("V");
|
|
if (tab === this.loadingTab) dump("L");
|
|
if (tab === this.requestedTab) dump("R");
|
|
if (state == this.STATE_LOADED) dump("(+)");
|
|
if (state == this.STATE_LOADING) dump("(+?)");
|
|
if (state == this.STATE_UNLOADED) dump("(-)");
|
|
if (state == this.STATE_UNLOADING) dump("(-?)");
|
|
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>
|
|
|
|
<method name="attachFormFill">
|
|
<body><![CDATA[
|
|
for (let browser of this.browsers) {
|
|
browser.attachFormFill();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="detachFormFill">
|
|
<body><![CDATA[
|
|
for (let browser of this.browsers) {
|
|
browser.detachFormFill();
|
|
}
|
|
]]></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="userTypedClear"
|
|
onget="return this.mCurrentBrowser.userTypedClear;"
|
|
onset="return this.mCurrentBrowser.userTypedClear = val;"/>
|
|
|
|
<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 {
|
|
stringID = tab.linkedBrowser.audioMuted ?
|
|
"tabs.unmuteAudio.background.tooltip" :
|
|
"tabs.muteAudio.background.tooltip";
|
|
label = this.mStringBundle.getString(stringID);
|
|
}
|
|
} else {
|
|
label = tab.getAttribute("label") +
|
|
(this.AppConstants.E10S_TESTING_ONLY && tab.linkedBrowser && tab.linkedBrowser.isRemoteBrowser ? " - e10s" : "");
|
|
}
|
|
event.target.setAttribute("label", label);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleEvent">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
switch (aEvent.type) {
|
|
case "keydown":
|
|
this._handleKeyDownEvent(aEvent);
|
|
break;
|
|
case "keypress":
|
|
this._handleKeyPressEventMac(aEvent);
|
|
break;
|
|
case "sizemodechange":
|
|
if (aEvent.target == window) {
|
|
this.mCurrentBrowser.setDocShellIsActiveAndForeground(
|
|
window.windowState != window.STATE_MINIMIZED);
|
|
}
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="receiveMessage">
|
|
<parameter name="aMessage"/>
|
|
<body><![CDATA[
|
|
let json = aMessage.json;
|
|
let browser = aMessage.target;
|
|
|
|
switch (aMessage.name) {
|
|
case "DOMTitleChanged": {
|
|
let tab = this.getTabForBrowser(browser);
|
|
if (!tab || tab.hasAttribute("pending"))
|
|
return;
|
|
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;
|
|
}
|
|
|
|
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 = aMessage.data.spellInfo;
|
|
if (spellInfo)
|
|
spellInfo.target = aMessage.target.messageManager;
|
|
let documentURIObject = makeURI(aMessage.data.docLocation,
|
|
aMessage.data.charSet,
|
|
makeURI(aMessage.data.baseURI));
|
|
gContextMenuContentData = { isRemote: true,
|
|
event: aMessage.objects.event,
|
|
popupNode: aMessage.objects.popupNode,
|
|
browser: browser,
|
|
editFlags: aMessage.data.editFlags,
|
|
spellInfo: spellInfo,
|
|
principal: aMessage.data.principal,
|
|
customMenuItems: aMessage.data.customMenuItems,
|
|
addonInfo: aMessage.data.addonInfo,
|
|
documentURIObject: documentURIObject,
|
|
docLocation: aMessage.data.docLocation,
|
|
charSet: aMessage.data.charSet,
|
|
referrer: aMessage.data.referrer,
|
|
referrerPolicy: aMessage.data.referrerPolicy,
|
|
contentType: aMessage.data.contentType,
|
|
contentDisposition: aMessage.data.contentDisposition,
|
|
frameOuterWindowID: aMessage.data.frameOuterWindowID,
|
|
selectionInfo: aMessage.data.selectionInfo,
|
|
disableSetDesktopBackground: aMessage.data.disableSetDesktopBg,
|
|
loginFillInfo: aMessage.data.loginFillInfo,
|
|
};
|
|
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;
|
|
this.selectedTab = tab;
|
|
window.focus();
|
|
break;
|
|
}
|
|
case "Browser:Init": {
|
|
let tab = this.getTabForBrowser(browser);
|
|
if (!tab)
|
|
return;
|
|
|
|
this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
|
|
browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
|
|
break;
|
|
}
|
|
case "Findbar:Keypress":
|
|
if (!gFindBarInitialized) {
|
|
// If the find bar for this tab is not yet alive, change that,
|
|
// and make sure we return the result:
|
|
return gFindBar.receiveMessage(aMessage);
|
|
}
|
|
break;
|
|
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="observe">
|
|
<parameter name="aSubject"/>
|
|
<parameter name="aTopic"/>
|
|
<parameter name="aData"/>
|
|
<body><![CDATA[
|
|
if (aTopic == "live-resize-start") {
|
|
let browser = this.mCurrentTab.linkedBrowser;
|
|
let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
|
|
if (fl && fl.tabParent && !this.mActiveResizeDisplayportSuppression) {
|
|
fl.tabParent.suppressDisplayport(true);
|
|
this.mActiveResizeDisplayportSuppression = browser;
|
|
}
|
|
} else if (aTopic == "live-resize-end") {
|
|
let browser = this.mActiveResizeDisplayportSuppression;
|
|
if (browser) {
|
|
let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
|
|
if (fl && fl.tabParent) {
|
|
fl.tabParent.suppressDisplayport(false);
|
|
this.mActiveResizeDisplayportSuppression = null;
|
|
}
|
|
}
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<constructor>
|
|
<![CDATA[
|
|
let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack");
|
|
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._tPos = 0;
|
|
this.mCurrentTab._fullyOpen = true;
|
|
this.mCurrentTab.lastAccessed = Infinity;
|
|
this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
|
|
this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
|
|
|
|
// set up the shared autoscroll popup
|
|
this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
|
|
this._autoScrollPopup.id = "autoscroller";
|
|
this.appendChild(this._autoScrollPopup);
|
|
this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
|
|
this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
|
|
this.updateWindowResizers();
|
|
|
|
// Hook up the event listeners to the first browser
|
|
var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
|
|
const nsIWebProgress = Components.interfaces.nsIWebProgress;
|
|
const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
|
|
.createInstance(nsIWebProgress);
|
|
filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
|
|
this.mTabListeners[0] = tabListener;
|
|
this.mTabFilters[0] = 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("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 (var i = 0; i < this.mTabListeners.length; ++i) {
|
|
let browser = this.getBrowserAtIndex(i);
|
|
if (browser.registeredOpenURI) {
|
|
this._placesAutocomplete.unregisterOpenPage(browser.registeredOpenURI);
|
|
this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI);
|
|
delete browser.registeredOpenURI;
|
|
}
|
|
browser.webProgress.removeProgressListener(this.mTabFilters[i]);
|
|
this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]);
|
|
this.mTabFilters[i] = null;
|
|
this.mTabListeners[i].destroy();
|
|
this.mTabListeners[i] = null;
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
]]>
|
|
</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>
|
|
</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, and we're not in tabview/panorama with
|
|
// the prompt being a beforeunload one, we won't switch tabs
|
|
// (unless this behaviour has been disabled entirely using the pref).
|
|
if (event.detail && event.detail.tabPrompt &&
|
|
!(event.detail.inPermitUnload && ("TabView" in window) && TabView.isVisible()) &&
|
|
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 title = browser.contentTitle;
|
|
let uri = browser.currentURI;
|
|
let icon = browser.mIconURL;
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (this.selectedBrowser == browser) {
|
|
this.updateBrowserRemotenessByURL(browser, "about:tabcrashed");
|
|
browser.setAttribute("crashedPageTitle", title);
|
|
browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
|
|
browser.removeAttribute("crashedPageTitle");
|
|
tab.setAttribute("crashed", true);
|
|
} else {
|
|
this.updateBrowserRemoteness(browser, false);
|
|
SessionStore.reviveCrashedTab(tab);
|
|
}
|
|
|
|
tab.removeAttribute("soundplaying");
|
|
this.setIcon(tab, icon);
|
|
]]>
|
|
</handler>
|
|
<handler event="DOMAudioPlaybackStarted">
|
|
<![CDATA[
|
|
if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
|
|
!event.isTrusted)
|
|
return;
|
|
|
|
var browser = event.originalTarget;
|
|
var tab = this.getTabForBrowser(browser);
|
|
if (!tab)
|
|
return;
|
|
|
|
if (!tab.hasAttribute("soundplaying")) {
|
|
tab.setAttribute("soundplaying", true);
|
|
this._tabAttrModified(tab, ["soundplaying"]);
|
|
}
|
|
]]>
|
|
</handler>
|
|
<handler event="DOMAudioPlaybackStopped">
|
|
<![CDATA[
|
|
if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
|
|
!event.isTrusted)
|
|
return;
|
|
|
|
var browser = event.originalTarget;
|
|
var tab = this.getTabForBrowser(browser);
|
|
if (!tab)
|
|
return;
|
|
|
|
if (tab.hasAttribute("soundplaying")) {
|
|
tab.removeAttribute("soundplaying");
|
|
this._tabAttrModified(tab, ["soundplaying"]);
|
|
}
|
|
]]>
|
|
</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);
|
|
|
|
try {
|
|
this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled");
|
|
} catch (ex) {
|
|
this._tabAnimationLoggingEnabled = 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._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.MozMarginStart = - (width + scrollButtonWidth + paddingStart) + "px";
|
|
}
|
|
|
|
this.style.MozPaddingStart = width + paddingStart + "px";
|
|
|
|
} else {
|
|
this.removeAttribute("positionpinnedtabs");
|
|
|
|
for (let i = 0; i < numPinned; i++) {
|
|
let tab = this.childNodes[i];
|
|
tab.style.MozMarginStart = "";
|
|
}
|
|
|
|
this.style.MozPaddingStart = "";
|
|
}
|
|
|
|
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;
|
|
|
|
let draggingRight = screenX > draggedTab._dragData.animLastScreenX;
|
|
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();
|
|
break;
|
|
case "resize":
|
|
if (aEvent.target != window)
|
|
break;
|
|
|
|
TabsInTitlebar.updateAppearance();
|
|
|
|
var width = this.mTabstrip.boxObject.width;
|
|
if (width != this.mTabstripWidth) {
|
|
this.adjustTabstrip();
|
|
this._fillTrailingGap();
|
|
this._handleTabSelect();
|
|
this.mTabstripWidth = width;
|
|
}
|
|
|
|
this.tabbrowser.updateWindowResizers();
|
|
break;
|
|
case "mouseout":
|
|
// If the "related target" (the node to which the pointer went) is not
|
|
// a child of the current document, the mouse just left the window.
|
|
let relatedTarget = aEvent.relatedTarget;
|
|
if (relatedTarget && relatedTarget.ownerDocument == document)
|
|
break;
|
|
case "mousemove":
|
|
if (document.getElementById("tabContextMenu").state != "open")
|
|
this._unlockTabSizing();
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<field name="_animateElement">
|
|
this.mTabstrip._scrollButtonDown;
|
|
</field>
|
|
|
|
<method name="_notifyBackgroundTab">
|
|
<parameter name="aTab"/>
|
|
<body><![CDATA[
|
|
if (aTab.pinned)
|
|
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;
|
|
// Disallow dropping multiple items
|
|
if (dt.mozItemCount > 1)
|
|
return "none";
|
|
|
|
var types = dt.mozTypesAt(0);
|
|
var sourceNode = null;
|
|
// tabs are always added as the first type
|
|
if (types[0] == TAB_DROP_TYPE) {
|
|
var sourceNode = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (sourceNode instanceof XULElement &&
|
|
sourceNode.localName == "tab" &&
|
|
sourceNode.ownerDocument.defaultView instanceof ChromeWindow &&
|
|
sourceNode.ownerDocument.documentElement.getAttribute("windowtype") == "navigator:browser" &&
|
|
sourceNode.ownerDocument.defaultView.gBrowser.tabContainer == sourceNode.parentNode) {
|
|
// Do not allow transfering a private tab to a non-private window
|
|
// and vice versa.
|
|
if (PrivateBrowsingUtils.isWindowPrivate(window) !=
|
|
PrivateBrowsingUtils.isWindowPrivate(sourceNode.ownerDocument.defaultView))
|
|
return "none";
|
|
|
|
if (window.gMultiProcessBrowser !=
|
|
sourceNode.ownerDocument.defaultView.gMultiProcessBrowser)
|
|
return "none";
|
|
|
|
return dt.dropEffect == "copy" ? "copy" : "move";
|
|
}
|
|
}
|
|
|
|
if (browserDragAndDrop.canDropLink(event)) {
|
|
return "link";
|
|
}
|
|
return "none";
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_handleNewTab">
|
|
<parameter name="tab"/>
|
|
<body><![CDATA[
|
|
if (tab.parentNode != this)
|
|
return;
|
|
tab._fullyOpen = true;
|
|
|
|
this.adjustTabstrip();
|
|
|
|
if (tab.getAttribute("selected") == "true") {
|
|
this._fillTrailingGap();
|
|
this._handleTabSelect();
|
|
} else {
|
|
this._notifyBackgroundTab(tab);
|
|
}
|
|
|
|
// XXXmano: this is a temporary workaround for bug 345399
|
|
// We need to manually update the scroll buttons disabled state
|
|
// if a tab was inserted to the overflow area or removed from it
|
|
// without any scrolling and when the tabbar has already
|
|
// overflowed.
|
|
this.mTabstrip._updateScrollButtonsDisabledState();
|
|
|
|
// Preload the next about:newtab if there isn't one already.
|
|
this.tabbrowser._createPreloadBrowser();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_canAdvanceToTab">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
return !aTab.closing;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_handleTabTelemetryStart">
|
|
<parameter name="aTab"/>
|
|
<parameter name="aURI"/>
|
|
<body>
|
|
<![CDATA[
|
|
// Animation-smoothness telemetry/logging
|
|
if (Services.telemetry.canRecordExtended || this._tabAnimationLoggingEnabled) {
|
|
if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) {
|
|
// Indicate newtab page animation where other tabs are unaffected
|
|
// (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute)
|
|
aTab._recordingTabOpenPlain = true;
|
|
}
|
|
aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils)
|
|
.startFrameTimeRecording();
|
|
}
|
|
|
|
// Overall animation duration
|
|
aTab._animStartTime = Date.now();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="_handleTabTelemetryEnd">
|
|
<parameter name="aTab"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!aTab._animStartTime) {
|
|
return;
|
|
}
|
|
|
|
Services.telemetry.getHistogramById(aTab.closing ?
|
|
"FX_TAB_ANIM_CLOSE_MS" :
|
|
"FX_TAB_ANIM_OPEN_MS")
|
|
.add(Date.now() - aTab._animStartTime);
|
|
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: true});
|
|
} else if (event.originalTarget.localName == "box") {
|
|
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;
|
|
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.setAttribute("type", "drag");
|
|
this._dndPanel.appendChild(canvas);
|
|
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;
|
|
}
|
|
dt.setDragImage(toDrag, -16 * scale, -16 * scale);
|
|
|
|
// _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.MozMarginStart = (-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) {
|
|
// 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 newIndex = this._getDropIndex(event, false);
|
|
let newTab = this.tabbrowser.addTab("about:blank");
|
|
let newBrowser = this.tabbrowser.getBrowserForTab(newTab);
|
|
let draggedBrowserURL = draggedTab.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.tabbrowser.updateBrowserRemotenessByURL(newBrowser,
|
|
draggedBrowserURL);
|
|
|
|
// Stop the about:blank load
|
|
newBrowser.stop();
|
|
// make sure it has a docshell
|
|
newBrowser.docShell;
|
|
|
|
let numPinned = this.tabbrowser._numPinnedTabs;
|
|
if (newIndex < numPinned || draggedTab.pinned && newIndex == numPinned)
|
|
this.tabbrowser.pinTab(newTab);
|
|
this.tabbrowser.moveTabTo(newTab, newIndex);
|
|
|
|
// 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.tabbrowser.selectedTab = newTab;
|
|
|
|
draggedTab.parentNode._finishAnimateTabMove();
|
|
this.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab);
|
|
|
|
// Call updateCurrentBrowser to make sure the URL bar is up to date
|
|
// for our new tab after we've done swapBrowsersAndCloseOther.
|
|
this.tabbrowser.updateCurrentBrowser(true);
|
|
} else {
|
|
// Pass true to disallow dropping javascript: or data: urls
|
|
let url;
|
|
try {
|
|
url = browserDragAndDrop.drop(event, { }, true);
|
|
} catch (ex) {}
|
|
|
|
if (!url)
|
|
return;
|
|
|
|
let bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground");
|
|
|
|
if (event.shiftKey)
|
|
bgLoad = !bgLoad;
|
|
|
|
let tab = this._getDragTargetTab(event, true);
|
|
if (!tab || dropEffect == "copy") {
|
|
// We're adding a new tab.
|
|
let newIndex = this._getDropIndex(event, true);
|
|
let newTab = this.tabbrowser.loadOneTab(url, {inBackground: bgLoad, allowThirdPartyFixup: true});
|
|
this.tabbrowser.moveTabTo(newTab, newIndex);
|
|
} else {
|
|
// Load in an existing tab.
|
|
try {
|
|
let webNav = Ci.nsIWebNavigation;
|
|
let flags = webNav.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
|
|
webNav.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
|
|
this.tabbrowser.getBrowserForTab(tab).loadURIWithFlags(url, flags);
|
|
if (!bgLoad)
|
|
this.selectedItem = tab;
|
|
} catch(ex) {
|
|
// Just ignore invalid urls
|
|
}
|
|
}
|
|
}
|
|
|
|
if (draggedTab) {
|
|
delete draggedTab._dragData;
|
|
}
|
|
]]></handler>
|
|
|
|
<handler event="dragend"><![CDATA[
|
|
// Note: while this case is correctly handled here, this event
|
|
// isn't dispatched when the tab is moved within the tabstrip,
|
|
// see bug 460801.
|
|
|
|
this._finishAnimateTabMove();
|
|
|
|
var dt = event.dataTransfer;
|
|
var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0);
|
|
if (dt.mozUserCancelled || dt.dropEffect != "none" || this._isCustomizing) {
|
|
delete draggedTab._dragData;
|
|
return;
|
|
}
|
|
|
|
// Disable detach within the browser toolbox
|
|
var eX = event.screenX;
|
|
var eY = event.screenY;
|
|
var wX = window.screenX;
|
|
// check if the drop point is horizontally within the window
|
|
if (eX > wX && eX < (wX + window.outerWidth)) {
|
|
let bo = this.mTabstrip.boxObject;
|
|
// also avoid detaching if the the tab was dropped too close to
|
|
// the tabbar (half a tab)
|
|
let endScreenY = bo.screenY + 1.5 * bo.height;
|
|
if (eY < endScreenY && eY > window.screenY)
|
|
return;
|
|
}
|
|
|
|
// screen.availLeft et. al. only check the screen that this window is on,
|
|
// but we want to look at the screen the tab is being dropped onto.
|
|
var sX = {}, sY = {}, sWidth = {}, sHeight = {};
|
|
Cc["@mozilla.org/gfx/screenmanager;1"]
|
|
.getService(Ci.nsIScreenManager)
|
|
.screenForRect(eX, eY, 1, 1)
|
|
.GetAvailRect(sX, sY, sWidth, sHeight);
|
|
// ensure new window entirely within screen
|
|
var winWidth = Math.min(window.outerWidth, sWidth.value);
|
|
var winHeight = Math.min(window.outerHeight, sHeight.value);
|
|
var left = Math.min(Math.max(eX - draggedTab._dragData.offsetX, sX.value),
|
|
sX.value + sWidth.value - winWidth);
|
|
var top = Math.min(Math.max(eY - draggedTab._dragData.offsetY, sY.value),
|
|
sY.value + sHeight.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: true});
|
|
// 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,fadein,pinned,selected,visuallyselected,busy,crashed"
|
|
anonid="tab-icon-image"
|
|
class="tab-icon-image"
|
|
validate="never"
|
|
role="presentation"/>
|
|
<xul:image xbl:inherits="crashed,busy,soundplaying,pinned,muted"
|
|
anonid="overlay-icon"
|
|
class="tab-icon-overlay"
|
|
role="presentation"/>
|
|
<xul:label flex="1"
|
|
anonid="tab-label"
|
|
xbl:inherits="value=visibleLabel,crop,accesskey,fadein,pinned,selected,visuallyselected,attention"
|
|
class="tab-text tab-label"
|
|
role="presentation"/>
|
|
<xul:image xbl:inherits="soundplaying,pinned,muted,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>
|
|
|
|
<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="_logicallySelected">
|
|
<setter>
|
|
<![CDATA[
|
|
if (val)
|
|
this.setAttribute("selected", "true");
|
|
else
|
|
this.removeAttribute("selected");
|
|
|
|
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.
|
|
this._logicallySelected = val;
|
|
|
|
// If we're non-e10s we should update the visual selection as well at the same time
|
|
if (!gMultiProcessBrowser) {
|
|
this._visuallySelected = val;
|
|
}
|
|
|
|
return val;
|
|
]]>
|
|
</setter>
|
|
</property>
|
|
|
|
<property name="label">
|
|
<getter>
|
|
return this.getAttribute("label");
|
|
</getter>
|
|
<setter>
|
|
this.setAttribute("label", val);
|
|
let event = new CustomEvent("TabLabelModified", {
|
|
bubbles: true,
|
|
cancelable: true
|
|
});
|
|
this.dispatchEvent(event);
|
|
|
|
// Let listeners prevent synchronizing the actual label to the
|
|
// visible label (allowing them to override the visible label).
|
|
if (!event.defaultPrevented)
|
|
this.visibleLabel = val;
|
|
</setter>
|
|
</property>
|
|
<property name="visibleLabel">
|
|
<getter>
|
|
return this.getAttribute("visibleLabel");
|
|
</getter>
|
|
<setter>
|
|
this.setAttribute("visibleLabel", 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="lastAccessed">
|
|
<getter>
|
|
return this._lastAccessed == Infinity ? Date.now() : this._lastAccessed;
|
|
</getter>
|
|
<setter>
|
|
this._lastAccessed = val;
|
|
</setter>
|
|
</property>
|
|
|
|
<field name="mOverCloseButton">false</field>
|
|
<property name="_overPlayingIcon" readonly="true">
|
|
<getter><![CDATA[
|
|
let iconVisible = this.hasAttribute("soundplaying") ||
|
|
this.hasAttribute("muted");
|
|
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">
|
|
<body>
|
|
<![CDATA[
|
|
let tabContainer = this.parentNode;
|
|
let browser = this.linkedBrowser;
|
|
if (browser.audioMuted) {
|
|
browser.unmute();
|
|
this.removeAttribute("muted");
|
|
} else {
|
|
browser.mute();
|
|
this.setAttribute("muted", "true");
|
|
}
|
|
tabContainer.tabbrowser._tabAttrModified(this, ["muted"]);
|
|
]]>
|
|
</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) // "Tab Groups" menuitem and its menuseparator
|
|
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]);
|
|
let fromTab = this._selectedPanel ? this.getRelatedElement(this._selectedPanel)
|
|
: null;
|
|
|
|
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>
|