diff options
Diffstat (limited to 'webbrowser/base/content/tabbrowser.xml')
-rw-r--r-- | webbrowser/base/content/tabbrowser.xml | 5403 |
1 files changed, 5403 insertions, 0 deletions
diff --git a/webbrowser/base/content/tabbrowser.xml b/webbrowser/base/content/tabbrowser.xml new file mode 100644 index 0000000..b5edd54 --- /dev/null +++ b/webbrowser/base/content/tabbrowser.xml @@ -0,0 +1,5403 @@ +<?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/. --> + +<!DOCTYPE bindings [ +<!ENTITY % tabBrowserDTD SYSTEM "chrome://browser/locale/tabbrowser.dtd" > +%tabBrowserDTD; +]> + +<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"> + <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" disablehistory="true" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,datetimepicker,authdosprotected"/> + </xul:stack> + </xul:vbox> + </xul:hbox> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIMessageListener"> + + <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, + function (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="mTabBox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); + </field> + <field name="mPanelContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); + </field> + <field name="mStringBundle"> + document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle"); + </field> + <field name="mCurrentTab"> + null + </field> + <field name="_lastRelatedTab"> + null + </field> + <field name="mCurrentBrowser"> + null + </field> + <field name="mProgressListeners"> + [] + </field> + <field name="mTabsProgressListeners"> + [] + </field> + <field name="mTabListeners"> + [] + </field> + <field name="mTabFilters"> + [] + </field> + <field name="mIsBusy"> + false + </field> + <field name="_outerWindowIDBrowserMap"> + new Map(); + </field> + <field name="arrowKeysShouldWrap" readonly="true"> +#ifdef XP_MACOSX + true +#else + false +#endif + </field> + + <field name="_autoScrollPopup"> + null + </field> + + <field name="_previewMode"> + false + </field> + + <property name="_numPinnedTabs" readonly="true"> + <getter><![CDATA[ + for (var i = 0; i < this.tabs.length; i++) { + if (!this.tabs[i].pinned) + break; + } + return i; + ]]></getter> + </property> + + <property name="popupAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; + } + let stack = this.mCurrentBrowser.parentNode; + // Create an anchor for the popup + const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let popupAnchor = document.createElementNS(NS_XUL, "hbox"); + popupAnchor.className = "popup-anchor"; + popupAnchor.hidden = true; + stack.appendChild(popupAnchor); + return this.mCurrentTab._popupAnchor = popupAnchor; + ]]></getter> + </property> + + <method name="updateWindowResizers"> + <body><![CDATA[ + if (!window.gShowPageResizers) + return; + + var show = document.getElementById("addon-bar").collapsed && + 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).docShell.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.setAttribute("fadein", "true"); + aTab.removeAttribute("pinned"); + aTab.style.MozMarginStart = ""; + this.tabContainer._unlockTabSizing(); + this.tabContainer._positionPinnedTabs(); + this.tabContainer.adjustTabstrip(); + + this.getBrowserForTab(aTab).docShell.isAppTab = false; + + if (aTab.selected) + this._setCloseKeyState(true); + + let event = document.createEvent("Events"); + event.initEvent("TabUnpinned", true, false); + aTab.dispatchEvent(event); + ]]></body> + </method> + + <method name="previewTab"> + <parameter name="aTab"/> + <parameter name="aCallback"/> + <body> + <![CDATA[ + let currentTab = this.selectedTab; + try { + // Suppress focus, ownership and selected tab changes + this._previewMode = true; + this.selectedTab = aTab; + aCallback(); + } finally { + this.selectedTab = currentTab; + this._previewMode = false; + } + ]]> + </body> + </method> + + <method name="getBrowserAtIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.browsers[aIndex]; + ]]> + </body> + </method> + + <method name="getBrowserIndexForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab._tPos : -1; + ]]> + </body> + </method> + + <method name="getBrowserForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aDocument.defaultView); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + var tab = this._getTabForContentWindow(aWindow); + return tab ? tab.linkedBrowser : null; + ]]> + </body> + </method> + + <method name="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + return this._outerWindowIDBrowserMap.get(aID); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + for (let i = 0; i < this.browsers.length; i++) { + if (this.browsers[i].contentWindow == 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[ + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + let browser = (aBrowser || this.mCurrentBrowser); + let stack = browser.parentNode; + let self = this; + + let promptBox = { + appendPrompt : function(args, onCloseCallback) { + let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt"); + // stack.appendChild(newPrompt); + stack.insertBefore(newPrompt, browser.nextSibling); + browser.setAttribute("tabmodalPromptShowing", true); + + newPrompt.clientTop; // style flush to assure binding is attached + + let prompts = this.listPrompts(); + if (prompts.length > 1) { + // Let's hide ourself behind the current prompt. + newPrompt.hidden = true; + } + + let tab = self._getTabForContentWindow(browser.contentWindow); + newPrompt.init(args, tab, onCloseCallback); + return newPrompt; + }, + + removePrompt : function(aPrompt) { + stack.removeChild(aPrompt); + + let prompts = this.listPrompts(); + if (prompts.length) { + let prompt = prompts[prompts.length - 1]; + prompt.hidden = false; + prompt.Dialog.setDefaultFocus(); + } else { + browser.removeAttribute("tabmodalPromptShowing"); + browser.focus(); + } + }, + + listPrompts : function(aPrompt) { + let els = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt"); + // NodeList --> real JS array + let prompts = Array.slice(els); + return prompts; + }, + }; + + return promptBox; + ]]> + </body> + </method> + + <method name="getTabFromAudioEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") || + !aEvent.isTrusted) { + return null; + } + + var browser = aEvent.originalTarget; + var tab = this.getTabForBrowser(browser); + return tab; + ]]> + </body> + </method> + + <method name="_callProgressListeners"> + <parameter name="aBrowser"/> + <parameter name="aMethod"/> + <parameter name="aArguments"/> + <parameter name="aCallGlobalListeners"/> + <parameter name="aCallTabsListeners"/> + <body><![CDATA[ + var rv = true; + + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + this.mProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + this.mTabsProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + if (!p[aMethod].apply(p, aArguments)) + rv = false; + } catch (e) { + // don't inhibit other listeners + Components.utils.reportError(e); + } + } + }); + } + + 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"/> + <body> + <![CDATA[ + return ({ + mTabBrowser: this, + mTab: aTab, + mBrowser: aBrowser, + mBlank: aStartsBlank, + + // cache flags for correct status UI update after tab switching + mStateFlags: 0, + 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; + + if (gMultiProcessBrowser) + return true; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + try { + let channel = aRequest.QueryInterface(Ci.nsIChannel); + if (channel.originalURI.schemeIs("about") && + (channel.URI.schemeIs("jar") || channel.URI.schemeIs("file"))) + return false; + } catch (e) {} + + 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; + let location, originalLocation; + try { + aRequest.QueryInterface(nsIChannel) + location = aRequest.URI; + originalLocation = aRequest.originalURI; + } catch (ex) {} + + if (aStateFlags & nsIWebProgressListener.STATE_START) { + this.mRequestCount++; + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP) { + const NS_ERROR_UNKNOWN_HOST = 2152398878; + if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) { + // to prevent bug 235825: wait for the request handled + // by the automatic keyword resolver + return; + } + // since we (try to) only handle STATE_STOP of the last request, + // the count of open requests should now be 0 + this.mRequestCount = 0; + } + + if (aStateFlags & nsIWebProgressListener.STATE_START && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + if (aWebProgress.isTopLevel) + this.mBrowser.urlbarChangeTracker.startedLoad(); + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { + this.mTab.setAttribute("busy", "true"); + if (!gMultiProcessBrowser) { + if (aWebProgress.isTopLevel && + !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); + } + } + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = true; + } + } + else if (aStateFlags & nsIWebProgressListener.STATE_STOP && + aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) { + + if (this.mTab.hasAttribute("busy")) { + this.mTab.removeAttribute("busy"); + this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + let isSuccessful = Components.isSuccessCode(aStatus); + if (!isSuccessful && !isTabEmpty(this.mTab)) { + // Restore the current document's location in case the + // request was stopped (possibly from a content script) + // before the location changed. + + this.mBrowser.userTypedValue = null; + + if (this.mTab.selected && gURLBar) + URLBarSetURI(); + } else if (isSuccessful) { + this.mBrowser.urlbarChangeTracker.finishedLoad(); + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + if (this.mBlank) + this.mBlank = false; + + // 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) { + let isSameDocument = + !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + // We need to clear the typed value + // if the document failed to load, to make sure the urlbar reflects the + // failed URI (particularly for SSL errors). However, don't clear the value + // if the error page's URI is about:blank, because that causes complete + // loss of urlbar contents for invalid URI errors (see bug 867957). + if (this.mBrowser.didStartLoadSinceLastUserTyping() || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank")) + this.mBrowser.userTypedValue = null; + + // If the browser was playing audio, we should remove the playing state. + if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) { + clearTimeout(this.mTab._soundPlayingAttrRemovalTimer); + this.mTab._soundPlayingAttrRemovalTimer = 0; + this.mTab.removeAttribute("soundplaying"); + this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]); + } + + // If the browser was previously muted, we should restore the muted state. + if (this.mTab.hasAttribute("muted")) { + this.mTab.linkedBrowser.mute(); + } + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState. See bug 550565. + if (!gMultiProcessBrowser) { + if (aWebProgress.isLoadingDocument && + !(this.mBrowser.docShell.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)) + this.mBrowser.mIconURL = null; + } + + let autocomplete = this.mTabBrowser._placesAutocomplete; + if (this.mBrowser.registeredOpenURI) { + autocomplete.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); + 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"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let 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); + } + // We do not serialize the principal from within SessionStore.jsm, + // hence if aLoadingPrincipal is null we default to the + // systemPrincipal which will allow the favicon to load. + let loadingPrincipal = aLoadingPrincipal + ? aLoadingPrincipal + : Services.scriptSecurityManager.getSystemPrincipal(); + let loadType = PrivateBrowsingUtils.isWindowPrivate(window) + ? this.mFaviconService.FAVICON_LOAD_PRIVATE + : this.mFaviconService.FAVICON_LOAD_NON_PRIVATE; + + this.mFaviconService.setAndFetchFaviconForPage( + browser.currentURI, aURI, false, loadType, null, loadingPrincipal); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl != aTab.getAttribute("image")) { + if (sizedIconUrl) + aTab.setAttribute("image", sizedIconUrl); + else + aTab.removeAttribute("image"); + this._tabAttrModified(aTab, ["image"]); + } + + if (Services.prefs.getBoolPref("browser.chrome.favicons.process")) { + let favImage = new Image; + favImage.src = browser.mIconURL; + var tabBrowser = this; + favImage.onload = function () { + try { + // Draw the icon on a hidden canvas + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + var tabImg = document.getAnonymousElementByAttribute(aTab, "anonid", "tab-icon"); + var w = tabImg.boxObject.width; + var h = tabImg.boxObject.height; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx.drawImage(favImage, 0, 0, w, h); + icon = canvas.toDataURL(); + browser.mIconURL = icon; + aTab.setAttribute("image", icon); + } + catch (e) { + console.warn("Processing of favicon failed."); + // Canvas failed: icon remains as it was + } + tabBrowser._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + } + } + + 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[ + // Bug 691610 - e10s support for useDefaultIcon + if (gMultiProcessBrowser) + return; + + var browser = this.getBrowserForTab(aTab); + var docURIObject = browser.contentDocument.documentURIObject; + var icon = null; + <!-- Pale Moon: new image icon method, see bug #305986 --> + let req = browser.contentDocument.imageRequest; + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.contentDocument instanceof ImageDocument && + req && req.image) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons") && sz) { + try { + <!-- Main method: draw on a hidden canvas --> + var canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + var tabImg = document.getAnonymousElementByAttribute(aTab, "anonid", "tab-icon"); + var w = tabImg.boxObject.width; + var h = tabImg.boxObject.height; + canvas.width = w; + canvas.height = h; + var ctx = canvas.getContext('2d'); + ctx.drawImage(browser.contentDocument.body.firstChild, 0, 0, w, h); + icon = canvas.toDataURL(); + } + catch (e) { + <!-- Fallback method in case canvas method fails, restricted by sz --> + try { + + if (req && + req.image && + req.image.width <= sz && + req.image.height <= sz) + icon = browser.currentURI; + } + catch (e) { + <!-- Both methods fail (very large or corrupt image): icon remains null --> + } + } + } + } + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + else if (this.shouldLoadFavIcon(docURIObject)) { + let url = docURIObject.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon, browser.contentPrincipal); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (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="freezeTitlebar"> + <parameter name="aTitle"/> + <body> + <![CDATA[ + this._frozenTitle = aTitle || ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="unfreezeTitlebar"> + <body> + <![CDATA[ + this._frozenTitle = ""; + this.updateTitlebar(); + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + this.ownerDocument.title = this._frozenTitle || + 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) { + 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 (oldBrowser) { + oldBrowser.setAttribute("type", "content-targetable"); + oldBrowser.docShellIsActive = false; + this.finder.mListeners.forEach(l => oldBrowser.finder.removeResultListener(l)); + } + + var updateBlockedPopups = false; + if (!oldBrowser || + (oldBrowser.blockedPopups && !newBrowser.blockedPopups) || + (!oldBrowser.blockedPopups && newBrowser.blockedPopups)) + updateBlockedPopups = true; + + newBrowser.setAttribute("type", "content-primary"); + newBrowser.docShellIsActive = + (window.windowState != window.STATE_MINIMIZED); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.tabContainer.selectedItem; + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.addResultListener(l)); + this.showTab(this.mCurrentTab); + + var backForwardContainer = document.getElementById("unified-back-forward-button"); + if (backForwardContainer) { + backForwardContainer.setAttribute("switchingtabs", "true"); + window.addEventListener("MozAfterPaint", function removeSwitchingtabsAttr() { + window.removeEventListener("MozAfterPaint", removeSwitchingtabsAttr); + backForwardContainer.removeAttribute("switchingtabs"); + }); + } + + if (updateBlockedPopups) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + var loc = this.mCurrentBrowser.currentURI; + + // Bug 666809 - SecurityUI support for e10s + var webProgress = this.mCurrentBrowser.webProgress; + var securityUI = this.mCurrentBrowser.securityUI; + + // Update global findbar with new content browser + if (gFindBarInitialized) { + gFindBar.browser = newBrowser; + } + + this._callProgressListeners(null, "onLocationChange", + [webProgress, null, loc, 0], true, + false); + + if (securityUI) { + this._callProgressListeners(null, "onSecurityChange", + [webProgress, null, securityUI.state], 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.removeAttribute("unread"); + this.selectedTab.lastAccessed = Date.now(); + + // Bug 666816 - TypeAheadFind support for e10s + if (!gMultiProcessBrowser) + this._fastFind.setDocShell(this.mCurrentBrowser.docShell); + + this.updateTitlebar(); + + this.mCurrentTab.removeAttribute("titlechanged"); + } + + // 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"]); + + // Adjust focus + oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused); + do { + // When 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(); + break; + } + + // 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(); + break; + } + + // 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(); + break; + } else if (isTabEmpty(this.mCurrentTab)) { + focusAndSelectUrlBar(); + break; + } + } + + // If the find bar is focused, keep it focused. + if (gFindBarInitialized && + !gFindBar.hidden && + gFindBar.getElement("findbar-textbox").getAttribute("focused") == "true") + break; + + // Otherwise, focus the content area. + let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); + 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); + } while (false); + } + + this.tabContainer._setPositionalAttributes(); + ]]> + </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 aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aLoadInBackground = params.inBackground; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + var bgLoad = (aLoadInBackground != null) ? aLoadInBackground : + Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + var owner = bgLoad ? null : this.selectedTab; + var tab = this.addTab(aURI, { + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: aReferrerURI, + referrerPolicy: aReferrerPolicy, + charset: aCharset, + postData: aPostData, + ownerTab: owner, + allowThirdPartyFixup: aAllowThirdPartyFixup, + fromExternal: aFromExternal, + originPrincipal: aOriginPrincipal, + relatedToCurrent: aRelatedToCurrent, + opener: aOpener }); + if (!bgLoad) + this.selectedTab = tab; + + return tab; + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + let aAllowThirdPartyFixup; + let aTargetTab; + let aNewIndex = -1; + let aPostDatas = []; + if (arguments.length == 2 && + typeof arguments[1] == "object") { + let params = arguments[1]; + aLoadInBackground = params.inBackground; + aReplace = params.replace; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aTargetTab = params.targetTab; + aNewIndex = typeof params.newIndex === "number" ? + params.newIndex : aNewIndex; + aPostDatas = params.postDatas || aPostDatas; + } + + if (!aURIs.length) + return; + + // The tab selected after this new tab is closed (i.e. the new tab's + // "owner") is the next adjacent tab (i.e. not the previously viewed tab) + // when several urls are opened here (i.e. closing the first should select + // the next of many URLs opened) or if the pref to have UI links opened in + // the background is set (i.e. the link is not being opened modally) + // + // i.e. + // Number of URLs Load UI Links in BG Focus Last Viewed? + // == 1 false YES + // == 1 true NO + // > 1 false/true NO + var multiple = aURIs.length > 1; + var owner = multiple || aLoadInBackground ? null : this.selectedTab; + var firstTabAdded = null; + var targetTabIndex = -1; + + if (aReplace) { + let browser; + if (aTargetTab) { + browser = this.getBrowserForTab(aTargetTab); + targetTabIndex = aTargetTab._tPos; + } else { + browser = this.mCurrentBrowser; + targetTabIndex = this.tabContainer.selectedIndex; + } + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (aAllowThirdPartyFixup) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + } + try { + browser.loadURIWithFlags(aURIs[0], { + flags, postData: aPostDatas[0] + }); + } catch (e) { + // Ignore failure in case a URI is wrong, so we can continue + // opening the next ones. + } + } else { + firstTabAdded = this.addTab(aURIs[0], { + ownerTab: owner, + skipAnimation: multiple, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[0] + }); + if (aNewIndex !== -1) { + this.moveTabTo(firstTabAdded, aNewIndex); + targetTabIndex = firstTabAdded._tPos; + } + } + + let tabNum = targetTabIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], { + skipAnimation: true, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[i] + }); + if (targetTabIndex !== -1) + this.moveTabTo(tab, ++tabNum); + } + + if (!aLoadInBackground) { + if (firstTabAdded) { + // .selectedTab setter focuses the content area + this.selectedTab = firstTabAdded; + } + else + this.selectedBrowser.focus(); + } + ]]></body> + </method> + + <method name="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 aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aRelatedToCurrent; + var aSkipAnimation; + var aOriginPrincipal; + var aSkipBackgroundNotify; + var aOpener; + if (arguments.length == 2 && + typeof arguments[1] == "object" && + !(arguments[1] instanceof Ci.nsIURI)) { + let params = arguments[1]; + aTriggeringPrincipal = params.triggeringPrincipal; + aReferrerURI = params.referrerURI; + aReferrerPolicy = params.referrerPolicy; + aCharset = params.charset; + aPostData = params.postData; + aOwner = params.ownerTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aSkipAnimation = params.skipAnimation; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + aSkipBackgroundNotify = params.skipBackgroundNotify; + } + + // if we're adding tabs, we're past interrupt mode, ditch the owner + if (this.mCurrentTab.owner) + this.mCurrentTab.owner = null; + + var t = document.createElementNS(NS_XUL, "tab"); + + let aURIObject = null; + try { + aURIObject = Services.io.newURI(aURI || "about:blank"); + } catch (ex) { /* we'll try to fix up this URL later */ } + + var uriIsAboutBlank = !aURI || aURI == "about:blank"; + + if (!aURI || isBlankPageURL(aURI)) + t.setAttribute("label", this.mStringBundle.getString("tabs.emptyTabTitle")); + else + t.setAttribute("label", aURI); + + t.setAttribute("crop", "end"); + t.setAttribute("validate", "never"); //PMed + t.setAttribute("onerror", "this.removeAttribute('image');"); + + if (aSkipBackgroundNotify) { + t.setAttribute("skipbackgroundnotify", true); + } + + t.className = "tabbrowser-tab"; + + this.tabContainer._unlockTabSizing(); + + // When overflowing, new tabs are scrolled into view smoothly, which + // doesn't go well together with the width transition. So we skip the + // transition in that case. + let animate = !aSkipAnimation && + this.tabContainer.getAttribute("overflow") != "true" && + Services.prefs.getBoolPref("browser.tabs.animate"); + if (!animate) { + t.setAttribute("fadein", "true"); + setTimeout(function (tabContainer) { + tabContainer._handleNewTab(t); + }, 0, this.tabContainer); + } + + // invalidate 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; + + var b = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "browser"); + b.setAttribute("type", "content-targetable"); + b.setAttribute("message", "true"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + + if (Services.prefs.getPrefType("browser.tabs.remote") == Services.prefs.PREF_BOOL && + Services.prefs.getBoolPref("browser.tabs.remote")) { + b.setAttribute("remote", "true"); + } + + if (window.gShowPageResizers && document.getElementById("addon-bar").collapsed && + window.windowState == window.STATE_NORMAL) { + b.setAttribute("showresizer", "true"); + } + + if (aOpener) { + b.QueryInterface(Ci.nsIFrameLoaderOwner).presetOpenerWindow(aOpener); + } + + if (this.hasAttribute("autocompletepopup")) + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + b.setAttribute("autoscrollpopup", this._autoScrollPopup.id); + + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + + if (this.hasAttribute("authdosprotected")) { + b.setAttribute("authdosprotected", this.getAttribute("authdosprotected")); + } + + // 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.appendChild(browserSidebarContainer); + + var position = this.tabs.length - 1; + var uniqueId = "panel" + Date.now() + position; + notificationbox.id = uniqueId; + t.linkedPanel = uniqueId; + t.linkedBrowser = b; + this._tabForBrowser.set(b, t); + t._tPos = position; + this.tabContainer._setPositionalAttributes(); + + // 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"); + } + + // 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); + + this.tabContainer.updateVisibility(); + + // wire up a progress listener for the new browser object. + var tabListener = this.mTabProgressListener(t, b, uriIsAboutBlank); + 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._fastFind = this.fastFind; + b.droppedLinkHandler = handleDroppedLink; + + // If we just created a new tab that loads the default + // newtab url, swap in a preloaded page if possible. + // Do nothing if we're a private window. + let docShellsSwapped = false; + if (aURI == BROWSER_NEW_TAB_URL && + !PrivateBrowsingUtils.isWindowPrivate(window)) { + docShellsSwapped = gBrowserNewTabPreloader.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 (aOriginPrincipal && aURI) { + let {URI_INHERITS_SECURITY_CONTEXT} = Ci.nsIProtocolHandler; + // Unless we know for sure we're not inheriting principals, + // force the about:blank viewer to have the right principal: + if (!aURIObject || + (Services.io.getProtocolFlags(aURIObject.scheme) & URI_INHERITS_SECURITY_CONTEXT)) { + b.createAboutBlankContentViewer(aOriginPrincipal); + } + } + + // If we didn't swap docShells with a preloaded browser + // then let's just continue loading the page normally. + if (!docShellsSwapped && !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; + try { + b.loadURIWithFlags(aURI, { + flags: flags, + triggeringPrincipal: aTriggeringPrincipal, + referrerURI: 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 (Services.prefs.getPrefType("browser.tabs.remote") == Services.prefs.PREF_BOOL && + !Services.prefs.getBoolPref("browser.tabs.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"); + + // This call to adjustTabstrip is redundant but needed so that + // when opening a second tab, the first tab's close buttons + // appears immediately rather than when the transition ends. + if (this.tabs.length - this._removingTabs.length == 2) + this.tabContainer.adjustTabstrip(); + }.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: + // If there are multiple windows, pinned tabs will be closed, so + // we warn about them, too; if there is just one window, pinned + // tabs should come back on restart, so exclude them from warning. + var numberOfWindows = 0; + var browserEnum = Services.wm.getEnumerator("navigator:browser"); + while (browserEnum.hasMoreElements() && numberOfWindows < 2) { + numberOfWindows++; + browserEnum.getNext(); + } + if (numberOfWindows > 1) { + tabsToClose = this.tabs.length - this._removingTabs.length + } + else { + 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 buttonPressed = + ps.confirmEx(window, + bundle.getString("tabs.closeWarningTitle"), + bundle.getFormattedString("tabs.closeWarningMultipleTabs", + [tabsToClose]), + (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 prefs unless they press OK and have unchecked the box + if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value) { + Services.prefs.setBoolPref("browser.tabs.warnOnClose", false); + Services.prefs.setBoolPref("browser.tabs.warnOnCloseOtherTabs", 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"/> + <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], {animate: true}); + } + } + ]]> + </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]); + } + } + ]]> + </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; + } + + // 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)) + 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"); + + if (this.tabs.length - this._removingTabs.length == 1) { + // The second tab just got closed and we will end up with a single + // one. Remove the first tab's close button immediately (if needed) + // rather than after the tabclose animation ends. + this.tabContainer.adjustTabstrip(); + } + + 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"/> + <body> + <![CDATA[ + if (aTab.closing || + aTab._pendingPermitUnload || + this._windowIsClosing) + return false; + + var browser = this.getBrowserForTab(aTab); + + if (!aTabWillBeMoved) { + let ds = browser.docShell; + if (ds && ds.contentViewer) { + // We need to block while calling permitUnload() because it + // processes the event queue and may lead to another removeTab() + // call before permitUnload() even returned. + aTab._pendingPermitUnload = true; + let permitUnload = ds.contentViewer.permitUnload(); + delete aTab._pendingPermitUnload; + + if (!permitUnload) + return false; + } + } + + var closeWindow = false; + var newTab = false; + if (this.tabs.length - this._removingTabs.length == 1) { + closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab : + !window.toolbar.visible || + this.tabContainer._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._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow))) + return null; + + newTab = true; + } + + aTab.closing = true; + this._removingTabs.push(aTab); + this._visibleTabs = null; // invalidate cache + 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 (!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); + 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. + Array.forEach(this.tabs, function (tab) { + 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(); + + if (browser == this.mCurrentBrowser) + this.mCurrentBrowser = null; + + 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); + } + + // Pale Moon: if resizing immediately, select the tab immediately to the left + // instead of the right (if not leftmost) to prevent focus swap and + // "selected tab not under cursor" + // FIXME: Tabs must be sliding in from the left for this, or it'd unfocus + // in the other direction! Disabled for now. Is there an easier way? :hover? + // Is this even needed when resizing immediately?... + + //if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) { + // if (this.selectedTab._tPos > 1) { + // let newPos = this.selectedTab._tPos - 1; + // this.selectedTab = this.tabs[newPos]; + // } + //} + + // 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) { + let selectedPanel = this.mTabBox.selectedPanel; + + this.mPanelContainer.removeChild(panel); + + // Under the hood, a selectedIndex attribute controls which panel + // is displayed. Removing a panel A which precedes the selected + // panel B makes selectedIndex point to the panel next to B. We + // need to explicitly preserve B as the selected panel. + this.mTabBox.selectedPanel = selectedPanel; + } + + 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; + + // That's gBrowser for the other window, not the tab's browser! + var remoteBrowser = aOtherTab.ownerDocument.defaultView.gBrowser; + var isPending = aOtherTab.hasAttribute("pending"); + + // Expedite the removal of the icon if it was already scheduled. + if (aOtherTab._soundPlayingAttrRemovalTimer) { + clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer); + aOtherTab._soundPlayingAttrRemovalTimer = 0; + aOtherTab.removeAttribute("soundplaying"); + remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]); + } + + // First, start teardown of the other browser. Make sure to not + // fire the beforeunload event in the process. Close the other + // window if this was its last tab. + if (!remoteBrowser._beginRemoveTab(aOtherTab, true, true)) + return; + + let ourBrowser = this.getBrowserForTab(aOurTab); + let otherBrowser = aOtherTab.linkedBrowser; + + let modifiedAttrs = []; + if (aOtherTab.hasAttribute("muted")) { + aOurTab.setAttribute("muted", "true"); + aOurTab.muteReason = aOtherTab.muteReason; + ourBrowser.mute(); + modifiedAttrs.push("muted"); + } + if (aOtherTab.hasAttribute("soundplaying")) { + aOurTab.setAttribute("soundplaying", "true"); + modifiedAttrs.push("soundplaying"); + } + + // If 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) { + let ss = Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + ss.setTabState(aOurTab, ss.getTabState(aOtherTab)); + + // Make sure to unregister any open URIs. + this._swapRegisteredOpenURIs(ourBrowser, otherBrowser); + } else { + // Workarounds for bug 458697 + // Icon might have been set on DOMLinkAdded, don't override that. + if (!ourBrowser.mIconURL && otherBrowser.mIconURL) + this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal); + var isBusy = aOtherTab.hasAttribute("busy"); + if (isBusy) { + aOurTab.setAttribute("busy", "true"); + modifiedAttrs.push("busy"); + if (aOurTab.selected) + this.mIsBusy = true; + } + + this._swapBrowserDocShells(aOurTab, otherBrowser); + } + + // 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); + } + // Swap the docshells + ourBrowser.swapDocShells(aOtherBrowser); + + // Register new outerWindowIDs. + this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser); + if (remoteBrowser) { + remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser); + } + // Restore the progress listener + this.mTabListeners[index] = tabListener = + this.mTabProgressListener(aOurTab, ourBrowser, 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); + delete aOurBrowser.registeredOpenURI; + } + + // If the other/new URI is registered as open then copy it over. + if (aOtherBrowser.registeredOpenURI) { + aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI; + delete aOtherBrowser.registeredOpenURI; + } + ]]> + </body> + </method> + + <method name="reloadAllTabs"> + <body> + <![CDATA[ + let tabs = this.visibleTabs; + let l = tabs.length; + for (var i = 0; i < l; i++) { + try { + this.getBrowserForTab(tabs[i]).reload(); + } catch (e) { + // ignore failure to reload so others will be reloaded + } + } + ]]> + </body> + </method> + + <method name="reloadTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = this.getBrowserForTab(aTab); + // Reset DOS mitigation for basic auth prompt + delete browser.authPromptCounter; + browser.reload(); + ]]> + </body> + </method> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (arguments.length != 1) { + Components.utils.reportError("gBrowser.addProgressListener was " + + "called with a second argument, " + + "which is not supported. See bug " + + "608628."); + } + + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + this.mProgressListeners = + this.mProgressListeners.filter(function (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(function (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[ + Array.forEach(this.tabs, function(tab) { + if (aTabs.indexOf(tab) == -1) + this.hideTab(tab); + else + this.showTab(tab); + }, this); + + 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[ + if (gNavToolbox.collapsed) { + return this.mTabBox.selectedTab; + } + // Update the tab + this.mTabBox.selectedTab = val; + return val; + ]]> + </setter> + </property> + + <property name="selectedBrowser" + onget="return this.mCurrentBrowser;" + readonly="true"/> + + <property name="browsers" readonly="true"> + <getter> + <![CDATA[ + return this._browsers || + (this._browsers = Array.map(this.tabs, function (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> + + <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._selected = 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]._selected = false; + } + this.mCurrentTab._selected = true; + + if (wasFocused) + this.mCurrentTab.focus(); + + this.tabContainer._handleTabSelect(false); + + if (aTab.pinned) + this.tabContainer._positionPinnedTabs(); + + this.tabContainer._setPositionalAttributes(); + + var evt = document.createEvent("UIEvents"); + evt.initUIEvent("TabMove", true, false, window, oldPosition); + aTab.dispatchEvent(evt); + ]]> + </body> + </method> + + <method name="moveTabForward"> + <body> + <![CDATA[ + let nextTab = this.mCurrentTab.nextSibling; + while (nextTab && nextTab.hidden) + nextTab = nextTab.nextSibling; + + if (nextTab) + this.moveTabTo(this.mCurrentTab, nextTab._tPos); + else if (this.arrowKeysShouldWrap) + this.moveTabToStart(); + ]]> + </body> + </method> + + <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 Cc["@mozilla.org/browser/sessionstore;1"] + .getService(Ci.nsISessionStore) + .duplicateTab(window, aTab); + ]]> + </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 (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.attachFormFill(); + } + ]]></body> + </method> + + <method name="detachFormFill"> + <body><![CDATA[ + for (var i = 0; i < this.mPanelContainer.childNodes.length; ++i) { + var cb = this.getBrowserAtIndex(i); + cb.detachFormFill(); + } + ]]></body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <field name="_fastFind">null</field> + <property name="fastFind" + readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"] + .createInstance(Components.interfaces.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]> + </getter> + </property> + + <field name="_lastSearchString">null</field> + <field name="_lastSearchHighlight">false</field> + + <field name="finder"><![CDATA[ + ({ + mTabBrowser: this, + mListeners: new Set(), + get finder() { + return this.mTabBrowser.mCurrentBrowser.finder; + }, + destroy: function() { + this.finder.destroy(); + }, + addResultListener: function(aListener) { + this.mListeners.add(aListener); + this.finder.addResultListener(aListener); + }, + removeResultListener: function(aListener) { + this.mListeners.delete(aListener); + this.finder.removeResultListener(aListener); + }, + get searchString() { + return this.finder.searchString; + }, + get clipboardSearchString() { + return this.finder.clipboardSearchString; + }, + set clipboardSearchString(val) { + return this.finder.clipboardSearchString = val; + }, + set caseSensitive(val) { + return this.finder.caseSensitive = val; + }, + set entireWord(val) { + return this.finder.entireWord = val; + }, + get highlighter() { + return this.finder.highlighter; + }, + get matchesCountLimit() { + return this.finder.matchesCountLimit; + }, + fastFind: function(...args) { + this.finder.fastFind(...args); + }, + findAgain: function(...args) { + this.finder.findAgain(...args); + }, + setSearchStringToSelection: function() { + return this.finder.setSearchStringToSelection(); + }, + highlight: function(...args) { + this.finder.highlight(...args); + }, + getInitialSelection: function() { + this.finder.getInitialSelection(); + }, + getActiveSelectionText: function() { + return this.finder.getActiveSelectionText(); + }, + enableSelection: function() { + this.finder.enableSelection(); + }, + removeSelection: function() { + this.finder.removeSelection(); + }, + focusContent: function() { + this.finder.focusContent(); + }, + onFindbarClose: function() { + this.finder.onFindbarClose(); + }, + onFindbarOpen: function() { + this.finder.onFindbarOpen(); + }, + onModalHighlightChange: function(...args) { + return this.finder.onModalHighlightChange(...args); + }, + onHighlightAllChange: function(...args) { + return this.finder.onHighlightAllChange(...args); + }, + keyPress: function(aEvent) { + this.finder.keyPress(aEvent); + }, + requestMatchesCount: function(...args) { + this.finder.requestMatchesCount(...args); + }, + onIteratorRangeFound: function(...args) { + this.finder.onIteratorRangeFound(...args); + }, + onIteratorReset: function() { + this.finder.onIteratorReset(); + }, + onIteratorRestart: function(...args) { + this.finder.onIteratorRestart(...args); + }, + onIteratorStart: function(...args) { + this.finder.onIteratorStart(...args); + }, + onLocationChange: function(...args) { + this.finder.onLocationChange(...args); + } + }) + ]]></field> + + <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.contentWindow;"/> + + <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.contentDocument;" + 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="_handleKeyEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + if (!aEvent.isTrusted) { + // Don't let untrusted events mess with tabs. + return; + } + + if (aEvent.altKey) + return; + + if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) { + switch (aEvent.keyCode) { + case aEvent.DOM_VK_PAGE_UP: + this.moveTabBackward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + case aEvent.DOM_VK_PAGE_DOWN: + this.moveTabForward(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + } + +#ifdef XP_MACOSX + if (!aEvent.metaKey) + return; + + var offset = 1; + switch (aEvent.charCode) { + case '}'.charCodeAt(0): + offset = -1; + case '{'.charCodeAt(0): + if (window.getComputedStyle(this, null).direction == "ltr") + offset *= -1; + this.tabContainer.advanceSelectedTab(offset, true); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } +#else + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + !this.mCurrentTab.pinned) { + this.removeCurrentTab({animate: true}); + aEvent.stopPropagation(); + aEvent.preventDefault(); + } +#endif + ]]></body> + </method> + + <property name="userTypedValue" + onget="return this.mCurrentBrowser.userTypedValue;" + onset="return this.mCurrentBrowser.userTypedValue = val;"/> + + <method name="createTooltip"> + <parameter name="event"/> + <body><![CDATA[ + event.stopPropagation(); + var tab = document.tooltipNode; + if (tab.localName != "tab") { + event.preventDefault(); + return; + } + + var stringID, label; + if (tab.mOverCloseButton) { + stringID = "tabs.closeTab"; + } else if (tab._overPlayingIcon) { + if (tab.linkedBrowser.audioBlocked) { + stringID = "tabs.unblockAudio.tooltip"; + } else { + stringID = tab.linkedBrowser.audioMuted ? + "tabs.unmuteAudio.tooltip" : + "tabs.muteAudio.tooltip"; + } + } else { + label = tab.getAttribute("label"); + } + if (stringID && !label) { + label = this.mStringBundle.getString(stringID); + } + event.target.setAttribute("label", label); + ]]></body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keypress": + this._handleKeyEvent(aEvent); + break; + case "sizemodechange": + if (aEvent.target == window) { + this.mCurrentBrowser.docShellIsActive = + (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) + return; + let titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + break; + } + case "DOMWebNotificationClicked": { + let tab = this.getTabForBrowser(browser); + if (!tab) + return; + this.selectedTab = tab; + window.focus(); + break; + } + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + let browserStack = document.getAnonymousElementByAttribute(this, "anonid", "browserStack"); + this.mCurrentBrowser = document.getAnonymousElementByAttribute(this, "anonid", "initialBrowser"); + if (Services.prefs.getBoolPref("browser.tabs.remote")) { + browserStack.removeChild(this.mCurrentBrowser); + this.mCurrentBrowser.setAttribute("remote", true); + browserStack.appendChild(this.mCurrentBrowser); + } + + this.mCurrentTab = this.tabContainer.firstChild; + document.addEventListener("keypress", this, false); + window.addEventListener("sizemodechange", this, false); + + var uniqueId = "panel" + Date.now(); + this.mPanelContainer.childNodes[0].id = uniqueId; + this.mCurrentTab.linkedPanel = uniqueId; + this.mCurrentTab._tPos = 0; + this.mCurrentTab._fullyOpen = true; + 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); + 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; + + try { + // We assume this can only fail because mCurrentBrowser's docShell + // hasn't been created, yet. This may be caused by code accessing + // gBrowser before the window has finished loading. + this._addProgressListenerForInitialTab(); + } catch (e) { + // The binding was constructed too early, wait until the initial + // tab's document is ready, then add the progress listener. + this._waitForInitialContentDocument(); + } + + this.style.backgroundColor = + Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "-moz-default-background-color" : + Services.prefs.getCharPref("browser.display.background_color"); + + if (Services.prefs.getBoolPref("browser.tabs.remote")) { + messageManager.addMessageListener("DOMTitleChanged", this); + } else { + this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID, + this.mCurrentBrowser); + } + messageManager.addMessageListener("DOMWebNotificationClicked", this); + ]]> + </constructor> + + <method name="_addProgressListenerForInitialTab"> + <body><![CDATA[ + this.webProgress.addProgressListener(this.mTabFilters[0], Ci.nsIWebProgress.NOTIFY_ALL); + ]]></body> + </method> + + <method name="_waitForInitialContentDocument"> + <body><![CDATA[ + let obs = (subject, topic) => { + if (this.browsers[0].contentWindow == subject) { + Services.obs.removeObserver(obs, topic); + this._addProgressListenerForInitialTab(); + } + }; + + // We use content-document-global-created as an approximation for + // "docShell is initialized". We can do this because in the + // mTabProgressListener we care most about the STATE_STOP notification + // that will reset mBlank. That means it's important to at least add + // the progress listener before the initial about:blank load stops + // if we can't do it before the load starts. + Services.obs.addObserver(obs, "content-document-global-created", false); + ]]></body> + </method> + + <destructor> + <![CDATA[ + for (var i = 0; i < this.mTabListeners.length; ++i) { + let browser = this.getBrowserAtIndex(i); + if (browser.registeredOpenURI) { + this._placesAutocomplete.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; + } + document.removeEventListener("keypress", this, false); + window.removeEventListener("sizemodechange", this, false); + + if (Services.prefs.getBoolPref("browser.tabs.remote")) + messageManager.removeMessageListener("DOMTitleChanged", this); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <method name="enterTabbedMode"> + <body> + Application.console.log("enterTabbedMode is an obsolete method and " + + "will be removed in a future release."); + </body> + </method> + <field name="mTabbedMode" readonly="true">true</field> + <method name="setStripVisibilityTo"> + <parameter name="aShow"/> + <body> + this.tabContainer.visible = aShow; + </body> + </method> + <method name="getStripVisibility"> + <body> + return this.tabContainer.visible; + </body> + </method> + <property name="mContextTab" readonly="true" + onget="return TabContextMenu.contextTab;"/> + <property name="mPrefs" readonly="true" + onget="return Services.prefs;"/> + <property name="mTabContainer" readonly="true" + onget="return this.tabContainer;"/> + <property name="mTabs" readonly="true" + onget="return this.tabs;"/> + <!-- + - Compatibility hack: several extensions depend on this property to + - access the tab context menu or tab container, so keep that working for + - now. Ideally we can remove this once extensions are using + - tabbrowser.tabContextMenu and tabbrowser.tabContainer directly. + --> + <property name="mStrip" readonly="true"> + <getter> + <![CDATA[ + return ({ + self: this, + childNodes: [null, this.tabContextMenu, this.tabContainer], + firstChild: { nextSibling: this.tabContextMenu }, + getElementsByAttribute: function (attr, attrValue) { + if (attr == "anonid" && attrValue == "tabContextMenu") + return [this.self.tabContextMenu]; + return []; + }, + // Also support adding event listeners (forward to the tab container) + addEventListener: function (a,b,c) { this.self.tabContainer.addEventListener(a,b,c); }, + removeEventListener: function (a,b,c) { this.self.tabContainer.removeEventListener(a,b,c); } + }); + ]]> + </getter> + </property> + <field name="_soundPlayingAttrRemovalTimer">0</field> + </implementation> + + <handlers> + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) + return; + + var tab = this._getTabForContentWindow(event.target); + if (tab) { + this.removeTab(tab); + event.preventDefault(); + } + ]]> + </handler> + <handler event="DOMWillOpenModalDialog" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // We're about to open a modal dialog, make sure the opening + // tab is brought to the front. + this.selectedTab = this._getTabForContentWindow(event.target.top); + ]]> + </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) { + return; + } + + var titleChanged = this.setTabTitle(tab); + if (titleChanged && !tab.selected && !tab.hasAttribute("busy")) + tab.setAttribute("titlechanged", "true"); + ]]> + </handler> + <handler event="DOMAudioPlaybackStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + clearTimeout(tab._soundPlayingAttrRemovalTimer); + tab._soundPlayingAttrRemovalTimer = 0; + + let modifiedAttrs = []; + if (tab.hasAttribute("soundplaying-scheduledremoval")) { + tab.removeAttribute("soundplaying-scheduledremoval"); + modifiedAttrs.push("soundplaying-scheduledremoval"); + } + + if (!tab.hasAttribute("soundplaying")) { + tab.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + this._tabAttrModified(tab, modifiedAttrs); + ]]> + </handler> + <handler event="DOMAudioPlaybackStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("soundplaying")) { + let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS"); + + tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`); + tab.setAttribute("soundplaying-scheduledremoval", "true"); + this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]); + + tab._soundPlayingAttrRemovalTimer = setTimeout(() => { + tab.removeAttribute("soundplaying-scheduledremoval"); + tab.removeAttribute("soundplaying"); + this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]); + }, removalDelay); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStarted"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (!tab.hasAttribute("blocked")) { + tab.setAttribute("blocked", true); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + <handler event="DOMAudioPlaybackBlockStopped"> + <![CDATA[ + var tab = getTabFromAudioEvent(event) + if (!tab) { + return; + } + + if (tab.hasAttribute("blocked")) { + tab.removeAttribute("blocked"); + this._tabAttrModified(tab, ["blocked"]); + } + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabbox" + extends="chrome://global/content/bindings/tabbox.xml#tabbox"> + <implementation> + <property name="tabs" readonly="true" + onget="return document.getBindingParent(this).tabContainer;"/> + </implementation> + </binding> + + <binding id="tabbrowser-arrowscrollbox" extends="chrome://global/content/bindings/scrollbox.xml#arrowscrollbox-clicktoscroll"> + <implementation> + <!-- Override scrollbox.xml method, since our scrollbox's children are + inherited from the binding parent --> + <method name="_getScrollableElements"> + <body><![CDATA[ + return Array.filter(document.getBindingParent(this).childNodes, + this._canScrollToElement, this); + ]]></body> + </method> + <method name="_canScrollToElement"> + <parameter name="tab"/> + <body><![CDATA[ + return !tab.pinned && !tab.hidden; + ]]></body> + </method> + </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); + + tabs.tabbrowser._removingTabs.forEach(tabs.tabbrowser.removeTab, + tabs.tabbrowser); + + 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;" +#ifndef XP_MACOSX + clicktoscroll="true" +#endif + 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" + command="cmd_newNavigatorTab" + onclick="checkForMiddleClick(this, event);" + onmouseover="document.getBindingParent(this)._enterNewTab();" + onmouseout="document.getBindingParent(this)._leaveNewTab();" + tooltiptext="&newTabButton.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"); + this.mCloseButtons = Services.prefs.getIntPref("browser.tabs.closeButtons"); + this._closeWindowWithLastTab = Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab"); + + var tab = this.firstChild; + tab.setAttribute("label", + this.tabbrowser.mStringBundle.getString("tabs.emptyTabTitle")); + tab.setAttribute("crop", "end"); + tab.setAttribute("onerror", "this.removeAttribute('image');"); + this.adjustTabstrip(); + + Services.prefs.addObserver("browser.tabs.", this._prefObserver, false); + window.addEventListener("resize", this, false); + window.addEventListener("load", this, false); + + this._tabAnimationLoggingEnabled = Services.prefs.getBoolPref("browser.tabs.animationLogging.enabled", false); + this._browserNewtabpageEnabled = Services.prefs.getBoolPref("browser.newtabpage.enabled"); + ]]> + </constructor> + + <destructor> + <![CDATA[ + Services.prefs.removeObserver("browser.tabs.", this._prefObserver); + ]]> + </destructor> + + <field name="tabbrowser" readonly="true"> + document.getElementById(this.getAttribute("tabbrowser")); + </field> + + <field name="tabbox" readonly="true"> + this.tabbrowser.mTabBox; + </field> + + <field name="contextMenu" readonly="true"> + document.getElementById("tabContextMenu"); + </field> + + <field name="mTabstripWidth">0</field> + + <field name="mTabstrip"> + document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); + </field> + + <field name="_firstTab">null</field> + <field name="_lastTab">null</field> + <field name="_afterSelectedTab">null</field> + <field name="_beforeHoveredTab">null</field> + <field name="_afterHoveredTab">null</field> + + <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"); + ]]></body> + </method> + + <field name="_prefObserver"><![CDATA[({ + tabContainer: this, + + observe: function (subject, topic, data) { + switch (data) { + case "browser.tabs.closeButtons": + this.tabContainer.mCloseButtons = Services.prefs.getIntPref(data); + this.tabContainer.adjustTabstrip(); + break; + case "browser.tabs.autoHide": + this.tabContainer.updateVisibility(); + break; + case "browser.tabs.closeWindowWithLastTab": + this.tabContainer._closeWindowWithLastTab = Services.prefs.getBoolPref(data); + this.tabContainer.adjustTabstrip(); + break; + } + } + });]]></field> + <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")); + + goSetCommandEnabled("cmd_ToggleTabsOnTop", visible); + + TabsOnTop.syncUI(); + + 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 && + !Services.prefs.getBoolPref("browser.tabs.autoHide"); + else + this.visible = true; + ]]></body> + </method> + + <method name="adjustTabstrip"> + <body><![CDATA[ + let numTabs = this.childNodes.length - + this.tabbrowser._removingTabs.length; + // modes for tabstrip + // 0 - button on active tab only + // 1 - close buttons on all tabs, if available space allows + // 2 - no close buttons at all + // 3 - close button at the end of the tabstrip + switch (this.mCloseButtons) { + case 0: + // If we decide we want to hide the close tab button on the last tab + // when closing the window with the last tab, then we should check + // if (numTabs == 1 && this._closeWindowWithLastTab) here and set + // this.setAttribute("closebuttons", "hidden") appropriately + this.setAttribute("closebuttons", "activetab"); + break; + case 1: + if (numTabs == 1) { + // See remark about potentially hiding the close tab button, above. + this.setAttribute("closebuttons", "alltabs"); + } else 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. + this.setAttribute("closebuttons", "alltabs"); + } else { + let tab = this.tabbrowser.visibleTabs[this.tabbrowser._numPinnedTabs]; + if (tab && tab.getBoundingClientRect().width > this.mTabClipWidth) + this.setAttribute("closebuttons", "alltabs"); + else + this.setAttribute("closebuttons", "activetab"); + } + break; + case 2: + case 3: + this.setAttribute("closebuttons", "never"); + break; + } + var tabstripClosebutton = document.getElementById("tabs-closebutton"); + if (tabstripClosebutton && tabstripClosebutton.parentNode == this._container) + tabstripClosebutton.collapsed = this.mCloseButtons != 3; + ]]></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") { +#ifdef XP_WIN + // Don't need to do anything if we're in overflow mode and we're closing + // the last tab. + if (isEndTab) +#else + // 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) +#endif + 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; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + 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; + + // Resize immediately if preffed + // XXX: we may want to make this a three-state pref to disable this early + // exit if people prefer a mix of behavior (don't resize in overflow, + // but resize if not overflowing) + if (Services.prefs.getBoolPref("browser.tabs.resize_immediately")) + 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; + + let sizemode = document.documentElement.getAttribute("sizemode"); + TabsInTitlebar.allowedBy("sizemode", + sizemode == "maximized" || sizemode == "fullscreen"); + + 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(); + + // 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(); + + // 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"/> + <body><![CDATA[ + let tab = event.target.localName == "tab" ? event.target : null; + if (tab && + (event.type == "drop" || event.type == "dragover") && + event.dataTransfer.dropEffect == "link") { + 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"/> + <body><![CDATA[ + var tabs = this.childNodes; + var tab = this._getDragTargetTab(event); + 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="_setEffectAllowedForDataTransfer"> + <parameter name="event"/> + <body><![CDATA[ + var dt = event.dataTransfer; + // Disallow dropping multiple items + if (dt.mozItemCount > 1) + return dt.effectAllowed = "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 dt.effectAllowed = "none"; + +#ifdef XP_MACOSX + return dt.effectAllowed = event.altKey ? "copy" : "move"; +#else + return dt.effectAllowed = event.ctrlKey ? "copy" : "move"; +#endif + } + } + + if (browserDragAndDrop.canDropLink(event)) { + // Here we need to do this manually + return dt.effectAllowed = dt.dropEffect = "link"; + } + return dt.effectAllowed = "none"; + ]]></body> + </method> + + <method name="_handleNewTab"> + <parameter name="tab"/> + <body><![CDATA[ + if (tab.parentNode != this) + return; + tab._fullyOpen = true; + + this.adjustTabstrip(); + + if (tab.getAttribute("selected") == "true") { + this._fillTrailingGap(); + this._handleTabSelect(); + } else { + if (tab.hasAttribute("skipbackgroundnotify")) { + tab.removeAttribute("skipbackgroundnotify"); + } else { + this._notifyBackgroundTab(tab); + } + } + + // XXXmano: this is a temporary workaround for bug 345399 + // We need to manually update the scroll buttons disabled state + // if a tab was inserted to the overflow area or removed from it + // without any scrolling and when the tabbar has already + // overflowed. + this.mTabstrip._updateScrollButtonsDisabledState(); + ]]></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 (this._tabAnimationLoggingEnabled) { + if (aURI == "about:newtab" && (aTab._tPos == 1 || aTab._tPos == 2)) { + // Indicate newtab page animation where other tabs are unaffected + // (for which case, the 2nd or 3rd tabs are good representatives, even if not absolute) + aTab._recordingTabOpenPlain = true; + } + aTab._recordingHandle = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .startFrameTimeRecording(); + } + + // Overall animation duration + aTab._animStartTime = Date.now(); + ]]> + </body> + </method> + + <method name="_handleTabTelemetryEnd"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (!aTab._animStartTime) { + return; + } + + aTab._animStartTime = 0; + + // Handle tab animation smoothness telemetry/logging of frame intervals and paint times + if (!("_recordingHandle" in aTab)) { + return; + } + + let paints = {}; + let intervals = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .stopFrameTimeRecording(aTab._recordingHandle, paints); + delete aTab._recordingHandle; + paints = paints.value; // The result array itself. + let frameCount = intervals.length; + + if (this._tabAnimationLoggingEnabled) { + let msg = "Tab " + (aTab.closing ? "close" : "open") + " (Frame-interval / paint-processing):\n"; + for (let i = 0; i < frameCount; i++) { + msg += Math.round(intervals[i]) + " / " + Math.round(paints[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. + // But if we recorded only 1 frame (very rare), then the first paint duration is a good + // representative of the first frame interval for our cause (indicates very bad animation). + // First paint duration is always useful for us. + if (frameCount > 0) { + let averageInterval = 0; + let averagePaint = paints[0]; + for (let i = 1; i < frameCount; i++) { + averageInterval += intervals[i]; + averagePaint += paints[i]; + }; + averagePaint /= frameCount; + averageInterval = (frameCount == 1) + ? averagePaint + : averageInterval / (frameCount - 1); + + if (aTab._recordingTabOpenPlain) { + delete aTab._recordingTabOpenPlain; + } + } + ]]> + </body> + </method> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mTabstripClosebutton" readonly="true" + onget="return document.getElementById('tabs-closebutton');"/> + <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[ +#ifndef XP_MACOSX + // 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 || + (TabsOnTop.enabled && this.parentNode._dragBindingAlive)) + return; +#endif + + if (event.button != 0 || + event.originalTarget.localName != "box") + return; + + // See comments in the "mousedown" and "click" event handlers of the + // tabbrowser-tabs binding. + if (!this._blockDblClick) + BrowserOpenTab(); + + event.preventDefault(); + ]]></handler> + + <!-- Consider that the in-tab close button is only shown on the active + tab. When clicking on an inactive tab at the position where the + close button will appear during the click, no "click" event will be + dispatched, because the mousedown and mouseup events don't have the + same event target. For that reason use "mousedown" instead of "click" + to implement in-tab close button behavior. (Pale Moon UXP issue #775) + --> + <handler event="mousedown" button="0" phase="capturing"><![CDATA[ + /* 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. + * + * Also prevent errant doubleclick on the close button from opening + * a new tab (bug 343628): + * 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. + */ + + // Reset flags at the beginning of a series of clicks: + if (event.detail == 1) { + this.flagClickOnCloseButton = false; + this.flagActivateTabOrClickOnTabbar = false; + } + + this.blockCloseButtonOnDblclick = this.flagActivateTabOrClickOnTabbar; + this._blockDblClick = this.flagClickOnCloseButton; + + // Set flags: + let eventTargetIsCloseButton = + event.originalTarget.classList.contains("tab-close-button"); + this.flagClickOnCloseButton = eventTargetIsCloseButton; + this.flagActivateTabOrClickOnTabbar = + ((!eventTargetIsCloseButton && event.detail == 1) || + event.originalTarget.localName == "box"); + ]]></handler> + + <handler event="click" button="0"><![CDATA[ + // See comment in the "mousedown" event handler of the + // tabbrowser-tabs binding. + if (event.originalTarget.classList.contains("tab-close-button") && + !this.blockCloseButtonOnDblclick) { + gBrowser.removeTab(document.getBindingParent(event.originalTarget), + {animate: true, byMouse: true,}); + this._blockDblClick = true; + } + ]]></handler> + + <handler event="click" button="1"><![CDATA[ + if (event.target.localName == "tab") { + if (this.childNodes.length > 1 || !this._closeWindowWithLastTab) + this.tabbrowser.removeTab(event.target, {animate: true, byMouse: true}); + } else if (event.originalTarget.localName == "box") { + BrowserOpenTab(); + } else { + return; + } + + event.stopPropagation(); + ]]></handler> + + <handler event="keypress"><![CDATA[ + if (event.altKey || event.shiftKey || +#ifdef XP_MACOSX + !event.metaKey) +#else + !event.ctrlKey || event.metaKey) +#endif + return; + + 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: + // Stop the keypress event for the above keyboard + // shortcuts only. + return; + } + event.stopPropagation(); + event.preventDefault(); + ]]></handler> + + <handler event="dragstart"><![CDATA[ + var tab = this._getDragTargetTab(event); + if (!tab) + 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 = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); + canvas.mozOpaque = true; + canvas.width = 160 * scale; + canvas.height = 90 * scale; + PageThumbs.captureToCanvas(browser, canvas); + dt.setDragImage(canvas, -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) 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._setEffectAllowedForDataTransfer(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); + 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); + 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 (dropEffect != "link") { // 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); + 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); + let newTab = this.tabbrowser.addTab("about:blank"); + let newBrowser = this.tabbrowser.getBrowserForTab(newTab); + // 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 links; + try { + links = browserDragAndDrop.dropLinks(event, true); + } catch (ex) {} + +// // valid urls don't contain spaces ' '; if we have a space it isn't a valid url. +// if (!url || url.includes(" ")) //PMed + if (!links || links.length === 0) //FF + return; + + let inBackground = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (event.shiftKey) + inBackground = !inBackground; + + let targetTab = this._getDragTargetTab(event); + let replace = !(!targetTab || dropEffect == "copy"); + let newIndex = this._getDropIndex(event); + let urls = links.map(link => link.url); + this.tabbrowser.loadTabs(urls, { + inBackground, + replace, + allowThirdPartyFixup: true, + targetTab, + newIndex, + }); + } + + if (draggedTab) { + delete draggedTab._dragData; + } + ]]></handler> + + <handler event="dragend"><![CDATA[ + // Note: while this case is correctly handled here, this event + // isn't dispatched when the tab is moved within the tabstrip, + // see bug 460801. + + this._finishAnimateTabMove(); + + var dt = event.dataTransfer; + var draggedTab = dt.mozGetDataAt(TAB_DROP_TYPE, 0); + if (dt.mozUserCancelled || dt.dropEffect != "none") { + 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 { + this.tabbrowser.replaceTabWithWindow(draggedTab, { screenX: left, + screenY: top, +#ifndef XP_WIN + outerWidth: winWidth, + outerHeight: winHeight +#endif + }); + } + 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="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" closetabtext="&closeTab.label;"> + <xul:stack class="tab-stack" flex="1"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background"> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-start"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-middle"/> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-background-end"/> + </xul:hbox> + <xul:hbox xbl:inherits="pinned,selected,titlechanged" + class="tab-content" align="center"> + <xul:image xbl:inherits="fadein,pinned,busy,progress,selected" + class="tab-throbber" + role="presentation" + layer="true" /> + <xul:image xbl:inherits="validate,src=image,fadein,pinned,selected" + class="tab-icon-image" + role="presentation" + anonid="tab-icon"/> + <xul:image xbl:inherits="busy,soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected" + anonid="overlay-icon" + class="tab-icon-overlay" + role="presentation"/> + <xul:label flex="1" + xbl:inherits="value=label,crop,accesskey,fadein,pinned,selected" + class="tab-text tab-label" + role="presentation"/> + <xul:image xbl:inherits="soundplaying,soundplaying-scheduledremoval,pinned,muted,blocked,selected" + anonid="soundplaying-icon" + class="tab-icon-sound" + role="presentation"/> + <xul:toolbarbutton anonid="close-button" + xbl:inherits="fadein,pinned,selected" + class="tab-close-button close-icon"/> + </xul:hbox> + </xul:stack> + </content> + + <implementation> + <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> + + <field name="mOverCloseButton">false</field> + <property name="_overPlayingIcon" readonly="true"> + <getter><![CDATA[ + let iconVisible = this.hasAttribute("soundplaying") || + this.hasAttribute("muted") || + this.hasAttribute("blocked"); + let soundPlayingIcon = + document.getAnonymousElementByAttribute(this, "anonid", "soundplaying-icon"); + let overlayIcon = + document.getAnonymousElementByAttribute(this, "anonid", "overlay-icon"); + + return soundPlayingIcon && soundPlayingIcon.matches(":hover") || + (overlayIcon && overlayIcon.matches(":hover") && iconVisible); + ]]></getter> + </property> + <field name="mCorrespondingMenuitem">null</field> + <field name="closing">false</field> + <field name="lastAccessed">0</field> + + <method name="toggleMuteAudio"> + <parameter name="aMuteReason"/> + <body> + <![CDATA[ + let tabContainer = this.parentNode; + let browser = this.linkedBrowser; + let modifiedAttrs = []; + if (browser.audioBlocked) { + this.removeAttribute("blocked"); + modifiedAttrs.push("blocked"); + + // We don't want sound icon flickering between "blocked", "none" and + // "sound-playing", here adding the "soundplaying" is to keep the + // transition smoothly. + if (!this.hasAttribute("soundplaying")) { + this.setAttribute("soundplaying", true); + modifiedAttrs.push("soundplaying"); + } + + browser.resumeMedia(); + } else { + if (browser.audioMuted) { + browser.unmute(); + this.removeAttribute("muted"); + } else { + browser.mute(); + this.setAttribute("muted", "true"); + } + this.muteReason = aMuteReason || null; + modifiedAttrs.push("muted"); + } + tabContainer.tabbrowser._tabAttrModified(this, modifiedAttrs); + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="mouseover"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = true; + + let tab = event.target; + if (tab.closing) + return; + + let tabContainer = this.parentNode; + let visibleTabs = tabContainer.tabbrowser.visibleTabs; + let tabIndex = visibleTabs.indexOf(tab); + 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"); + } + } + ]]></handler> + <handler event="mouseout"><![CDATA[ + let anonid = event.originalTarget.getAttribute("anonid"); + if (anonid == "close-button") + this.mOverCloseButton = false; + + let tabContainer = this.parentNode; + if (tabContainer._beforeHoveredTab) { + tabContainer._beforeHoveredTab.removeAttribute("beforehovered"); + tabContainer._beforeHoveredTab = null; + } + if (tabContainer._afterHoveredTab) { + tabContainer._afterHoveredTab.removeAttribute("afterhovered"); + tabContainer._afterHoveredTab = null; + } + ]]></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; + 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); + + if (!aTab.mCorrespondingMenuitem) { + 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[ + 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 alignRight = false; + + if (getComputedStyle(document.documentElement).direction == "rtl") + alignRight = !alignRight; + + let rect = this.getBoundingClientRect(); + this._mouseTargetRect = { + top: rect.top, + bottom: rect.bottom, + left: alignRight ? window.innerWidth - rect.width : 0, + right: alignRight ? window.innerWidth : rect.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> + +</bindings> |