diff options
Diffstat (limited to '')
-rw-r--r-- | comm/suite/browser/tabbrowser.xml | 3707 |
1 files changed, 3707 insertions, 0 deletions
diff --git a/comm/suite/browser/tabbrowser.xml b/comm/suite/browser/tabbrowser.xml new file mode 100644 index 0000000000..390fbaa0cf --- /dev/null +++ b/comm/suite/browser/tabbrowser.xml @@ -0,0 +1,3707 @@ +<?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://navigator/locale/tabbrowser.dtd" > +%tabBrowserDTD; +]> + +<bindings id="tabBrowserBindings" + xmlns="http://www.mozilla.org/xbl" + xmlns:html="http://www.w3.org/1999/xhtml" + 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://navigator/skin/tabbrowser.css"/> + </resources> + + <content> + <xul:stringbundle anonid="tbstringbundle" src="chrome://navigator/locale/tabbrowser.properties"/> + <xul:tabbox anonid="tabbox" flex="1" eventnode="document"> + <xul:hbox class="tab-drop-indicator-bar" collapsed="true"> + <xul:image class="tab-drop-indicator" mousethrough="always"/> + </xul:hbox> + <xul:hbox class="tabbrowser-strip" collapsed="true" tooltip="_child" context="_child" + anonid="strip" + ondragstart="this.parentNode.parentNode._onDragStart(event);" + ondragover="this.parentNode.parentNode._onDragOver(event);" + ondrop="this.parentNode.parentNode._onDrop(event);" + ondragleave="this.parentNode.parentNode._onDragLeave(event);"> + <xul:tooltip onpopupshowing="event.stopPropagation(); return this.parentNode.parentNode.parentNode.doPreview(this);" + onpopuphiding="this.parentNode.parentNode.parentNode.resetPreview(this);" orient="vertical"> + <xul:label class="tooltip-label" crop="right"/> + <xul:label class="tooltip-label" hidden="true"><html:canvas class="tab-tooltip-canvas"/></xul:label> + </xul:tooltip> + <xul:menupopup anonid="tabContextMenu" onpopupshowing="return document.getBindingParent(this).updatePopupMenu(this);"> + <xul:menuitem label="&closeTab.label;" accesskey="&closeTab.accesskey;" + tbattr="tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeTab(tabbrowser.mContextTab);"/> + <xul:menuitem label="&closeOtherTabs.label;" accesskey="&closeOtherTabs.accesskey;" + tbattr="tabbrowser-multiple tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeAllTabsBut(tabbrowser.mContextTab);"/> + <xul:menuitem label="&closeTabsToTheEnd.label;" + accesskey="&closeTabsToTheEnd.accesskey;" + tbattr="tabbrowser-totheend tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.removeTabsToTheEndFrom(tabbrowser.mContextTab);"/> + <xul:menuseparator/> + <xul:menuitem label="&newTab.label;" accesskey="&newTab.accesskey;" + xbl:inherits="oncommand=onnewtab"/> + <xul:menuitem label="&undoCloseTab.label;" accesskey="&undoCloseTab.accesskey;" + tbattr="tabbrowser-undoclosetab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.undoCloseTab(0);"/> + <xul:menuseparator/> + <xul:menuitem label="&bookmarkGroup.label;" accesskey="&bookmarkGroup.accesskey;" + tbattr="tabbrowser-multiple" + xbl:inherits="oncommand=onbookmarkgroup"/> + <xul:menuseparator/> + <xul:menuitem label="&reloadTab.label;" accesskey="&reloadTab.accesskey;" + tbattr="tabbrowser-tab" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.reloadTab(tabbrowser.mContextTab);"/> + <xul:menuitem label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;" + tbattr="tabbrowser-multiple" + oncommand="var tabbrowser = this.parentNode.parentNode.parentNode.parentNode; + tabbrowser.reloadAllTabs();"/> + </xul:menupopup> + + <xul:tabs class="tabbrowser-tabs" flex="1" + anonid="tabcontainer" + tooltiptextnew="&newTabButton.tooltip;" + tooltiptextclose="&closeTabButton.tooltip;" + tooltiptextalltabs="&listAllTabs.tooltip;" + setfocus="false" + onclick="this.parentNode.parentNode.parentNode.onTabClick(event);" + xbl:inherits="onnewtab,onnewtabclick" + onclosetab="var node = this.parentNode; + while (node.localName != 'tabbrowser') + node = node.parentNode; + node.removeCurrentTab();"> + <xul:tab selected="true" validate="never" + onerror="this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); + this.removeAttribute('image');" + width="0" flex="100" + class="tabbrowser-tab" label="&untitledTab;" crop="end"/> + </xul:tabs> + </xul:hbox> + <xul:tabpanels flex="1" class="plain" selectedIndex="0" anonid="panelcontainer"> + <xul:notificationbox class="browser-notificationbox" xbl:inherits="popupnotification"> + <xul:stack flex="1" anonid="browserStack"> + <xul:browser flex="1" type="content" primary="true" + xbl:inherits="tooltip=contenttooltip,contextmenu=contentcontextmenu,autocompletepopup,datetimepicker"/> + </xul:stack> + </xul:notificationbox> + </xul:tabpanels> + </xul:tabbox> + <children/> + </content> + <implementation implements="nsIDOMEventListener, nsIObserver"> + <field name="closingTabsEnum" readonly="true"> + ({ ALL: 0, OTHER: 1, TO_END: 2 }); + </field> + <field name="mSessionStore" readonly="true"> + Cc["@mozilla.org/suite/sessionstore;1"].getService(Ci.nsISessionStore); + </field> + <field name="mTabBox" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabbox"); + </field> + <field name="mStrip" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "strip"); + </field> + <field name="tabContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "tabcontainer"); + </field> + <field name="mPanelContainer" readonly="true"> + document.getAnonymousElementByAttribute(this, "anonid", "panelcontainer"); + </field> + <field name="tabs" readonly="true"> + this.tabContainer.childNodes + </field> + <field name="mStringBundle"> + document.getAnonymousElementByAttribute(this, "anonid", "tbstringbundle"); + </field> + <field name="mCurrentTab"> + null + </field> + <field name="mPreviousTab"> + null + </field> + <field name="mCurrentBrowser"> + null + </field> + <field name="mProgressListeners"> + [] + </field> + <field name="mTabsProgressListeners"> + [] + </field> + <field name="mTabListeners"> + new Array() + </field> + <field name="mTabFilters"> + new Array() + </field> + <field name="mLastRelatedIndex"> + 0 + </field> + <field name="usePrivateBrowsing" readonly="true"> + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsILoadContext) + .usePrivateBrowsing; + </field> + <field name="mContextTab"> + null + </field> + + <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 (AppConstants.platform == "macosx") { + if (!aEvent.metaKey) + return; + + var offset = 1; + switch (aEvent.charCode) { + case '}'.charCodeAt(0): + offset = -1; + case '{'.charCodeAt(0): + if (window.getComputedStyle(this, null).direction == "ltr") + offset *= -1; + this.tabContainer.advanceSelectedTab(offset, true); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + } else { + if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && + aEvent.keyCode == KeyEvent.DOM_VK_F4 && + this.getStripVisibility()) { + this.removeCurrentTab(); + aEvent.stopPropagation(); + aEvent.preventDefault(); + return; + } + + if (aEvent.target == this) { + switch (aEvent.keyCode) { + case KeyEvent.DOM_VK_UP: + this.moveTabBackward(); + break; + case KeyEvent.DOM_VK_DOWN: + this.moveTabForward(); + break; + case KeyEvent.DOM_VK_RIGHT: + case KeyEvent.DOM_VK_LEFT: + this.moveTabOver(aEvent); + break; + case KeyEvent.DOM_VK_HOME: + this.moveTabToStart(); + break; + case KeyEvent.DOM_VK_END: + this.moveTabToEnd(); + break; + default: + // Stop the keypress event for the above keyboard + // shortcuts only. + return; + } + aEvent.stopPropagation(); + aEvent.preventDefault(); + } + } + ]]></body> + </method> + + <field name="arrowKeysShouldWrap"> + null + </field> + <field name="nextTabNumber"> + 0 + </field> + <field name="_browsers"> + null + </field> + <field name="savedBrowsers"> + new Array() + </field> + <field name="referenceTab"> + null + </field> + + <method name="doPreview"> + <parameter name="aPopup"/> + <body> + <![CDATA[ + var tab = document.tooltipNode; + if (tab.localName != "tab") + return false; + var b = tab.linkedBrowser; + if (!b) + return false; + + var label = aPopup.firstChild; + label.setAttribute("value", tab.getAttribute("label")); + + var canvas = aPopup.lastChild.firstChild; + canvas.parentNode.hidden = true; + + var win = b.contentWindow; + var w = win.innerWidth; + var h = win.innerHeight; + + if (tab == this.mCurrentTab || h == 0 || + !Services.prefs.getBoolPref("browser.tabs.tooltippreview.enable")) { + return true; + } + + var ctx; + try { + ctx = canvas.getContext("2d"); + } catch (e) { + return true; + } + + label.width = 0; + aPopup.setAttribute("tabpreview", "true"); + + var canvasW = Services.prefs.getIntPref("browser.tabs.tooltippreview.width"); + var canvasH = Math.round(canvasW * h / w); + canvas.width = canvasW; + canvas.height = canvasH; + canvas.parentNode.hidden = false; + + var bgColor = Services.prefs.getBoolPref("browser.display.use_system_colors") ? + "Window" : + Services.prefs.getCharPref("browser.display.background_color"); + if (b.contentDocument instanceof ImageDocument && + !(b.contentDocument.imageRequest.imageStatus & + Ci.imgIRequest.STATUS_ERROR)) { + ctx.fillStyle = bgColor; + ctx.fillRect(0, 0, canvasW, canvasH); + var img = b.contentDocument.body.firstChild; + var ratio = img.naturalHeight / img.naturalWidth; + if (img.naturalHeight <= canvasH && img.naturalWidth <= canvasW) { + ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight); + } + else if (ratio * canvasW > canvasH) { + ctx.drawImage(img, 0, 0, canvasH / ratio, canvasH); + } + else { + ctx.drawImage(img, 0, 0, canvasW, ratio * canvasW); + } + } + else { + ctx.save(); + ctx.scale(canvasW / w, canvasH / h); + ctx.drawWindow(win, win.pageXOffset, win.pageYOffset, w, h, bgColor); + ctx.restore(); + } + return true; + ]]> + </body> + </method> + + <!-- XXXcst This should not be needed, but it seems that the tooltip + sizing is happening too early when we want to stop showing the + preview. This clears the label's width early (i.e. when the + previous preview disappears) so that when the next tooltip appears, + it doesn't start with a bad size. For now, I blame Gecko. --> + <method name="resetPreview"> + <parameter name="aPopup"/> + <body> + <![CDATA[ + var label = aPopup.firstChild; + // If this function is removed, these two lines need to be restored + // to the non-preview codepath above. + label.removeAttribute("width"); + aPopup.removeAttribute("tabpreview"); + ]]> + </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 browsers = this.browsers; + for (var i = 0; i < browsers.length; i++) + if (browsers[i].contentDocument == aDocument) + return i; + return -1; + ]]> + </body> + </method> + + <method name="getBrowserForDocument"> + <parameter name="aDocument"/> + <body> + <![CDATA[ + var browsers = this.browsers; + for (var i = 0; i < browsers.length; i++) + if (browsers[i].contentDocument == aDocument) + return browsers[i]; + return null; + ]]> + </body> + </method> + + <method name="getBrowserForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + const browsers = this.browsers; + for (let browser of browsers) { + if (browser.contentWindow == aWindow) + return browser; + } + return null; + ]]> + </body> + </method> + + <method name="getNotificationBox"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return aBrowser ? aBrowser.parentNode.parentNode + : this.mCurrentBrowser.parentNode.parentNode; + ]]> + </body> + </method> + + <method name="_callProgressListeners"> + <parameter name="aBrowser"/> + <parameter name="aMethod"/> + <parameter name="aArguments"/> + <parameter name="aCallGlobalListeners"/> + <parameter name="aCallTabsListeners"/> + <body><![CDATA[ + if (!aBrowser) + aBrowser = this.mCurrentBrowser; + + if (aCallGlobalListeners != false && + aBrowser == this.mCurrentBrowser) { + this.mProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + p[aMethod].apply(p, aArguments); + } catch (e) { + // don't inhibit other listeners + Cu.reportError(e); + } + } + }); + } + + if (aCallTabsListeners != false) { + aArguments.unshift(aBrowser); + + this.mTabsProgressListeners.forEach(function (p) { + if (aMethod in p) { + try { + p[aMethod].apply(p, aArguments); + } catch (e) { + // don't inhibit other listeners + Cu.reportError(e); + } + } + }); + } + ]]></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, + mFeeds: [], + mRequest: null, + mStateFlags: 0, + mStatus: 0, + mMessage: "", + + // cache flags for correct status UI update after tab switching + mTotalProgress: 0, + + // count of open requests (should always be 0 or 1) + mRequestCount: 0, + + _callProgressListeners: function () { + Array.prototype.unshift.call(arguments, this.mBrowser); + return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments); + }, + + _shouldShowProgress: function (aRequest) { + if (this.mBlank) + return false; + + // Don't show progress indicators in tabs for about: URIs + // pointing to local resources. + if ((aRequest instanceof Ci.nsIChannel) && + aRequest.originalURI.schemeIs("about") && + (aRequest.URI.schemeIs("jar") || aRequest.URI.schemeIs("file"))) + return false; + + return true; + }, + + onProgressChange: function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) { + this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0; + + if (!this._shouldShowProgress(aRequest)) + return; + + if (this.mTotalProgress && this.mTab.hasAttribute("busy")) + 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; + + let location; + let originalLocation; + try { + aRequest.QueryInterface(Ci.nsIChannel) + location = aRequest.URI; + originalLocation = aRequest.originalURI; + } catch (ex) {} + + if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) { + this.mRequestCount++; + } + else if (aStateFlags & Ci.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 & Ci.nsIWebProgressListener.STATE_START && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + if (aWebProgress.isTopLevel) { + this.mFeeds = []; + // Need to use originalLocation rather than location because things + // like about:privatebrowsing arrive with nsIRequest pointing to + // their resolved jar: or file: URIs. + if (!(originalLocation && gInitialPages.has(originalLocation.spec) && + originalLocation != "about:blank" && + this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) { + // This will trigger clearing the location bar. Don't do it if + // we loaded off a blank browser and this is an initial page load + // (e.g. about:privatebrowsing, etc.) so we avoid clearing the + // location bar in case the user is typing in it. + // Loading about:blank shouldn't trigger this, either, because its + // loads are "special". + this.mBrowser.urlbarChangeTracker.startedLoad(); + } + // If the browser is loading it must not be crashed anymore. + this.mTab.removeAttribute("crashed"); + } + + if (this._shouldShowProgress(aRequest)) { + if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) && + aWebProgress && aWebProgress.isTopLevel) { + this.mTab.setAttribute("busy", "true"); + + // Do the following only for the top frame not any subframes. + // Remove favicon. This shows busy and progress indicators even during a reload. + this.mTab.removeAttribute("image"); + + if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD)) + this.mTabBrowser.setTabTitleLoading(this.mTab); + } + + if (this.mTab.selected) + this.mTabBrowser.mIsBusy = true; + } + } + else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP && + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { + + if (this.mTab.hasAttribute("busy")) { + this.mTab.removeAttribute("busy"); + this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]); + if (!this.mTab.selected) + this.mTab.setAttribute("unread", "true"); + } + this.mTab.removeAttribute("progress"); + + if (aWebProgress.isTopLevel) { + let isSuccessful = Components.isSuccessCode(aStatus); + if (!isSuccessful && !isTabEmpty(this.mTab)) { + // Restore the current document's location in case the + // request was stopped (possibly from a content script) + // before the location changed. + this.mBrowser.userTypedValue = null; + + let inLoadURI = this.mBrowser.inLoadURI; + if (this.mTab.selected && gURLBar && !inLoadURI) + URLBarSetURI(); + } else if (isSuccessful) { + this.mBrowser.urlbarChangeTracker.finishedLoad(); + } + + if (!this.mBrowser.mIconURL) + this.mTabBrowser.useDefaultIcon(this.mTab); + } + + if (this.mBlank) + this.mBlank = false; + + // For keyword URIs clear the user typed value since they will be changed into real URIs. + if (location && location.scheme == "keyword") + this.mBrowser.userTypedValue = null; + + if (this.mTab.label == this.mTabBrowser.mStringBundle.getString("tabs.loading")) + 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 & (Ci.nsIWebProgressListener.STATE_START | + Ci.nsIWebProgressListener.STATE_STOP)) { + // reset cached temporary values at beginning and end + this.mMessage = ""; + this.mTotalProgress = 0; + } + this.mStateFlags = aStateFlags; + this.mStatus = aStatus; + }, + + onLocationChange: function (aWebProgress, aRequest, aLocation, aFlags) { + // OnLocationChange is called for both the top-level content + // and the subframes. + let topLevel = aWebProgress.isTopLevel; + + if (topLevel) { + let isSameDocument = + !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT); + // We need to clear the typed value + // if the document failed to load, to make sure the urlbar reflects the + // failed URI (particularly for SSL errors). However, don't clear the value + // if the error page's URI is about:blank, because that causes complete + // loss of urlbar contents for invalid URI errors (see bug 867957). + // Another reason to clear the userTypedValue is if this was an anchor + // navigation initiated by the user. + if (this.mBrowser.didStartLoadSinceLastUserTyping() || + ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) && + aLocation.spec != "about:blank") || + (isSameDocument && this.mBrowser.inLoadURI)) + this.mBrowser.userTypedValue = null; + + // Don't clear the favicon if this onLocationChange was + // triggered by a pushState or a replaceState (bug 550565) or + // a hash change (bug 408415). + if (aWebProgress.isLoadingDocument && !isSameDocument) + this.mBrowser.mIconURL = null; + } + + 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.mMessage = aMessage; + + this.mTabBrowser._callProgressListeners(this.mBrowser, "onStatusChange", + [aWebProgress, aRequest, aStatus, aMessage]); + }, + + onSecurityChange: function (aWebProgress, aRequest, aState) { + this.mTabBrowser._callProgressListeners(this.mBrowser, "onSecurityChange", + [aWebProgress, aRequest, aState]); + }, + + onRefreshAttempted: function(aWebProgress, aURI, aDelay, aSameURI) + { + var allowRefresh = true; + if (this.mTabBrowser.mCurrentTab == this.mTab) { + this.mTabBrowser.mProgressListeners.forEach( + function notifyRefreshAttempted(element) { + if (element && "onRefreshAttempted" in element) { + try { + if (!element.onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI)) + allowRefresh = false; + } catch (e) { + Cu.reportError(e); + } + } + } + ); + } + + this.mTabBrowser.mTabsProgressListeners.forEach( + function notifyRefreshAttempted(element) { + if (element && "onRefreshAttempted" in element) { + try { + if (!element.onRefreshAttempted(this.mBrowser, aWebProgress, aURI, aDelay, aSameURI)) + allowRefresh = false; + } catch (e) { + Cu.reportError(e); + } + } + } + , this); + return allowRefresh; + }, + + addFeed: function(aLink) + { + this.mFeeds.push(aLink); + }, + + QueryInterface: function(aIID) { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsIWebProgressListener2) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.NS_NOINTERFACE; + } + }); + ]]> + </body> + </method> + + <method name="mInstallSH"> + <parameter name="aBrowser"/> + <parameter name="aSH"/> + <body> + <![CDATA[ + return ({ + mBrowser: aBrowser, + mSH: aSH, + + onProgressChange : function (aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onStateChange : function(aWebProgress, aRequest, aStateFlags, aStatus) + { + if ((aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) && + (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)) { + function refresh(closure) { + closure.mBrowser.webNavigation.sessionHistory = closure.mSH; + closure.mBrowser.webProgress.removeProgressListener(closure); + delete closure.mBrowser._SHListener; + closure.mSH.QueryInterface(Ci.nsIWebNavigation) + .gotoIndex(closure.mSH.index); + } + setTimeout(refresh, 0, this); + } + }, + + onLocationChange : function(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange : function(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange : function(aWebProgress, aRequest, aState) + { + }, + + QueryInterface : function(aIID) + { + if (aIID.equals(Ci.nsIWebProgressListener) || + aIID.equals(Ci.nsISupportsWeakReference) || + aIID.equals(Ci.nsISupports)) + return this; + throw Cr.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) { + if (!(aURI instanceof Ci.nsIURI)) { + aURI = makeURI(aURI); + } + + // We do not serialize the principal from within nsSessionStore.js, + // hence if aLoadingPrincipal is null we default to the + // systemPrincipal which will allow the favicon to load. + let loadingPrincipal = aLoadingPrincipal || + Services.scriptSecurityManager.getSystemPrincipal(); + + PlacesUIUtils.loadFavicon(browser, loadingPrincipal, aURI); + } + + let sizedIconUrl = browser.mIconURL || ""; + if (sizedIconUrl != aTab.getAttribute("image")) { + if (sizedIconUrl) + aTab.setAttribute("image", sizedIconUrl); + else + aTab.removeAttribute("image"); + this._tabAttrModified(aTab, ["image"]); + } + + this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]); + ]]> + </body> + </method> + + <method name="getIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let browser = aTab ? aTab.linkedBrowser : this.selectedBrowser; + return browser.mIconURL; + ]]> + </body> + </method> + + <method name="buildFavIconString"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI); + } catch (e) { + } + return aURI.resolve("/favicon.ico"); + ]]> + </body> + </method> + + <method name="shouldLoadFavIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI); + } catch (e) { + } + return (aURI && Services.prefs.getBoolPref("browser.chrome.site_icons") && + Services.prefs.getBoolPref("browser.chrome.favicons") && + ("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https"))); + ]]> + </body> + </method> + + <method name="useDefaultIcon"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = this.getBrowserForTab(aTab); + var documentURI = browser.documentURI; + var icon = null; + + if (browser.imageDocument) { + if (Services.prefs.getBoolPref("browser.chrome.site_icons")) { + let sz = Services.prefs.getIntPref("browser.chrome.image_icons.max_size"); + if (browser.imageDocument.width <= sz && + browser.imageDocument.height <= sz) { + icon = browser.currentURI; + } + } + } + + // Use documentURIObject in the check for shouldLoadFavIcon so that we + // do the right thing with about:-style error pages. Bug 453442 + if (!icon && this.shouldLoadFavIcon(documentURI)) { + let url = documentURI.prePath + "/favicon.ico"; + if (!this.isFailedIcon(url)) + icon = url; + } + this.setIcon(aTab, icon, browser.contentPrincipal); + ]]> + </body> + </method> + + <method name="isFailedIcon"> + <parameter name="aURI"/> + <body> + <![CDATA[ + if (!(aURI instanceof Ci.nsIURI)) + aURI = makeURI(aURI); + return PlacesUtils.favicons.isFailedFavicon(aURI); + ]]> + </body> + </method> + + <method name="loadFavIcon"> + <parameter name="aURI"/> + <parameter name="aAttr"/> + <parameter name="aElt"/> + <parameter name="aLoadingPrincipal"/> + <body> + <![CDATA[ + let iconURL = this.buildFavIconString(aURI); + let iconURI = Services.io.newURI(iconURL); + let faviconFlags = this.usePrivateBrowsing ? + PlacesUtils.favicons.FAVICON_LOAD_PRIVATE + : PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE; + let loadingPrincipal = aLoadingPrincipal || + Services.scriptSecurityManager.getSystemPrincipal(); + + PlacesUtils.favicons + .setAndFetchFaviconForPage(aURI, iconURI, false, faviconFlags, + null, + loadingPrincipal); + if (PlacesUtils.favicons.isFailedFavicon(aURI)) { + return; + } + + aElt.setAttribute(aAttr, iconURL); + ]]> + </body> + </method> + + <method name="addToMissedIconCache"> + <parameter name="aURI"/> + <body> + <![CDATA[ + let uri = Services.io.newURI(aURI); + PlacesUtils.favicons.addFailedFavicon(uri); + ]]> + </body> + </method> + + <method name="getTitleForURI"> + <parameter name="aURI"/> + <body> + <![CDATA[ + try { + aURI = Services.uriFixup.createExposableURI(aURI).spec; + } catch (e) { + aURI = aURI.spec; + } + + if (aURI == "about:blank") + return ""; + + // We have a URI. Let's try to unescape it using a character set + // in case the URI is not ASCII. + try { + let characterSet = this.mCurrentBrowser.contentDocument.characterSet; + let textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"] + .getService(Ci.nsITextToSubURI); + aURI = textToSubURI.unEscapeNonAsciiURI(characterSet, aURI); + } catch (e) { + // Do nothing. + } + return aURI; + ]]> + </body> + </method> + + <method name="updateUrlBar"> + <parameter name="aWebProgress"/> + <parameter name="aRequest"/> + <parameter name="aLocation"/> + <parameter name="aFlags"/> + <parameter name="aSecurityUI"/> + <parameter name="aBrowser"/> + <parameter name="aFeeds"/> + <body> + <![CDATA[ + this.mProgressListeners.forEach( + function notifyUrlBar(element) { + try { + if ("onLocationChange" in element) + element.onLocationChange(aWebProgress, aRequest, aLocation, aFlags); + // If switching tabs, the security may have changed. + if (aSecurityUI && "onSecurityChange" in element) + element.onSecurityChange(aWebProgress, null, aSecurityUI.state); + // If the document already exists, just resend cached data. + if (!aRequest && aWebProgress.isTopLevel) { + if (aBrowser.mIconURL && "onLinkIconAvailable" in element) + element.onLinkIconAvailable(aBrowser.mIconURL); + if ("onFeedAvailable" in element) { + aFeeds.forEach( + function notifyFeedAvailable(feed) { + element.onFeedAvailable(feed); + } + ); + } + } + } catch (e) { + Cu.reportError(e); + } + } + ); + ]]> + </body> + </method> + + <method name="getWindowTitleForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + var newTitle = ""; + var docTitle; + var docElement = this.ownerDocument.documentElement; + var sep = docElement.getAttribute("titlemenuseparator"); + var modifier = docElement.getAttribute("titlemodifier"); + + // Strip out any null bytes in the content title, since the + // underlying widget implementations of nsWindow::SetTitle pass + // null-terminated strings to system APIs. + if (aBrowser.docShell.contentViewer) + docTitle = aBrowser.contentTitle.replace(/\0/g, ""); + + if (!docTitle && !modifier) { + docTitle = this.getTitleForURI(aBrowser.currentURI); + if (!docTitle) { + // Here we actually override contenttitlesetting, because we + // don't want the titledefault value. + docTitle = this.mStringBundle.getString("tabs.untitled"); + } + } + + if (docTitle) { + newTitle += docElement.getAttribute("titlepreface") + 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 + // (only for schemes that support a host) + try { + if (docElement.getAttribute("chromehidden").includes("location")) { + let uri = Services.uriFixup.createExposableURI( + aBrowser.currentURI); + if (uri.schemeIs("about")) + newTitle = uri.spec + sep + newTitle; + else if (uri.host) + newTitle = uri.prePath + sep + newTitle; + } + } catch (e) { + } + + return newTitle; + ]]> + </body> + </method> + + <method name="updateTitlebar"> + <body> + <![CDATA[ + var newTitle = this.getWindowTitleForBrowser(this.mCurrentBrowser); + document.title = newTitle; + window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIBaseWindow).title = newTitle; + ]]> + </body> + </method> + + <method name="updatePopupMenu"> + <parameter name="aPopupMenu"/> + <body> + <![CDATA[ + this.mContextTab = aPopupMenu.triggerNode; + // The user might right-click on a tab or an empty part of the tabbar. + var isTab = this.mContextTab.localName == "tab"; + var isMultiple = this.tabs.length > 1; + var isAtEnd = this.getTabsToTheEndFrom(this.mContextTab).length == 0; + var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "*"); + for (let menuitem of menuItems) { + let tbattr = menuitem.getAttribute("tbattr"); + + if (tbattr.includes("tabbrowser-undoclosetab")) { + menuitem.disabled = (this.usePrivateBrowsing ? + this.savedBrowsers.length : + this.mSessionStore.getClosedTabCount(window)) == 0; + menuitem.hidden = (this.usePrivateBrowsing || + Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo") <= 0) && + Services.prefs.getIntPref("browser.tabs.max_tabs_undo") <= 0; + } + else + menuitem.disabled = + (tbattr.includes("tabbrowser-totheend") && isAtEnd) || + (tbattr.includes("tabbrowser-multiple") && !isMultiple) || + (tbattr.includes("tabbrowser-tab") && !isTab); + } + ]]> + </body> + </method> + + <field name="mAeroPeek">false</field> + + <method name="updateCurrentBrowser"> + <body> + <![CDATA[ + var newBrowser = this.mPanelContainer.selectedPanel.firstChild.firstChild; + var oldBrowser = this.mCurrentBrowser; + + // Transfer the dropped link handler to the new browser. + // Note: closing the current tab sets mCurrentBrowser to null + // so we use mCurrentTab.linkedBrowser instead. + newBrowser.droppedLinkHandler = this.mCurrentTab.linkedBrowser.droppedLinkHandler; + newBrowser.showWindowResizer = this.mCurrentTab.linkedBrowser.showWindowResizer; + newBrowser.docShellIsActive = this.mCurrentTab.linkedBrowser.docShellIsActive; + if (this.mCurrentBrowser) { + this.mCurrentBrowser.droppedLinkHandler = null; + this.mCurrentBrowser.docShellIsActive = false; + this.mCurrentBrowser.removeAttribute("primary"); + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.removeResultListener(l)); + } + + let oldTab = this.mCurrentTab; + + // Preview mode should not reset the owner. + if (!this._previewMode && !oldTab.selected) + oldTab.owner = null; + + let lastRelatedTab = this.mLastRelatedIndex ? this.tabs[this.mLastRelatedIndex] : null; + if (lastRelatedTab && !lastRelatedTab.selected) { + lastRelatedTab.owner = null; + } + + newBrowser.setAttribute("primary", "true"); + this.mCurrentBrowser = newBrowser; + this.mCurrentTab = this.selectedTab; + this.mCurrentTab.removeAttribute("unread"); + this.finder.mListeners.forEach(l => this.mCurrentBrowser.finder.addResultListener(l)); + + var tabListener = this.mTabListeners[this.tabContainer.selectedIndex]; + + if (!oldBrowser || + (!oldBrowser.blockedPopups != !newBrowser.blockedPopups)) + this.mCurrentBrowser.updateBlockedPopups(); + + // Update the URL bar. + this.updateUrlBar(newBrowser.webProgress, + null, + newBrowser.currentURI, + 0, + newBrowser.securityUI, + newBrowser, + tabListener.mFeeds); + + // Send the state, status and progress to all progress listeners. + var flags = tabListener.mStateFlags & + (Ci.nsIWebProgressListener.STATE_START | + Ci.nsIWebProgressListener.STATE_STOP); + this._callProgressListeners(null, "onStateChange", + [this.mCurrentBrowser.webProgress, + tabListener.mRequest, + flags, + tabListener.mStatus], + true, false); + + this._callProgressListeners(null, "onStatusChange", + [this.mCurrentBrowser.webProgress, + tabListener.mRequest, + tabListener.mStatus, + tabListener.mMessage], + true, false); + + // Also send the onUpdateCurrentBrowser event for compatibility + this._callProgressListeners(null, "onUpdateCurrentBrowser", + [tabListener.mStateFlags, + tabListener.mStatus, + tabListener.mMessage, + tabListener.mTotalProgress], + true, false); + + if (this.mAeroPeek) + return; + + // we only want to return to the parent tab if no other + // tabs have been opened and the user hasn't switched tabs + this.mPreviousTab = null; + this.mLastRelatedIndex = 0; + + // Update the window title. + this.updateTitlebar(); + + // FAYT + this.fastFind.setDocShell(this.mCurrentBrowser.docShell); + + // We've selected the new tab, so go ahead and notify listeners + this.mCurrentTab.dispatchEvent(new Event("TabSelect", + { bubbles: true, cancelable: false })); + + if (!document.commandDispatcher.focusedElement || + document.commandDispatcher.focusedElement.parentNode != + this.mCurrentTab.parentNode) { + // The focus was not on one of our tabs, so focus the new browser. + newBrowser.focus(); + } + ]]> + </body> + </method> + + <method name="onTabClick"> + <parameter name="event"/> + <body> + <![CDATA[ + // A middle mouse button click on a tab is a short cut for + // closing that tab. + if (event.button != 1 || event.target.localName != 'tab') + return; + + this.removeTab(event.target); + event.stopPropagation(); + event.preventDefault(); + ]]> + </body> + </method> + + <method name="onLinkEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var link = aEvent.originalTarget; + var href = link.href; + if (!href) + return; + + var targetDoc = link.ownerDocument; + var index = this.getBrowserIndexForDocument(targetDoc); + if (index < 0) + return; + + var rel = link.rel; + var type = link.type; + var isIcon = /(?:^|\s)icon(?:\s|$)/i.test(rel) && + Services.prefs.getBoolPref("browser.chrome.site_icons"); + if (isIcon) { + var iconUri = this.getLinkIconURI(link); + if (iconUri) + this.setIcon(this.tabs[index], iconUri, + link.nodePrincipal); + return; + } + + if (aEvent.type == "DOMLinkChanged") + return; + + var isFeed = /(?:^|\s)feed(?:\s|$)/i.test(rel) || + (/(?:^|\s)alternate(?:\s|$)/i.test(rel) && + !/(?:^|\s)stylesheet(?:\s|$)/i.test(rel) && + /^\s*application\/(?:atom|rss)\+xml\s*$/i.test(type)); + + if (!isFeed) + return; + + try { + let feedURI = Services.io.newURI(href, targetDoc.characterSet); + if (!/^https?$/.test(feedURI.scheme)) { + return; + } + urlSecurityCheck(feedURI, targetDoc.nodePrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); + } catch(e) { + return; + } + + this.mTabListeners[index].addFeed(link); + if (this.browsers[index] == this.mCurrentBrowser) { + this.mProgressListeners.forEach( + function notifyFeedAvailable(element) { + if ("onFeedAvailable" in element) { + try { + element.onFeedAvailable(link); + } catch (e) { + Cu.reportError(e); + } + } + } + ); + } + ]]> + </body> + </method> + + <method name="getLinkIconURI"> + <parameter name="aLink"/> + <body><![CDATA[ + var targetDoc = aLink.ownerDocument; + // Make a URI out of our href. + var uri = Services.io.newURI(aLink.href, targetDoc.characterSet); + + // Verify that the load of this icon is legal. + // Some error or special pages can load their favicon. + // To be on the safe side, only allow chrome:// favicons. + const re = /^about:(neterror|certerror|blocked)\?/; + var isAllowedPage = re.test(targetDoc.documentURI); + + if (!isAllowedPage || !uri.schemeIs("chrome")) { + try { + urlSecurityCheck(uri,targetDoc.nodePrincipal, + Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); + } catch(e) { + return null; + } + } + + // Security says okay, now ask content policy + try { + var contentPolicy = + Cc['@mozilla.org/layout/content-policy;1'] + .getService(Ci.nsIContentPolicy); + } catch (e) { + return null; // Refuse to load if we can't do a security check. + } + + if (contentPolicy.shouldLoad(Ci.nsIContentPolicy.TYPE_INTERNAL_IMAGE, + uri, targetDoc.documentURIObject, + aLink, aLink.type, + null) != Ci.nsIContentPolicy.ACCEPT) { + return null; + } + return uri; + ]]></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.loading"); + aTab.crop = "end"; + this._tabAttrModified(aTab, ["label", "crop"]); + ]]> + </body> + </method> + + <method name="setTabTitle"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var browser = aTab.linkedBrowser; + var title = browser.contentTitle; + var crop = "end"; + + if (!title) { + title = this.getTitleForURI(browser.currentURI); + + if (title) + crop = "center"; + else + title = this.mStringBundle.getString("tabs.untitled"); + } + aTab.label = title; + aTab.crop = crop; + ]]> + </body> + </method> + + <method name="setStripVisibilityTo"> + <parameter name="aShow"/> + <body> + <![CDATA[ + this.mStrip.collapsed = !aShow; + ]]> + </body> + </method> + + <method name="getStripVisibility"> + <body> + return !this.mStrip.collapsed; + </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 params = aReferrerURI; + if (!params || params instanceof Ci.nsIURI) { + params = { + triggeringPrincipal: Services.scriptSecurityManager + .getSystemPrincipal(), + referrerURI: aReferrerURI, + charset: aCharset, + postData: aPostData, + inBackground: aLoadInBackground, + allowThirdPartyFixup: aAllowThirdPartyFixup, + allowMixedContent: false, + userContextId: null, + opener: null, + }; + } + + params.focusNewTab = params.inBackground != null ? + !params.inBackground : + !Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + + if (params.focusNewTab) + params.ownerTab = this.selectedTab; + + return this.addTab(aURI, params); + ]]> + </body> + </method> + + <method name="loadTabs"> + <parameter name="aURIs"/> + <parameter name="aLoadInBackground"/> + <parameter name="aReplace"/> + <body><![CDATA[ + let aAllowThirdPartyFixup; + let aPostDatas = []; + let aUserContextId; + let aTriggeringPrincipal; + + // Additional parameters are in a params object. + // Firefox uses additional parameters not supported here. + if (arguments.length == 2 && + typeof arguments[1] == "object") { + let params = arguments[1]; + aLoadInBackground = params.inBackground; + aReplace = params.replace; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aPostDatas = params.postDatas || aPostDatas; + aUserContextId = params.userContextId; + aTriggeringPrincipal = params.triggeringPrincipal; + } + + 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; + 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], + triggeringPrincipal : aTriggeringPrincipal, + }); + } 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, + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[0], + userContextId: aUserContextId, + triggeringPrincipal: aTriggeringPrincipal, + }); + } + + let tabNum = targetTabIndex; + for (let i = 1; i < aURIs.length; ++i) { + let tab = this.addTab(aURIs[i], { + allowThirdPartyFixup: aAllowThirdPartyFixup, + postData: aPostDatas[i], + userContextId: aUserContextId, + triggeringPrincipal: aTriggeringPrincipal, + }); + 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="aFocusNewTab"/> + <parameter name="aAllowThirdPartyFixup"/> + <body> + <![CDATA[ + var aTriggeringPrincipal; + var aReferrerPolicy; + var aFromExternal; + var aOwner; + var aRelatedToCurrent; + var aAllowMixedContent; + var aNoReferrer; + var aUserContextId; + var aOriginPrincipal; + var aOpener; + if (arguments.length == 2 && + arguments[1] != null && + 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; + aFocusNewTab = params.focusNewTab; + aAllowThirdPartyFixup = params.allowThirdPartyFixup; + aFromExternal = params.fromExternal; + aRelatedToCurrent = params.relatedToCurrent; + aAllowMixedContent = params.allowMixedContent; + aNoReferrer = params.noReferrer; + aUserContextId = params.userContextId; + aOriginPrincipal = params.originPrincipal; + aOpener = params.opener; + } + + // If we're adding tabs, we're past interrupt mode, ditch the owner. + if (this.mCurrentTab.owner) + this.mCurrentTab.owner = null; + + this._browsers = null; // invalidate cache + + var t = this.referenceTab.cloneNode(true); + + var blank = !aURI || aURI == "about:blank"; + + if (!blank) + t.setAttribute("label", aURI); + + this.tabContainer.appendChild(t); + + var b = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "browser"); + b.setAttribute("type", "content"); + b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu")); + b.setAttribute("tooltip", this.getAttribute("contenttooltip")); + b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup")); + if (this.hasAttribute("datetimepicker")) { + b.setAttribute("datetimepicker", this.getAttribute("datetimepicker")); + } + + // Check if we have a "parent" window which we need to set as our opener + if (aOpener) { + b.presetOpenerWindow(aOpener); + } + + // Create the browserStack container + var stack = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "stack"); + stack.setAttribute("anonid", "browserStack"); + stack.appendChild(b); + stack.setAttribute("flex", "1"); + + // Add the Message and the Browser to the box + var n = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "notificationbox"); + n.setAttribute("class", "browser-notificationbox"); + n.setAttribute("popupnotification", this.getAttribute("popupnotification")); + n.appendChild(stack); + + var uniqueId = "panel" + this.nextTabNumber++; + n.id = uniqueId; + t.linkedPanel = uniqueId; + t.linkedBrowser = b; + if (t.previousSibling.selected) + t.setAttribute("afterselected", true); + + // Prevent the superfluous initial load of a blank document + // if we're going to load something other than about:blank. + if (!blank) + 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(n); + + // We start our browsers out as inactive. + b.docShellIsActive = false; + + this.mStrip.collapsed = false; + + Services.prefs.setBoolPref("browser.tabs.forceHide", false); + + // If this new tab is owned by another, assert that relationship. + if (aOwner) + t.owner = aOwner; + + // wire up a progress listener for the new browser object. + var position = this.tabs.length - 1; + var tabListener = this.mTabProgressListener(t, b, blank); + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + if (!blank) { + // pretend the user typed this so it'll be available till + // the document successfully loads + b.userTypedValue = aURI; + + 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; + if (aFromExternal) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL; + if (aAllowMixedContent) + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; + try { + b.loadURIWithFlags(aURI, { + flags, + triggeringPrincipal : aTriggeringPrincipal, + referrerURI: aNoReferrer ? null : aReferrerURI, + charset: aCharset, + referrerPolicy: aReferrerPolicy, + postData: aPostData, + }); + } + catch (ex) { } + } + + t.dispatchEvent(new Event("TabOpen", + { bubbles: true, cancelable: false })); + + // 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 || aReferrerURI || + Services.prefs.getBoolPref("browser.tabs.insertAllTabsAfterCurrent")) && + Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) { + var lastRelatedIndex = this.mLastRelatedIndex || + this.tabContainer.selectedIndex; + if (this.mLastRelatedIndex) + this.tabs[this.mLastRelatedIndex].owner = null; + else + t.owner = this.selectedTab; + this.moveTabTo(t, ++lastRelatedIndex); + this.mLastRelatedIndex = lastRelatedIndex; + } + + if (aFocusNewTab) { + var parentTab = this.selectedTab; + this.selectedTab = t; + this.mPreviousTab = parentTab; + } + else { + // The user opened a background tab, so updateCurrentBrowser + // won't be called. Explicitly clear the previous tab. + this.mPreviousTab = null; + } + this.tabContainer._handleNewTab(t); + + return t; + ]]> + </body> + </method> + + <method name="warnAboutClosingTabs"> + <parameter name="aCloseTabs"/> + <parameter name="aTab"/> + <body> + <![CDATA[ + var tabsToClose; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + tabsToClose = this.tabs.length; + break; + case this.closingTabsEnum.OTHER: + tabsToClose = this.tabs.length - 1; + 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.warnOnCloseOther"; + if (!Services.prefs.getBoolPref(pref)) + return true; + + //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 warningTitle; + var warningMessage; + var closeButton; + var promptMessage; + switch (aCloseTabs) { + case this.closingTabsEnum.ALL: + warningTitle = "tabs.closeWarningTitleAll"; + warningMessage = + PluralForm.get(tabsToClose, + bundle.getString("tabs.closeWarningAll")); + closeButton = "tabs.closeButtonAll"; + promptMessage = "tabs.closeWarningPromptMeAll"; + break; + case this.closingTabsEnum.OTHER: + // fall through + case this.closingTabsEnum.TO_END: + // fall through + default: + warningTitle = "tabs.closeWarningTitle"; + warningMessage = + PluralForm.get(tabsToClose, + bundle.getString("tabs.closeWarningOther")); + closeButton = "tabs.closeButton"; + promptMessage = "tabs.closeWarningPromptMe"; + break; + } + + var ps = Services.prompt; + var buttonPressed = + ps.confirmEx(window, + bundle.getString(warningTitle), + warningMessage.replace("#1", tabsToClose), + (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1), + bundle.getString(closeButton), + null, null, + bundle.getString(promptMessage), + warnOnClose); + var reallyClose = (buttonPressed == 0); + // Don't set the pref unless they press OK and it's false + if (reallyClose && !warnOnClose.value) + Services.prefs.setBoolPref(pref, false); + + return reallyClose; + ]]> + </body> + </method> + + <method name="getTabsToTheEndFrom"> + <parameter name="aTab"/> + <body> + <![CDATA[ + let tabsToEnd = []; + let tabs = this.tabs; + 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]); + } + } + ]]> + </body> + </method> + + <method name="removeAllTabsBut"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) { + this.selectedTab = aTab; + + for (let i = this.tabs.length - 1; i >= 0; --i) { + if (this.tabs[i] != aTab) + this.removeTab(this.tabs[i]); + } + } + ]]> + </body> + </method> + + <method name="removeCurrentTab"> + <parameter name="aParams"/> + <body> + <![CDATA[ + return this.removeTab(this.mCurrentTab, aParams); + ]]> + </body> + </method> + + <method name="isBrowserEmpty"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + return aBrowser.sessionHistory.count < 2 && + aBrowser.currentURI.spec == "about:blank" && + !aBrowser.contentDocument.body.hasChildNodes(); + ]]> + </body> + </method> + + <method name="getUndoList"> + <body> + <![CDATA[ + var tabData = this.usePrivateBrowsing ? this.savedBrowsers : + JSON.parse(this.mSessionStore.getClosedTabData(window)); + return tabData.map(function(aTabData) { return aTabData.title; }); + ]]> + </body> + </method> + + <method name="undoCloseTab"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (this.usePrivateBrowsing) + return this.savedBrowsers.length ? this.restoreTab(aIndex) : null; + + return this.mSessionStore.getClosedTabCount(window) ? + this.mSessionStore.undoCloseTab(window, aIndex) : null; + ]]> + </body> + </method> + + <method name="restoreTab"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (aIndex >= this.savedBrowsers.length || aIndex < 0) + return null; + + this._browsers = null; + + var savedData = this.savedBrowsers.splice(aIndex, 1)[0]; + var t = savedData.browserData.tab; + var b = savedData.browserData.browser; + var hist = savedData.browserData.history; + + this.tabContainer.appendChild(t); + if (t.previousSibling.selected) + t.setAttribute("afterselected", true); + + // navigate back to the proper page from the light page + b.stop(); + b.webNavigation.gotoIndex(0); + + // reattach the old history + b.webNavigation.sessionHistory = hist; + + // add back the filters, security first (bug 313335) + var secFlags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | + Ci.nsIWebProgress.NOTIFY_LOCATION | + Ci.nsIWebProgress.NOTIFY_SECURITY; + b.webProgress.addProgressListener(b.securityUI, secFlags); + + var position = this.tabs.length - 1; + var tabListener = this.mTabProgressListener(t, b, false); + const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + b.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[position] = tabListener; + this.mTabFilters[position] = filter; + + t.dispatchEvent(new Event("TabOpen", + { bubbles: true, cancelable: false })); + + if (savedData.pos < position) + this.moveTabTo(t, savedData.pos); + + if (this.tabs.length == 2 && this.isBrowserEmpty(this)) + this.removeCurrentTab({ disableUndo: true }); + else { + this.selectedTab = t; + this.mStrip.collapsed = false; + } + this.tabContainer._handleNewTab(t); + + return t; + ]]> + </body> + </method> + + <method name="removeBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + let panel = this.getNotificationBox(aBrowser); + panel.destroy(); + aBrowser.destroy(); + + // The pagehide event that this removal triggers is safe + // because the browser is no longer current at this point. + panel.remove(); + + // Fix up the selected panel. + panel = this.getNotificationBox(this.selectedTab.linkedBrowser); + this.mTabBox.selectedPanel = panel; + ]]> + </body> + </method> + + <method name="removeTab"> + <parameter name="aTab"/> + <parameter name="aParams"/> + <body> + <![CDATA[ + this.mLastRelatedIndex = 0; + + if (!aParams) { + aParams = { + animate: false, + disableUndo: false + }; + } + + var oldBrowser = aTab.linkedBrowser; + + var ds = oldBrowser.docShell; + + if (ds.contentViewer && !ds.contentViewer.permitUnload()) + return; + + // We're committed to closing the tab now. + var l = this.tabs.length; + switch (l) { + case 1: + // add a new blank tab to replace the one we're about to close + // (this ensures that the remaining tab is as good as new) + this.addTab("about:blank"); + l++; + // fall through + case 2: + if (Services.prefs.getBoolPref("browser.tabs.autoHide")) + this.mStrip.collapsed = true; + } + + // Dispatch a notification. + // We dispatch it before any teardown so that event listeners can + // inspect the tab that's about to close. + aTab.dispatchEvent(new UIEvent("TabClose", + { bubbles: true, cancelable: false, view: window, + detail: !!aParams.disableUndo })); + var tabData = aTab.tabData || {}; + tabData.pos = this.getTabIndex(aTab); + tabData.panel = this.getNotificationBox(oldBrowser).id; + tabData.title = oldBrowser.contentDocument.title || + this.getTitleForURI(oldBrowser.currentURI) || + this.mStringBundle.getString("tabs.untitled"); + + var index = this.getTabIndex(aTab); + + // Remove SSL listener + oldBrowser.webProgress.removeProgressListener(oldBrowser.securityUI); + + // Remove the tab's filter and progress listener. + const filter = this.mTabFilters[index]; + oldBrowser.webProgress.removeProgressListener(filter); + filter.removeProgressListener(this.mTabListeners[index]); + this.mTabFilters.splice(index, 1); + this.mTabListeners.splice(index, 1); + + // We are no longer the primary content area + oldBrowser.removeAttribute("primary"); + + // Remove this tab as the owner of any other tabs, since it's going away. + for (let tab of this.tabs) { + if ("owner" in tab && tab.owner == aTab) + // |tab| is a child of the tab we're removing, make it an orphan. + tab.owner = null; + } + + // Now select the new tab before nuking the old one. + var currentIndex = this.tabContainer.selectedIndex; + + var newIndex = -1; + if (currentIndex > index) + newIndex = currentIndex - 1; + else if (currentIndex < index) + newIndex = currentIndex; + else if (index == l - 1) + newIndex = index - 1; + else + newIndex = index; + + if (oldBrowser == this.mCurrentBrowser) + this.mCurrentBrowser = null; + + // Invalidate browsers cache, as the tab is removed from the + // tab container. + this._browsers = null; + + let owner = ("owner" in aTab) ? aTab.owner : null; + + // Clean up before/after selected attributes before removing the + // tab. + aTab._selected = false; + aTab.remove(); + + // When the current tab is removed select a new tab + // and fire select events on tabpanels and tabs + if (owner && !owner.hidden && !owner.closing && + Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) { + this.selectedTab = owner; + } + else if (this.mPreviousTab && (aTab == this.mCurrentTab)) + this.selectedTab = this.mPreviousTab; + else { + this.tabContainer.selectedIndex = newIndex; + + // We need to explicitly clear this, because updateCurrentBrowser + // doesn't get called for a background tab + this.mPreviousTab = null; + } + + // Save the tab for undo. + // Even though we navigate to about:blank, it costs more RAM than + // really closing the tab. The pref controls how far you can undo + var maxUndoDepth = Services.prefs.getIntPref("browser.tabs.max_tabs_undo"); + var oldSH = oldBrowser.webNavigation.sessionHistory; + var inOnLoad = oldBrowser.docShell.isExecutingOnLoadHandler; + var isPopup = oldBrowser.contentWindow.opener && + !Services.prefs.getBoolPref("browser.tabs.cache_popups"); + if (maxUndoDepth <= 0 || aParams.disableUndo || inOnLoad || isPopup || this.isBrowserEmpty(oldBrowser)) { + // Undo is disabled/tab is blank. Kill the browser for real. + // 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. Also fix up the selected panel in the case + // the removed browser was to the left of the current browser. + this.removeBrowser(oldBrowser); + return; + } + + // preserve a pointer to the browser for undoing the close + // 1. save a copy of the session history (oldSH) + // 2. hook up a new history + // 3. add the last history entry from the old history the new + // history so we'll be able to go back from about:blank + // 4. load a light URL in the browser, pushing the current page + // into bfcache - allows for saving of JS modifications + // and also saves RAM by allowing bfcache to evict the full page + + tabData.browserData = { + tab: aTab, + browser: oldBrowser, + history: oldSH, + toJSON: function() {} // hides this object from JSON.stringify + }; + this.savedBrowsers.unshift(tabData); + + var newSH = Cc["@mozilla.org/browser/shistory;1"] + .createInstance(Ci.nsISHistory); + oldBrowser.webNavigation.sessionHistory = newSH; + var entry = oldSH.getEntryAtIndex(oldSH.index) + .QueryInterface(Ci.nsISHEntry) + .clone(); + // The bfcache entry is tightly coupled to the original shistory it + // belongs to, better to drop it. + entry.abandonBFCacheEntry(); + // don't try to repost data when restoring the tab + entry.postData = null; + newSH.addEntry(entry, true); + + // about:blank is light + oldBrowser.loadURI("about:blank"); + + // remove overflow from the undo stack + if (this.savedBrowsers.length > maxUndoDepth) { + tabData = this.savedBrowsers.pop(); + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + } + ]]> + </body> + </method> + + <method name="forgetSavedBrowser"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + if (aIndex >= this.savedBrowsers.length || aIndex < 0) + return false; + + var tabData = this.savedBrowsers.splice(aIndex, 1)[0]; + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + return true; + ]]> + </body> + </method> + + <method name="reloadAllTabs"> + <body> + <![CDATA[ + var l = this.tabs.length; + for (var i = 0; i < l; i++) { + try { + this.tabs[i].linkedBrowser.reload(); + } catch (e) { + // ignore failure to reload so others will be reloaded + } + } + ]]> + </body> + </method> + + <method name="reloadTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + this.getBrowserForTab(aTab).reload(); + ]]> + </body> + </method> + + <method name="addProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!aListener) + throw Cr.NS_ERROR_INVALID_ARG; + + if (this.mProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // push() does not disturb possibly ongoing iterations. + this.mProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!this.mProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // Create a new array, not to disturb possibly ongoing iterations. + this.mProgressListeners = + this.mProgressListeners.filter( + function removeListener(element) { + return element != aListener; + } + ); + ]]> + </body> + </method> + + <method name="addTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!aListener) + throw Cr.NS_ERROR_INVALID_ARG; + + if (this.mTabsProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // push() does not disturb possibly ongoing iterations. + this.mTabsProgressListeners.push(aListener); + ]]> + </body> + </method> + + <method name="removeTabsProgressListener"> + <parameter name="aListener"/> + <body> + <![CDATA[ + if (!this.mTabsProgressListeners.includes(aListener)) + throw Cr.NS_ERROR_FAILURE; + + // Create a new array, not to disturb possibly ongoing iterations. + this.mTabsProgressListeners = + this.mTabsProgressListeners.filter( + function removeListener(element) { + return element != aListener; + } + ); + ]]> + </body> + </method> + + <method name="_getTabForContentWindow"> + <parameter name="aWindow"/> + <body> + <![CDATA[ + const browsers = this.browsers; + for (var i = 0; i < browsers.length; ++i) + if (browsers[i].contentWindow == aWindow) + return this.tabs[i]; + + return null; + ]]> + </body> + </method> + + <method name="getTabForBrowser"> + <parameter name="aBrowser"/> + <body> + <![CDATA[ + for (var tab of this.tabs) + if (tab.linkedBrowser == aBrowser) + return tab; + + return null; + ]]> + </body> + </method> + + <method name="getBrowserForTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return aTab.linkedBrowser; + ]]> + </body> + </method> + + <method name="getBrowserForOuterWindowID"> + <parameter name="aID"/> + <body> + <![CDATA[ + for (var browser of this.browsers) + if (browser.outerWindowID == aID) + return browser; + + return null; + ]]> + </body> + </method> + + <method name="getTabIndex"> + <parameter name="aTab"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) + if (this.tabs[i] == aTab) + return i; + + throw Cr.NS_ERROR_ILLEGAL_VALUE; + ]]> + </body> + </method> + + <property name="popupAnchor" readonly="true"> + <getter><![CDATA[ + if (this.mCurrentTab._popupAnchor) { + return this.mCurrentTab._popupAnchor; + } + // Actually the notificationbox not the browserstack. + 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="selectTabAtIndex"> + <parameter name="aIndex"/> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // count backwards for aIndex < 0 + if (aIndex < 0) + aIndex += this.tabs.length; + + if (aIndex >= 0 && + aIndex < this.tabs.length && + aIndex != this.tabContainer.selectedIndex) + this.selectedTab = this.tabs[aIndex]; + + if (aEvent) { + aEvent.preventDefault(); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + + <property name="selectedTab"> + <getter> + return this.mTabBox.selectedTab; + </getter> + <setter> + <![CDATA[ + // Update the tab + this.mTabBox.selectedTab = val; + return val; + ]]> + </setter> + </property> + + <property name="selectedBrowser" + onget="return this.mCurrentBrowser;" + readonly="true"/> + + <property name="browsers" readonly="true"> + <getter> + <![CDATA[ + return this._browsers || + (this._browsers = Array.from(this.tabs, tab => tab.linkedBrowser)); + ]]> + </getter> + </property> + + <!-- Drag and drop observer API --> + <method name="_onDragStart"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var target = aEvent.target; + if (target.localName == "tab") { + var URI = target.linkedBrowser.currentURI; + var spec = URI.spec; + var title = target.linkedBrowser.contentTitle || spec; + var dt = aEvent.dataTransfer; + dt.mozSetDataAt("text/x-moz-url", spec + "\n" + title, 0); + dt.mozSetDataAt("text/uri-list", spec, 0); + dt.mozSetDataAt("text/plain", spec, 0); + dt.mozSetDataAt("text/html", '<a href="' + spec + '">' + title + '</a>', 0); + } + aEvent.stopPropagation(); + ]]> + </body> + </method> + + <method name="_onDragOver"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + aEvent.preventDefault(); + aEvent.stopPropagation(); + + var ib = document.getAnonymousElementByAttribute(this, "class", "tab-drop-indicator-bar"); + + // autoscroll the tab strip if we drag over the scroll buttons, + // even if we aren't dragging a tab + var pixelsToScroll = 0; + var tabStrip = this.tabContainer.arrowScrollbox; + var ltr = window.getComputedStyle(this, null).direction == "ltr"; + if (this.tabContainer.getAttribute("overflow") == "true") { + var targetAnonid = aEvent.originalTarget.getAttribute("anonid"); + switch (targetAnonid) { + case "scrollbutton-up": + pixelsToScroll = -tabStrip.scrollIncrement; + break; + case "scrollbutton-down": + case "alltabs-button": + pixelsToScroll = tabStrip.scrollIncrement; + break; + } + if (pixelsToScroll) + tabStrip.scrollByPixels((ltr ? 1 : -1) * pixelsToScroll); + } + + var ind = document.getAnonymousElementByAttribute(this, "class", "tab-drop-indicator"); + + var draggedTab = aEvent.dataTransfer.mozSourceNode; + var within = draggedTab && + draggedTab.parentNode == this.tabContainer; + var newIndexOn = within ? -1 : this.getDropOnIndex(aEvent); + + var ltr = window.getComputedStyle(this, null).direction == "ltr"; + var arrowX, tabBoxObject; + if (newIndexOn != -1) { + tabBoxObject = this.tabs[newIndexOn].boxObject; + arrowX = tabBoxObject.screenX + tabBoxObject.width / 2; + } + else { + var newIndexBetween = this.getDropIndex(aEvent); + if (within) { + var tabIndex = this.getTabIndex(draggedTab); + if (newIndexBetween == tabIndex || + newIndexBetween == tabIndex + 1) { + ib.collapsed = true; + return; + } + } + + if (newIndexBetween == this.tabs.length) { + tabBoxObject = this.tabs[this.tabs.length - 1].boxObject; + arrowX = tabBoxObject.x; + arrowX = tabBoxObject.screenX; + if (ltr) // for LTR "after" is on the right-hand side of the tab + arrowX += tabBoxObject.width; + } + else { + tabBoxObject = this.tabs[newIndexBetween].boxObject; + arrowX = tabBoxObject.screenX; + if (!ltr) // for RTL "before" is on the right-hand side of the tab + arrowX += tabBoxObject.width; + } + } + + var boxObject = tabStrip.scrollBoxObject; + // Check pixelsToScroll as well to prevent noticable judder. + if (pixelsToScroll > 0 || arrowX >= boxObject.screenX + boxObject.width) + arrowX = boxObject.screenX + boxObject.width; + else if (pixelsToScroll < 0 || arrowX < boxObject.screenX) + arrowX = boxObject.screenX; + + if (ltr) + ind.style.marginLeft = (arrowX - this.boxObject.screenX) + "px"; + else + ind.style.marginRight = (this.boxObject.screenX + this.boxObject.width - arrowX) + "px"; + + ib.collapsed = false; + ]]> + </body> + </method> + + <method name="_onDrop"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + document.getAnonymousElementByAttribute(this, "class", + "tab-drop-indicator-bar") + .collapsed = true; + aEvent.stopPropagation(); + + var newIndex = this.getDropIndex(aEvent); + var dt = aEvent.dataTransfer; + var draggedTab = dt.mozSourceNode; + if (draggedTab && draggedTab.parentNode == this.tabContainer) { + if (newIndex > this.getTabIndex(draggedTab)) + newIndex--; + this.moveTabTo(draggedTab, newIndex); + return; + } + + var url; + try { + // Pass true to disallow dropping javascript: or data: urls. + url = Services.droppedLinkHandler.dropLink(aEvent, {}, true); + } catch (ex) {} + + // Valid urls don't contain spaces ' '; if we have a space + // it isn't a valid url. + if (!url || url.includes(" ")) + return; + + getShortcutOrURIAndPostData(url).then(data => { + var bgLoad = Services.prefs.getBoolPref("browser.tabs.loadInBackground"); + if (aEvent.shiftKey) + bgLoad = !bgLoad; + + let triggeringPrincipal = browserDragAndDrop.getTriggeringPrincipal(aEvent); + + var tab = null; + tabIndex = this.getDropOnIndex(aEvent); + if (tabIndex != -1) { + // Load in an existing tab. + tab = this.tabs[tabIndex]; + tab.linkedBrowser.loadURI(data.url, { + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + if (this.mCurrentTab != tab && !bgLoad) + this.selectedTab = tab; + } + else if (dt.mozSourceDocument && + dt.mozSourceDocument.defaultView.top == content) { + // We're adding a new tab, and we may want parent-tab tracking. + + tab = this.loadOneTab(data.url, { + inBackground: bgLoad, + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + + this.moveTabTo(tab, newIndex); + } + else { + // We're adding a new tab, but do not want parent-tab tracking. + tab = this.addTab(data.url, { + allowThirdPartyFixup: true, + triggeringPrincipal, + }); + + this.moveTabTo(tab, newIndex); + if (this.mCurrentTab != tab && !bgLoad) + this.selectedTab = tab; + } + }); + ]]> + </body> + </method> + + <method name="_onDragLeave"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + var target = aEvent.relatedTarget; + while (target && target != this.mStrip) + target = target.parentNode; + + if (target) + return; + + document.getAnonymousElementByAttribute(this, "class", + "tab-drop-indicator-bar") + .collapsed = true; + aEvent.stopPropagation(); + ]]> + </body> + </method> + + <method name="moveTabTo"> + <parameter name="aTab"/> + <parameter name="aIndex"/> + <body> + <![CDATA[ + let oldPosition; + // for compatibility with extensions + if (typeof(aTab) == "number") { + oldPosition = aTab; + aTab = this.tabs[oldPosition]; + } else { + oldPosition = this.getTabIndex(aTab); + } + + if (oldPosition == aIndex) + return; + + this.mLastRelatedIndex = 0; + + this.mTabFilters.splice(aIndex, 0, this.mTabFilters.splice(oldPosition, 1)[0]); + this.mTabListeners.splice(aIndex, 0, this.mTabListeners.splice(oldPosition, 1)[0]); + + let wasFocused = (document.activeElement == this.mCurrentTab); + + if (aIndex >= oldPosition) + ++aIndex; + this.mCurrentTab._selected = false; + + // invalidate cache + this._browsers = 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. + var tab = this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex)); + + this.mCurrentTab._selected = true; + + if (wasFocused) + this.mCurrentTab.focus(); + + this.tabContainer._handleTabSelect(false); + + tab.dispatchEvent(new UIEvent("TabMove", + { bubbles: true, cancelable: false, view: window, + detail: oldPosition })); + ]]> + </body> + </method> + + <method name="getDropIndex"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) { + var coord = this.tabs[i].boxObject.screenX + + this.tabs[i].boxObject.width / 2; + if (window.getComputedStyle(this, null).direction == "ltr") { + if (aEvent.screenX < coord) + return i; + } else { + if (aEvent.screenX > coord) + return i; + } + } + + return this.tabs.length; + ]]> + </body> + </method> + + <method name="getDropOnIndex"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + for (var i = 0; i < this.tabs.length; ++i) { + var tabBoxObject = this.tabs[i].boxObject; + if (aEvent.screenX > tabBoxObject.screenX + tabBoxObject.width * .25 && + aEvent.screenX < tabBoxObject.screenX + tabBoxObject.width * .75) + return i; + } + + return -1; + ]]> + </body> + </method> + + <!-- moveTabLeft and moveTabRight methods have been kept for backwards + compatibility for extensions. Internally moveTabOver is used. --> + <method name="moveTabLeft"> + <body> + <![CDATA[ + if (window.getComputedStyle(this, null).direction == "ltr") + this.moveTabBackward(); + else + this.moveTabForward(); + ]]> + </body> + </method> + + <method name="moveTabRight"> + <body> + <![CDATA[ + if (window.getComputedStyle(this, null).direction == "ltr") + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <method name="moveTabForward"> + <body> + <![CDATA[ + var tabPos = this.tabContainer.selectedIndex; + if (tabPos < this.browsers.length - 1) { + this.moveTabTo(this.mCurrentTab, tabPos + 1); + } + else if (this.arrowKeysShouldWrap) + this.moveTabToStart(); + ]]> + </body> + </method> + + <method name="moveTabBackward"> + <body> + <![CDATA[ + var tabPos = this.tabContainer.selectedIndex; + if (tabPos > 0) { + this.moveTabTo(this.mCurrentTab, tabPos - 1); + } + else if (this.arrowKeysShouldWrap) + this.moveTabToEnd(); + ]]> + </body> + </method> + + <method name="moveTabToStart"> + <body> + <![CDATA[ + if (this.tabContainer.selectedIndex > 0) { + this.moveTabTo(this.mCurrentTab, 0); + } + ]]> + </body> + </method> + + <method name="moveTabToEnd"> + <body> + <![CDATA[ + if (this.tabContainer.selectedIndex < 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, null).direction; + var keyCode = aEvent.keyCode; + if ((direction == "ltr" && keyCode == KeyEvent.DOM_VK_RIGHT) || + (direction == "rtl" && keyCode == KeyEvent.DOM_VK_LEFT)) + this.moveTabForward(); + else + this.moveTabBackward(); + ]]> + </body> + </method> + + <!-- BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT + MAKE SURE TO ADD IT HERE AS WELL. --> + <property name="canGoBack" + onget="return this.mCurrentBrowser.canGoBack;" + readonly="true"/> + + <property name="canGoForward" + onget="return this.mCurrentBrowser.canGoForward;" + readonly="true"/> + + <method name="goBack"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goBack(); + ]]> + </body> + </method> + + <method name="goForward"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goForward(); + ]]> + </body> + </method> + + <method name="reload"> + <body> + <![CDATA[ + return this.mCurrentBrowser.reload(); + ]]> + </body> + </method> + + <method name="reloadWithFlags"> + <parameter name="aFlags"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.reloadWithFlags(aFlags); + ]]> + </body> + </method> + + <method name="stop"> + <body> + <![CDATA[ + return this.mCurrentBrowser.stop(); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURI"> + <parameter name="aURI"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset); + ]]> + </body> + </method> + + <!-- throws exception for unknown schemes --> + <method name="loadURIWithFlags"> + <parameter name="aURI"/> + <parameter name="aFlags"/> + <parameter name="aReferrerURI"/> + <parameter name="aCharset"/> + <parameter name="aPostData"/> + <body> + <![CDATA[ + // Note - the callee understands both: + // (a) loadURIWithFlags(aURI, aFlags, ...) + // (b) loadURIWithFlags(aURI, { flags: aFlags, ... }) + // Forwarding it as (a) here actually supports both (a) and (b), + // so you can call us either way too. + return this.mCurrentBrowser.loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData); + ]]> + </body> + </method> + + <method name="goHome"> + <body> + <![CDATA[ + return this.mCurrentBrowser.goHome(); + ]]> + </body> + </method> + + <property name="homePage"> + <getter> + <![CDATA[ + return this.mCurrentBrowser.homePage; + ]]> + </getter> + <setter> + <![CDATA[ + this.mCurrentBrowser.homePage = val; + return val; + ]]> + </setter> + </property> + + <method name="gotoIndex"> + <parameter name="aIndex"/> + <body> + <![CDATA[ + return this.mCurrentBrowser.gotoIndex(aIndex); + ]]> + </body> + </method> + + <property name="currentURI" + onget="return this.mCurrentBrowser.currentURI;" + readonly="true"/> + + <field name="finder"><![CDATA[ + ({ + mTabBrowser: this, + mListeners: new Set(), + get finder() { + return this.mTabBrowser.mCurrentBrowser.finder; + }, + 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(aSearchString, aLinksOnly, aDrawOutline) { + this.finder.fastFind(aSearchString, aLinksOnly, aDrawOutline); + }, + findAgain: function(aFindBackwards, aLinksOnly, aDrawOutline) { + this.finder.findAgain(aFindBackwards, aLinksOnly, aDrawOutline); + }, + 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); + } + }) + ]]></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.contentWindowAsCPOW"/> + + <property name="sessionHistory" + onget="return this.mCurrentBrowser.sessionHistory;" + readonly="true"/> + + <property name="markupDocumentViewer" + onget="return this.mCurrentBrowser.markupDocumentViewer;" + readonly="true"/> + + <property name="contentDocument" + onget="return this.mCurrentBrowser.contentDocument;" + readonly="true"/> + + <property name="contentTitle" + onget="return this.mCurrentBrowser.contentTitle;" + readonly="true"/> + + <property name="securityUI" + onget="return this.mCurrentBrowser.securityUI;" + readonly="true"/> + + <property name="userTypedValue" + onget="return this.mCurrentBrowser.userTypedValue;" + onset="return this.mCurrentBrowser.userTypedValue = val;"/> + + <property name="droppedLinkHandler" + onget="return this.mCurrentBrowser.droppedLinkHandler;" + onset="return this.mCurrentBrowser.droppedLinkHandler = val;"/> + + <property name="showWindowResizer" + onget="return this.mCurrentBrowser.showWindowResizer;" + onset="return this.mCurrentBrowser.showWindowResizer = val;"/> + + <property name="docShellIsActive" + onget="return this.mCurrentBrowser.docShellIsActive;" + onset="return this.mCurrentBrowser.docShellIsActive = val;"/> + + <property name="fullZoom" + onget="return this.mCurrentBrowser.fullZoom;" + onset="return this.mCurrentBrowser.fullZoom = val;"/> + + <property name="textZoom" + onget="return this.mCurrentBrowser.textZoom;" + onset="return this.mCurrentBrowser.textZoom = val;"/> + + <property name="isSyntheticDocument" + onget="return this.mCurrentBrowser.isSyntheticDocument;" + readonly="true"/> + + <property name="messageManager" + onget="return window.messageManager;" + readonly="true"/> + + <method name="observe"> + <parameter name="aSubject"/> + <parameter name="aTopic"/> + <parameter name="aData"/> + <body> + <![CDATA[ + var maxUndoDepth = 0; + switch (aTopic) { + case "browser:purge-session-history": + break; + + case "nsPref:changed": + if (aData == "browser.tabs.max_tabs_undo") { + maxUndoDepth = Math.max(0, Services.prefs.getIntPref(aData)); + break; + } + + default: + return; + } + + // Wipe out savedBrowsers since history is gone + while (this.savedBrowsers.length > maxUndoDepth) { + var tabData = this.savedBrowsers.pop(); + var deadBrowser = tabData.browserData.browser; + delete tabData.browserData; + this.removeBrowser(deadBrowser); + } + ]]> + </body> + </method> + + <field name="_fastFind">null</field> + <property name="fastFind" readonly="true"> + <getter> + <![CDATA[ + if (!this._fastFind) { + this._fastFind = Cc["@mozilla.org/typeaheadfind;1"] + .createInstance(Ci.nsITypeAheadFind); + this._fastFind.init(this.docShell); + } + return this._fastFind; + ]]> + </getter> + </property> + + <field name="_lastSearchString">null</field> + <field name="_lastSearchHighlight">false</field> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body><![CDATA[ + switch (aEvent.type) { + case "keypress": + this._handleKeyEvent(aEvent); + break; + } + ]]></body> + </method> + + <constructor> + <![CDATA[ + document.addEventListener("keypress", this); + this.arrowKeysShouldWrap = AppConstants.platform == "macosx"; + // Bail out early if we are in tabmail. See Bug 521803. + if (!this.mPanelContainer) + return; + + this.mCurrentBrowser = this.mPanelContainer.firstChild.firstChild.firstChild; + this.mCurrentTab = this.tabContainer.firstChild; + + var uniqueId = "panel" + this.nextTabNumber++; + this.mPanelContainer.childNodes[0].id = uniqueId; + this.tabs[0].linkedPanel = uniqueId; + this.tabs[0].linkedBrowser = this.mCurrentBrowser; + + // Ensure the browser's session history and security UI are wired up + // note that toolkit browser automatically inits its security UI + // when you get it but for xpfe you need to init it explicitly + if (!this.mCurrentBrowser.securityUI) + this.mCurrentBrowser.init(); + + // Wire up the tab's progress listener and filter. + var tabListener = this.mTabProgressListener(this.mCurrentTab, + this.mCurrentBrowser, + false); + var filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL); + this.webProgress.addProgressListener(filter, + Ci.nsIWebProgress.NOTIFY_ALL); + this.mTabListeners[0] = tabListener; + this.mTabFilters[0] = filter; + + if (!Services.prefs.getBoolPref("browser.tabs.autoHide") && + !Services.prefs.getBoolPref("browser.tabs.forceHide") && + window.toolbar.visible) + this.mStrip.collapsed = false; + + var t = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "tab"); + t.setAttribute("label", this.mStringBundle.getString("tabs.untitled")); + t.setAttribute("crop", "end"); + t.className = "tabbrowser-tab"; + t.style.maxWidth = Services.prefs.getIntPref("browser.tabs.tabMaxWidth") + "px"; + t.style.minWidth = Services.prefs.getIntPref("browser.tabs.tabMinWidth") + "px"; + t.width = 0; + t.flex = 100; + t.setAttribute("validate", "never"); + t.setAttribute("onerror", "this.parentNode.parentNode.parentNode.parentNode.addToMissedIconCache(this.getAttribute('image')); this.removeAttribute('image');"); + this.referenceTab = t; + + Services.obs.addObserver(this, "browser:purge-session-history"); + Services.prefs.addObserver("browser.tabs.max_tabs_undo", this); + + var onclick = this.getAttribute("oncontentclick"); + if (onclick) + this.onContentClick = new Function("event", onclick); + ]]> + </constructor> + + <destructor> + <![CDATA[ + document.removeEventListener("keypress", this); + // Bail out early if we are in tabmail. See Bug 521803. + if (!this.mPanelContainer) + return; + + for (var i = 0; i < this.mTabListeners.length; ++i) { + this.browsers[i].webProgress.removeProgressListener(this.mTabFilters[i]); + this.mTabFilters[i].removeProgressListener(this.mTabListeners[i]); + this.mTabFilters[i] = null; + this.mTabListeners[i] = null; + } + Services.obs.removeObserver(this, "browser:purge-session-history"); + Services.prefs.removeObserver("browser.tabs.max_tabs_undo", this); + this.savedBrowsers.forEach(function(aTabData) { + delete aTabData.browserData; + }); + ]]> + </destructor> + + <!-- Deprecated stuff, implemented for backwards compatibility. --> + <property name="mTabContainer" readonly="true" + onget="return this.tabContainer;"/> + <property name="mTabs" readonly="true" + onget="return this.tabs;"/> + </implementation> + + <handlers> + <handler event="select" action="if (event.originalTarget == this.mPanelContainer) this.updateCurrentBrowser();"/> + + <handler event="DOMLinkAdded" phase="capturing" action="this.onLinkEvent(event);"/> + <handler event="DOMLinkChanged" phase="capturing" action="this.onLinkEvent(event);"/> + + <handler event="DOMWindowClose" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + if (this.tabs.length == 1) + return; + + this.removeTab(this._getTabForContentWindow(event.target)); + event.preventDefault(); + ]]> + </handler> + + <handler event="DOMWebNotificationClicked" phase="capturing"> + <![CDATA[ + if (!event.isTrusted) + return; + + // The user clicked a desktop notification; make sure its + // tab is brought to the front and then raise the window. + this.selectedTab = this._getTabForContentWindow(event.target.top); + window.focus(); + ]]> + </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; + + this.setTabTitle(tab); + if (tab == this.mCurrentTab) + this.updateTitlebar(); + ]]> + </handler> + + <handler event="click" phase="capturing" group="system"> + <![CDATA[ + if (this.onContentClick) + this.onContentClick(event); + ]]> + </handler> + + </handlers> + </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.from(document.getBindingParent(this).childNodes) + .filter(this._canScrollToElement, + this); + ]]> + </body> + </method> + <method name="_canScrollToElement"> + <parameter name="aTab"/> + <body> + <![CDATA[ + return !aTab.pinned && !aTab.hidden; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="underflow"> + <![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.removeAttribute("overflow"); + ]]> + </handler> + <handler event="overflow"> + <![CDATA[ + if (event.detail == 0) + return; // Ignore vertical events + + var tabs = document.getBindingParent(this); + tabs.setAttribute("overflow", true); + tabs._handleTabSelect(false); + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-tabs" + extends="chrome://global/content/bindings/tabbox.xml#tabs"> + <content> + <xul:stack flex="1" class="tabs-stack"> + <xul:vbox> + <xul:spacer flex="1"/> + <xul:hbox class="tabs-bottom" align="center"/> + </xul:vbox> + <xul:vbox> + <xul:hbox> + <xul:stack> + <xul:spacer class="tabs-left"/> + <xul:toolbarbutton class="tabs-newbutton" context="" + anonid="tabstrip-newbutton" + xbl:inherits="oncommand=onnewtab,onclick=onnewtabclick,tooltiptext=tooltiptextnew"/> + </xul:stack> + <xul:arrowscrollbox anonid="arrowscrollbox" + class="tabbrowser-arrowscrollbox" + flex="1" + xbl:inherits="smoothscroll" + orient="horizontal" + style="min-width: 1px;"> + <children includes="tab"/> + <xul:spacer class="tabs-right" flex="1"/> + </xul:arrowscrollbox> + <children/> + <xul:stack> + <xul:spacer class="tabs-right"/> + <xul:hbox class="tabs-closebutton-box" align="stretch" pack="end"> + <xul:toolbarbutton class="tabs-alltabs-button" context="" + anonid="alltabs-button" + type="menu" + xbl:inherits="tooltiptext=tooltiptextalltabs"> + <xul:menupopup class="tabs-alltabs-popup" + anonid="alltabs-popup" + position="after_end"/> + </xul:toolbarbutton> + <xul:hbox align="center"> + <xul:toolbarbutton class="tabs-closebutton close-button" context="" + anonid="tabstrip-closebutton" + xbl:inherits="disabled=disableclose,oncommand=onclosetab,tooltiptext=tooltiptextclose"/> + </xul:hbox> + </xul:hbox> + </xul:stack> + </xul:hbox> + <xul:spacer class="tabs-bottom-spacer"/> + </xul:vbox> + </xul:stack> + </content> + + <implementation implements="nsIDOMEventListener"> + <constructor> + <![CDATA[ + var tab = this.firstChild; + // set the tabstrip's minWidth too, otherwise it immediately overflows + this.arrowScrollbox.style.minWidth = + tab.style.minWidth = Services.prefs.getIntPref("browser.tabs.tabMinWidth") + "px"; + tab.style.maxWidth = Services.prefs.getIntPref("browser.tabs.tabMaxWidth") + "px"; + window.addEventListener("resize", this); + ]]> + </constructor> + + <destructor> + <![CDATA[ + window.removeEventListener("resize", this); + ]]> + </destructor> + + <field name="arrowScrollboxWidth">0</field> + + <field name="arrowScrollbox"> + document.getAnonymousElementByAttribute(this, "anonid", "arrowscrollbox"); + </field> + + <method name="_handleTabSelect"> + <parameter name="aSmoothScroll"/> + <body> + <![CDATA[ + if (this.getAttribute("overflow") == "true") + this.arrowScrollbox.ensureElementIsVisible(this.selectedItem, + aSmoothScroll); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + switch (aEvent.type) + { + case "resize": + if (aEvent.target != window) + break; + var width = this.arrowScrollbox.boxObject.width; + if (width != this.arrowScrollboxWidth) + { + this._handleTabSelect(false); + this.arrowScrollboxWidth = width; + } + break; + } + ]]> + </body> + </method> + + <field name="mAllTabsPopup"> + document.getAnonymousElementByAttribute(this, "anonid", "alltabs-popup"); + </field> + + <field name="_animateElement"> + this.arrowScrollbox._scrollButtonDown; + </field> + + <method name="_notifyBackgroundTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + var scrollRect = this.arrowScrollbox.scrollClientRect; + var tab = aTab.getBoundingClientRect(); + + // Is the new tab already completely visible? + if (scrollRect.left <= tab.left && tab.right <= scrollRect.right) + return; + + if (this.arrowScrollbox.smoothScroll) { + let selected = 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.arrowScrollbox.ensureElementIsVisible(aTab); + return; + } + + this.arrowScrollbox.scrollByPixels(this.arrowScrollbox._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="_handleNewTab"> + <parameter name="aTab"/> + <body> + <![CDATA[ + if (aTab.parentNode != this) + return; + + if (aTab.getAttribute("selected") == "true") { + this._handleTabSelect(); + } else { + this._notifyBackgroundTab(aTab); + } + + /* 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.arrowScrollbox._updateScrollButtonsDisabledState(); + ]]> + </body> + </method> + + <method name="_handleMouseScroll"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + // Javascript does not have a logical XOR operator. + if (aEvent.shiftKey != Services.prefs.getBoolPref("browser.tabs.mouseScrollAdvancesTab")) { + this.advanceSelectedTab(aEvent.detail < 0 ? -1 : 1); + aEvent.stopPropagation(); + } + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="TabSelect" action="this._handleTabSelect();"/> + + <handler event="transitionend"> + <![CDATA[ + if (event.propertyName == "max-width") + this._handleNewTab(event.target); + ]]> + </handler> + + <handler event="DOMMouseScroll" phase="capturing"> + <![CDATA[ + this._handleMouseScroll(event); + ]]> + </handler> + </handlers> + </binding> + + <binding id="tabbrowser-alltabs-popup" + extends="chrome://global/content/bindings/popup.xml#popup"> + <implementation implements="nsIDOMEventListener"> + <method name="_tabOnTabClose"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + let menuItem = aEvent.target.mCorrespondingMenuitem; + if (menuItem) + menuItem.remove(); + ]]> + </body> + </method> + + <method name="handleEvent"> + <parameter name="aEvent"/> + <body> + <![CDATA[ + switch (aEvent.type) + { + case "TabClose": + this._tabOnTabClose(aEvent); + break; + case "TabOpen": + this._createTabMenuItem(aEvent.originalTarget); + break; + case "scroll": + this._updateTabsVisibilityStatus(); + break; + } + ]]> + </body> + </method> + + <method name="_updateTabsVisibilityStatus"> + <body> + <![CDATA[ + let tabContainer = document.getBindingParent(this); + let tabstripBO = tabContainer.arrowScrollbox.scrollBoxObject; + + for (let i = 0; i < this.childNodes.length; i++) + { + let curTabBO = this.childNodes[i].tab.boxObject; + if (curTabBO.screenX >= tabstripBO.screenX && + curTabBO.screenX + curTabBO.width <= tabstripBO.screenX + tabstripBO.width) + this.childNodes[i].removeAttribute("tabIsScrolled"); + else + this.childNodes[i].setAttribute("tabIsScrolled", "true"); + } + ]]> + </body> + </method> + + <method name="_createTabMenuItem"> + <parameter name="aTabNode"/> + <body> + <![CDATA[ + let menuItem = document.createElementNS( + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", + "menuitem"); + menuItem.setAttribute("class", "menuitem-iconic alltabs-item icon-holder"); + menuItem.setAttribute("label", aTabNode.label); + menuItem.setAttribute("crop", aTabNode.getAttribute("crop")); + + ["busy", "image", "selected"].forEach( + function(attribute) + { + if (aTabNode.hasAttribute(attribute)) + { + menuItem.setAttribute(attribute, aTabNode.getAttribute(attribute)); + } + } + ); + + // Keep some attributes of the menuitem in sync with its + // corresponding tab (e.g. the tab label) + aTabNode.mCorrespondingMenuitem = menuItem; + document.addBroadcastListenerFor(aTabNode, menuItem, "label"); + document.addBroadcastListenerFor(aTabNode, menuItem, "crop"); + document.addBroadcastListenerFor(aTabNode, menuItem, "image"); + document.addBroadcastListenerFor(aTabNode, menuItem, "busy"); + document.addBroadcastListenerFor(aTabNode, menuItem, "selected"); + aTabNode.addEventListener("TabClose", this); + menuItem.tab = aTabNode; + this.appendChild(menuItem); + return menuItem; + ]]> + </body> + </method> + </implementation> + + <handlers> + <handler event="popupshowing"> + <![CDATA[ + // set up the menu popup + let tabcontainer = document.getBindingParent(this); + let tabs = tabcontainer.childNodes; + + // Listen for changes in the tab bar. + let tabbrowser = document.getBindingParent(tabcontainer); + tabbrowser.addEventListener("TabOpen", this); + tabcontainer.arrowScrollbox.addEventListener("scroll", this); + + for (let i = 0; i < tabs.length; i++) + this._createTabMenuItem(tabs[i]); + this._updateTabsVisibilityStatus(); + ]]> + </handler> + + <handler event="popuphiding"> + <![CDATA[ + // clear out the menu popup and remove the listeners + while (this.hasChildNodes()) + { + let menuItem = this.lastChild; + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "label"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "crop"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "image"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "busy"); + document.removeBroadcastListenerFor(menuItem.tab, menuItem, "selected"); + menuItem.tab.removeEventListener("TabClose", this); + menuItem.tab.mCorrespondingMenuitem = null; + menuItem.tab = null; + menuItem.remove(); + } + let tabcontainer = document.getBindingParent(this); + tabcontainer.arrowScrollbox.removeEventListener("scroll", this); + document.getBindingParent(tabcontainer).removeEventListener("TabOpen", this); + ]]> + </handler> + + <handler event="command"> + <![CDATA[ + let tabcontainer = document.getBindingParent(this); + let tab = event.target.tab; + if (tabcontainer.selectedItem == tab) + tabcontainer._handleTabSelect(); + else + tabcontainer.selectedItem = tab; + ]]> + </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> + + </handlers> + </binding> +</bindings> |