2018-02-27 18:04:14 +00:00
|
|
|
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
|
|
|
* 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/. */
|
|
|
|
|
|
|
|
/* eslint-env mozilla/browser-window */
|
|
|
|
|
|
|
|
class TabBrowser {
|
|
|
|
constructor(container) {
|
|
|
|
this.container = container;
|
|
|
|
this.requiresAddonInterpositions = true;
|
|
|
|
|
|
|
|
// Pass along any used DOM methods to the container node. When this object turns
|
|
|
|
// into a custom element this won't be needed anymore.
|
|
|
|
this.addEventListener = this.container.addEventListener.bind(this.container);
|
|
|
|
this.removeEventListener = this.container.removeEventListener.bind(this.container);
|
|
|
|
this.dispatchEvent = this.container.dispatchEvent.bind(this.container);
|
|
|
|
this.getAttribute = this.container.getAttribute.bind(this.container);
|
|
|
|
this.hasAttribute = this.container.hasAttribute.bind(this.container);
|
|
|
|
this.setAttribute = this.container.setAttribute.bind(this.container);
|
|
|
|
this.removeAttribute = this.container.removeAttribute.bind(this.container);
|
|
|
|
this.appendChild = this.container.appendChild.bind(this.container);
|
|
|
|
this.ownerGlobal = this.container.ownerGlobal;
|
|
|
|
this.ownerDocument = this.container.ownerDocument;
|
|
|
|
this.namespaceURI = this.container.namespaceURI;
|
|
|
|
this.style = this.container.style;
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(this, {
|
|
|
|
_unifiedComplete: ["@mozilla.org/autocomplete/search;1?name=unifiedcomplete", "mozIPlacesAutoComplete"],
|
|
|
|
serializationHelper: ["@mozilla.org/network/serialization-helper;1", "nsISerializationHelper"],
|
|
|
|
mURIFixup: ["@mozilla.org/docshell/urifixup;1", "nsIURIFixup"],
|
|
|
|
});
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "initialBrowser", () => {
|
|
|
|
return document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
|
|
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "tabContainer", () => {
|
|
|
|
return document.getElementById(this.getAttribute("tabcontainer"));
|
|
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "tabs", () => {
|
|
|
|
return this.tabContainer.childNodes;
|
|
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "tabbox", () => {
|
|
|
|
return document.getAnonymousElementByAttribute(this.container, "anonid", "tabbox");
|
|
|
|
});
|
|
|
|
XPCOMUtils.defineLazyGetter(this, "mPanelContainer", () => {
|
|
|
|
return document.getAnonymousElementByAttribute(this.container, "anonid", "panelcontainer");
|
|
|
|
});
|
|
|
|
|
|
|
|
this.closingTabsEnum = { ALL: 0, OTHER: 1, TO_END: 2 };
|
|
|
|
|
|
|
|
this._visibleTabs = null;
|
|
|
|
|
|
|
|
this.mCurrentTab = null;
|
|
|
|
|
|
|
|
this._lastRelatedTabMap = new WeakMap();
|
|
|
|
|
|
|
|
this.mCurrentBrowser = null;
|
|
|
|
|
|
|
|
this.mProgressListeners = [];
|
|
|
|
|
|
|
|
this.mTabsProgressListeners = [];
|
|
|
|
|
|
|
|
this._tabListeners = new Map();
|
|
|
|
|
|
|
|
this._tabFilters = new Map();
|
|
|
|
|
|
|
|
this.mIsBusy = false;
|
|
|
|
|
|
|
|
this._outerWindowIDBrowserMap = new Map();
|
|
|
|
|
|
|
|
this.arrowKeysShouldWrap = AppConstants == "macosx";
|
|
|
|
|
|
|
|
this._autoScrollPopup = null;
|
|
|
|
|
|
|
|
this._previewMode = false;
|
|
|
|
|
|
|
|
this._lastFindValue = "";
|
|
|
|
|
|
|
|
this._contentWaitingCount = 0;
|
|
|
|
|
|
|
|
this.tabAnimationsInProgress = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binding from browser to tab
|
|
|
|
*/
|
|
|
|
this._tabForBrowser = new WeakMap();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Holds a unique ID for the tab change that's currently being timed.
|
|
|
|
* Used to make sure that multiple, rapid tab switches do not try to
|
|
|
|
* create overlapping timers.
|
|
|
|
*/
|
|
|
|
this._tabSwitchID = null;
|
|
|
|
|
|
|
|
this._preloadedBrowser = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* `_createLazyBrowser` will define properties on the unbound lazy browser
|
|
|
|
* which correspond to properties defined in XBL which will be bound to
|
|
|
|
* the browser when it is inserted into the document. If any of these
|
|
|
|
* properties are accessed by consumers, `_insertBrowser` is called and
|
|
|
|
* the browser is inserted to ensure that things don't break. This list
|
|
|
|
* provides the names of properties that may be called while the browser
|
|
|
|
* is in its unbound (lazy) state.
|
|
|
|
*/
|
|
|
|
this._browserBindingProperties = [
|
|
|
|
"canGoBack", "canGoForward", "goBack", "goForward", "permitUnload",
|
|
|
|
"reload", "reloadWithFlags", "stop", "loadURI", "loadURIWithFlags",
|
|
|
|
"goHome", "homePage", "gotoIndex", "currentURI", "documentURI",
|
|
|
|
"preferences", "imageDocument", "isRemoteBrowser", "messageManager",
|
|
|
|
"getTabBrowser", "finder", "fastFind", "sessionHistory", "contentTitle",
|
|
|
|
"characterSet", "fullZoom", "textZoom", "webProgress",
|
|
|
|
"addProgressListener", "removeProgressListener", "audioPlaybackStarted",
|
|
|
|
"audioPlaybackStopped", "pauseMedia", "stopMedia",
|
|
|
|
"resumeMedia", "mute", "unmute", "blockedPopups", "lastURI",
|
|
|
|
"purgeSessionHistory", "stopScroll", "startScroll",
|
|
|
|
"userTypedValue", "userTypedClear", "mediaBlocked",
|
|
|
|
"didStartLoadSinceLastUserTyping"
|
|
|
|
];
|
|
|
|
|
|
|
|
this._removingTabs = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Tab close requests are ignored if the window is closing anyway,
|
|
|
|
* e.g. when holding Ctrl+W.
|
|
|
|
*/
|
|
|
|
this._windowIsClosing = false;
|
|
|
|
|
|
|
|
// This defines a proxy which allows us to access browsers by
|
|
|
|
// index without actually creating a full array of browsers.
|
|
|
|
this.browsers = new Proxy([], {
|
|
|
|
has: (target, name) => {
|
|
|
|
if (typeof name == "string" && Number.isInteger(parseInt(name))) {
|
|
|
|
return (name in this.tabs);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
get: (target, name) => {
|
|
|
|
if (name == "length") {
|
|
|
|
return this.tabs.length;
|
|
|
|
}
|
|
|
|
if (typeof name == "string" && Number.isInteger(parseInt(name))) {
|
|
|
|
if (!(name in this.tabs)) {
|
|
|
|
return undefined;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
return this.tabs[name].linkedBrowser;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
return target[name];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List of browsers whose docshells must be active in order for print preview
|
|
|
|
* to work.
|
|
|
|
*/
|
|
|
|
this._printPreviewBrowsers = new Set();
|
|
|
|
|
|
|
|
this._switcher = null;
|
|
|
|
|
|
|
|
this._tabMinWidthLimit = 50;
|
|
|
|
|
|
|
|
this._soundPlayingAttrRemovalTimer = 0;
|
|
|
|
|
|
|
|
this._hoverTabTimer = null;
|
|
|
|
|
|
|
|
this.mCurrentBrowser = document.getAnonymousElementByAttribute(this.container, "anonid", "initialBrowser");
|
|
|
|
this.mCurrentBrowser.permanentKey = {};
|
|
|
|
|
|
|
|
CustomizableUI.addListener(this);
|
|
|
|
this._updateNewTabVisibility();
|
|
|
|
|
|
|
|
Services.obs.addObserver(this, "contextual-identity-updated");
|
|
|
|
|
|
|
|
this.mCurrentTab = this.tabContainer.firstChild;
|
|
|
|
const nsIEventListenerService =
|
|
|
|
Components.interfaces.nsIEventListenerService;
|
|
|
|
let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
|
|
|
|
.getService(nsIEventListenerService);
|
|
|
|
els.addSystemEventListener(document, "keydown", this, false);
|
|
|
|
if (AppConstants.platform == "macosx") {
|
|
|
|
els.addSystemEventListener(document, "keypress", this, false);
|
|
|
|
}
|
|
|
|
window.addEventListener("sizemodechange", this);
|
|
|
|
window.addEventListener("occlusionstatechange", this);
|
|
|
|
|
|
|
|
var uniqueId = this._generateUniquePanelID();
|
|
|
|
this.mPanelContainer.childNodes[0].id = uniqueId;
|
|
|
|
this.mCurrentTab.linkedPanel = uniqueId;
|
|
|
|
this.mCurrentTab.permanentKey = this.mCurrentBrowser.permanentKey;
|
|
|
|
this.mCurrentTab._tPos = 0;
|
|
|
|
this.mCurrentTab._fullyOpen = true;
|
|
|
|
this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
|
|
|
|
this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
|
|
|
|
|
|
|
|
// set up the shared autoscroll popup
|
|
|
|
this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
|
|
|
|
this._autoScrollPopup.id = "autoscroller";
|
|
|
|
this.appendChild(this._autoScrollPopup);
|
|
|
|
this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
|
|
|
|
this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
|
|
|
|
|
|
|
|
// Hook up the event listeners to the first browser
|
|
|
|
var tabListener = this.mTabProgressListener(this.mCurrentTab, this.mCurrentBrowser, true, false);
|
|
|
|
const nsIWebProgress = Components.interfaces.nsIWebProgress;
|
|
|
|
const filter = Components.classes["@mozilla.org/appshell/component/browser-status-filter;1"]
|
|
|
|
.createInstance(nsIWebProgress);
|
|
|
|
filter.addProgressListener(tabListener, nsIWebProgress.NOTIFY_ALL);
|
|
|
|
this._tabListeners.set(this.mCurrentTab, tabListener);
|
|
|
|
this._tabFilters.set(this.mCurrentTab, filter);
|
|
|
|
this.webProgress.addProgressListener(filter, nsIWebProgress.NOTIFY_ALL);
|
|
|
|
|
|
|
|
if (Services.prefs.getBoolPref("browser.display.use_system_colors"))
|
|
|
|
this.style.backgroundColor = "-moz-default-background-color";
|
|
|
|
else if (Services.prefs.getIntPref("browser.display.document_color_use") == 2)
|
|
|
|
this.style.backgroundColor = Services.prefs.getCharPref("browser.display.background_color");
|
|
|
|
|
|
|
|
let messageManager = window.getGroupMessageManager("browsers");
|
|
|
|
|
|
|
|
let remote = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
|
|
.QueryInterface(Ci.nsILoadContext)
|
|
|
|
.useRemoteTabs;
|
|
|
|
if (remote) {
|
|
|
|
messageManager.addMessageListener("DOMTitleChanged", this);
|
|
|
|
messageManager.addMessageListener("DOMWindowClose", this);
|
|
|
|
window.messageManager.addMessageListener("contextmenu", this);
|
|
|
|
messageManager.addMessageListener("Browser:Init", this);
|
|
|
|
|
|
|
|
// If this window has remote tabs, switch to our tabpanels fork
|
|
|
|
// which does asynchronous tab switching.
|
|
|
|
this.mPanelContainer.classList.add("tabbrowser-tabpanels");
|
|
|
|
} else {
|
|
|
|
this._outerWindowIDBrowserMap.set(this.mCurrentBrowser.outerWindowID,
|
|
|
|
this.mCurrentBrowser);
|
|
|
|
}
|
|
|
|
messageManager.addMessageListener("DOMWindowFocus", this);
|
|
|
|
messageManager.addMessageListener("RefreshBlocker:Blocked", this);
|
|
|
|
messageManager.addMessageListener("Browser:WindowCreated", this);
|
|
|
|
|
|
|
|
// To correctly handle keypresses for potential FindAsYouType, while
|
|
|
|
// the tab's find bar is not yet initialized.
|
|
|
|
this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
|
|
|
|
Services.prefs.addObserver("accessibility.typeaheadfind", this);
|
|
|
|
messageManager.addMessageListener("Findbar:Keypress", this);
|
|
|
|
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "animationsEnabled",
|
|
|
|
"toolkit.cosmeticAnimations.enabled", true);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "schedulePressureDefaultCount",
|
|
|
|
"browser.schedulePressure.defaultCount", 3);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingEnabled",
|
|
|
|
"browser.tabs.remote.warmup.enabled", false);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingMax",
|
|
|
|
"browser.tabs.remote.warmup.maxTabs", 3);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "tabWarmingUnloadDelay" /* ms */,
|
|
|
|
"browser.tabs.remote.warmup.unloadDelayMs", 2000);
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(this, "tabMinWidthPref",
|
|
|
|
"browser.tabs.tabMinWidth", this._tabMinWidthLimit,
|
|
|
|
(pref, prevValue, newValue) => this.tabMinWidth = newValue,
|
|
|
|
newValue => Math.max(newValue, this._tabMinWidthLimit),
|
|
|
|
);
|
|
|
|
|
|
|
|
this.tabMinWidth = this.tabMinWidthPref;
|
|
|
|
|
|
|
|
this._setupEventListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
get tabContextMenu() {
|
|
|
|
return this.tabContainer.contextMenu;
|
|
|
|
}
|
|
|
|
|
|
|
|
get visibleTabs() {
|
|
|
|
if (!this._visibleTabs)
|
|
|
|
this._visibleTabs = Array.filter(this.tabs,
|
|
|
|
tab => !tab.hidden && !tab.closing);
|
|
|
|
return this._visibleTabs;
|
|
|
|
}
|
|
|
|
|
|
|
|
get _numPinnedTabs() {
|
|
|
|
for (var i = 0; i < this.tabs.length; i++) {
|
|
|
|
if (!this.tabs[i].pinned)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
get popupAnchor() {
|
|
|
|
if (this.mCurrentTab._popupAnchor) {
|
|
|
|
return this.mCurrentTab._popupAnchor;
|
|
|
|
}
|
|
|
|
let stack = this.mCurrentBrowser.parentNode;
|
|
|
|
// Create an anchor for the popup
|
|
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
let popupAnchor = document.createElementNS(NS_XUL, "hbox");
|
|
|
|
popupAnchor.className = "popup-anchor";
|
|
|
|
popupAnchor.hidden = true;
|
|
|
|
stack.appendChild(popupAnchor);
|
|
|
|
return this.mCurrentTab._popupAnchor = popupAnchor;
|
|
|
|
}
|
|
|
|
|
|
|
|
set selectedTab(val) {
|
|
|
|
if (gNavToolbox.collapsed && !this._allowTabChange) {
|
|
|
|
return this.tabbox.selectedTab;
|
|
|
|
}
|
|
|
|
// Update the tab
|
|
|
|
this.tabbox.selectedTab = val;
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get selectedTab() {
|
|
|
|
return this.mCurrentTab;
|
|
|
|
}
|
|
|
|
|
|
|
|
get selectedBrowser() {
|
|
|
|
return this.mCurrentBrowser;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* BEGIN FORWARDED BROWSER PROPERTIES. IF YOU ADD A PROPERTY TO THE BROWSER ELEMENT
|
|
|
|
* MAKE SURE TO ADD IT HERE AS WELL.
|
|
|
|
*/
|
|
|
|
get canGoBack() {
|
|
|
|
return this.mCurrentBrowser.canGoBack;
|
|
|
|
}
|
|
|
|
|
|
|
|
get canGoForward() {
|
|
|
|
return this.mCurrentBrowser.canGoForward;
|
|
|
|
}
|
|
|
|
|
|
|
|
goBack() {
|
|
|
|
return this.mCurrentBrowser.goBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
goForward() {
|
|
|
|
return this.mCurrentBrowser.goForward();
|
|
|
|
}
|
|
|
|
|
|
|
|
reload() {
|
|
|
|
return this.mCurrentBrowser.reload();
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadWithFlags(aFlags) {
|
|
|
|
return this.mCurrentBrowser.reloadWithFlags(aFlags);
|
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
return this.mCurrentBrowser.stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* throws exception for unknown schemes
|
|
|
|
*/
|
|
|
|
loadURI(aURI, aReferrerURI, aCharset) {
|
|
|
|
return this.mCurrentBrowser.loadURI(aURI, aReferrerURI, aCharset);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* throws exception for unknown schemes
|
|
|
|
*/
|
|
|
|
loadURIWithFlags(aURI, aFlags, aReferrerURI, aCharset, aPostData) {
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
goHome() {
|
|
|
|
return this.mCurrentBrowser.goHome();
|
|
|
|
}
|
|
|
|
|
|
|
|
gotoIndex(aIndex) {
|
|
|
|
return this.mCurrentBrowser.gotoIndex(aIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
set homePage(val) {
|
|
|
|
this.mCurrentBrowser.homePage = val;
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get homePage() {
|
|
|
|
return this.mCurrentBrowser.homePage;
|
|
|
|
}
|
|
|
|
|
|
|
|
get currentURI() {
|
|
|
|
return this.mCurrentBrowser.currentURI;
|
|
|
|
}
|
|
|
|
|
|
|
|
get finder() {
|
|
|
|
return this.mCurrentBrowser.finder;
|
|
|
|
}
|
|
|
|
|
|
|
|
get docShell() {
|
|
|
|
return this.mCurrentBrowser.docShell;
|
|
|
|
}
|
|
|
|
|
|
|
|
get webNavigation() {
|
|
|
|
return this.mCurrentBrowser.webNavigation;
|
|
|
|
}
|
|
|
|
|
|
|
|
get webBrowserFind() {
|
|
|
|
return this.mCurrentBrowser.webBrowserFind;
|
|
|
|
}
|
|
|
|
|
|
|
|
get webProgress() {
|
|
|
|
return this.mCurrentBrowser.webProgress;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentWindow() {
|
|
|
|
return this.mCurrentBrowser.contentWindow;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentWindowAsCPOW() {
|
|
|
|
return this.mCurrentBrowser.contentWindowAsCPOW;
|
|
|
|
}
|
|
|
|
|
|
|
|
get sessionHistory() {
|
|
|
|
return this.mCurrentBrowser.sessionHistory;
|
|
|
|
}
|
|
|
|
|
|
|
|
get markupDocumentViewer() {
|
|
|
|
return this.mCurrentBrowser.markupDocumentViewer;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentDocument() {
|
|
|
|
return this.mCurrentBrowser.contentDocument;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentDocumentAsCPOW() {
|
|
|
|
return this.mCurrentBrowser.contentDocumentAsCPOW;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentTitle() {
|
|
|
|
return this.mCurrentBrowser.contentTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
get contentPrincipal() {
|
|
|
|
return this.mCurrentBrowser.contentPrincipal;
|
|
|
|
}
|
|
|
|
|
|
|
|
get securityUI() {
|
|
|
|
return this.mCurrentBrowser.securityUI;
|
|
|
|
}
|
|
|
|
|
|
|
|
set fullZoom(val) {
|
|
|
|
this.mCurrentBrowser.fullZoom = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get fullZoom() {
|
|
|
|
return this.mCurrentBrowser.fullZoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
set textZoom(val) {
|
|
|
|
this.mCurrentBrowser.textZoom = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get textZoom() {
|
|
|
|
return this.mCurrentBrowser.textZoom;
|
|
|
|
}
|
|
|
|
|
|
|
|
get isSyntheticDocument() {
|
|
|
|
return this.mCurrentBrowser.isSyntheticDocument;
|
|
|
|
}
|
|
|
|
|
|
|
|
set userTypedValue(val) {
|
|
|
|
return this.mCurrentBrowser.userTypedValue = val;
|
|
|
|
}
|
|
|
|
|
|
|
|
get userTypedValue() {
|
|
|
|
return this.mCurrentBrowser.userTypedValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
set tabMinWidth(val) {
|
|
|
|
this.tabContainer.style.setProperty("--tab-min-width", val + "px");
|
|
|
|
return val;
|
|
|
|
}
|
|
|
|
|
|
|
|
isFindBarInitialized(aTab) {
|
|
|
|
return (aTab || this.selectedTab)._findBar != undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
getFindBar(aTab) {
|
|
|
|
if (!aTab)
|
|
|
|
aTab = this.selectedTab;
|
|
|
|
|
|
|
|
if (aTab._findBar)
|
|
|
|
return aTab._findBar;
|
|
|
|
|
|
|
|
let findBar = document.createElementNS(this.namespaceURI, "findbar");
|
|
|
|
let browser = this.getBrowserForTab(aTab);
|
|
|
|
let browserContainer = this.getBrowserContainer(browser);
|
|
|
|
browserContainer.appendChild(findBar);
|
|
|
|
|
|
|
|
// Force a style flush to ensure that our binding is attached.
|
|
|
|
findBar.clientTop;
|
|
|
|
|
|
|
|
findBar.browser = browser;
|
|
|
|
findBar._findField.value = this._lastFindValue;
|
|
|
|
|
|
|
|
aTab._findBar = findBar;
|
|
|
|
|
|
|
|
let event = document.createEvent("Events");
|
|
|
|
event.initEvent("TabFindInitialized", true, false);
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
|
|
|
|
return findBar;
|
|
|
|
}
|
|
|
|
|
|
|
|
getStatusPanel() {
|
|
|
|
if (!this._statusPanel) {
|
|
|
|
this._statusPanel = document.createElementNS(this.namespaceURI, "statuspanel");
|
|
|
|
this._statusPanel.setAttribute("inactive", "true");
|
|
|
|
this._statusPanel.setAttribute("layer", "true");
|
|
|
|
this._appendStatusPanel();
|
|
|
|
}
|
|
|
|
return this._statusPanel;
|
|
|
|
}
|
|
|
|
|
|
|
|
_appendStatusPanel() {
|
|
|
|
if (this._statusPanel) {
|
|
|
|
let browser = this.selectedBrowser;
|
|
|
|
let browserContainer = this.getBrowserContainer(browser);
|
|
|
|
browserContainer.insertBefore(this._statusPanel, browser.parentNode.nextSibling);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pinTab(aTab) {
|
|
|
|
if (aTab.pinned)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (aTab.hidden)
|
|
|
|
this.showTab(aTab);
|
|
|
|
|
|
|
|
this.moveTabTo(aTab, this._numPinnedTabs);
|
|
|
|
aTab.setAttribute("pinned", "true");
|
|
|
|
this.tabContainer._unlockTabSizing();
|
|
|
|
this.tabContainer._positionPinnedTabs();
|
|
|
|
this.tabContainer._updateCloseButtons();
|
|
|
|
|
|
|
|
this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: true });
|
|
|
|
|
|
|
|
let event = document.createEvent("Events");
|
|
|
|
event.initEvent("TabPinned", true, false);
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
unpinTab(aTab) {
|
|
|
|
if (!aTab.pinned)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.moveTabTo(aTab, this._numPinnedTabs - 1);
|
|
|
|
aTab.removeAttribute("pinned");
|
|
|
|
aTab.style.marginInlineStart = "";
|
|
|
|
this.tabContainer._unlockTabSizing();
|
|
|
|
this.tabContainer._positionPinnedTabs();
|
|
|
|
this.tabContainer._updateCloseButtons();
|
|
|
|
|
|
|
|
this.getBrowserForTab(aTab).messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: false });
|
|
|
|
|
|
|
|
let event = document.createEvent("Events");
|
|
|
|
event.initEvent("TabUnpinned", true, false);
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
previewTab(aTab, aCallback) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
syncThrobberAnimations(aTab) {
|
|
|
|
aTab.ownerGlobal.promiseDocumentFlushed(() => {
|
|
|
|
if (!aTab.parentNode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const animations =
|
|
|
|
Array.from(aTab.parentNode.getElementsByTagName("tab"))
|
|
|
|
.map(tab => {
|
|
|
|
const throbber =
|
|
|
|
document.getAnonymousElementByAttribute(tab, "anonid", "tab-throbber");
|
|
|
|
return throbber ? throbber.getAnimations({ subtree: true }) : [];
|
|
|
|
})
|
|
|
|
.reduce((a, b) => a.concat(b))
|
|
|
|
.filter(anim =>
|
|
|
|
anim instanceof CSSAnimation &&
|
|
|
|
(anim.animationName === "tab-throbber-animation" ||
|
|
|
|
anim.animationName === "tab-throbber-animation-rtl") &&
|
|
|
|
(anim.playState === "running" || anim.playState === "pending"));
|
|
|
|
|
|
|
|
// Synchronize with the oldest running animation, if any.
|
|
|
|
const firstStartTime = Math.min(
|
|
|
|
...animations.map(anim => anim.startTime === null ? Infinity : anim.startTime)
|
|
|
|
);
|
|
|
|
if (firstStartTime === Infinity) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
for (let animation of animations) {
|
|
|
|
// If |animation| has been cancelled since this rAF callback
|
|
|
|
// was scheduled we don't want to set its startTime since
|
|
|
|
// that would restart it. We check for a cancelled animation
|
|
|
|
// by looking for a null currentTime rather than checking
|
|
|
|
// the playState, since reading the playState of
|
|
|
|
// a CSSAnimation object will flush style.
|
|
|
|
if (animation.currentTime !== null) {
|
|
|
|
animation.startTime = firstStartTime;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserAtIndex(aIndex) {
|
|
|
|
return this.browsers[aIndex];
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserIndexForDocument(aDocument) {
|
|
|
|
var tab = this._getTabForContentWindow(aDocument.defaultView);
|
|
|
|
return tab ? tab._tPos : -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserForDocument(aDocument) {
|
|
|
|
var tab = this._getTabForContentWindow(aDocument.defaultView);
|
|
|
|
return tab ? tab.linkedBrowser : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserForContentWindow(aWindow) {
|
|
|
|
var tab = this._getTabForContentWindow(aWindow);
|
|
|
|
return tab ? tab.linkedBrowser : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserForOuterWindowID(aID) {
|
|
|
|
return this._outerWindowIDBrowserMap.get(aID);
|
|
|
|
}
|
|
|
|
|
|
|
|
_getTabForContentWindow(aWindow) {
|
|
|
|
// When not using remote browsers, we can take a fast path by getting
|
|
|
|
// directly from the content window to the browser without looping
|
|
|
|
// over all browsers.
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
|
|
.QueryInterface(Ci.nsIDocShell)
|
|
|
|
.chromeEventHandler;
|
|
|
|
return this.getTabForBrowser(browser);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < this.browsers.length; i++) {
|
|
|
|
// NB: We use contentWindowAsCPOW so that this code works both
|
|
|
|
// for remote browsers as well. aWindow may be a CPOW.
|
|
|
|
if (this.browsers[i].contentWindowAsCPOW == aWindow)
|
|
|
|
return this.tabs[i];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
getTabForBrowser(aBrowser) {
|
|
|
|
return this._tabForBrowser.get(aBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
getNotificationBox(aBrowser) {
|
|
|
|
return this.getSidebarContainer(aBrowser).parentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
getSidebarContainer(aBrowser) {
|
|
|
|
return this.getBrowserContainer(aBrowser).parentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserContainer(aBrowser) {
|
|
|
|
return (aBrowser || this.mCurrentBrowser).parentNode.parentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
getTabModalPromptBox(aBrowser) {
|
|
|
|
let browser = (aBrowser || this.mCurrentBrowser);
|
|
|
|
if (!browser.tabModalPromptBox) {
|
|
|
|
browser.tabModalPromptBox = new TabModalPromptBox(browser);
|
|
|
|
}
|
|
|
|
return browser.tabModalPromptBox;
|
|
|
|
}
|
|
|
|
|
|
|
|
getTabFromAudioEvent(aEvent) {
|
|
|
|
if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
|
|
|
|
!aEvent.isTrusted) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
var browser = aEvent.originalTarget;
|
|
|
|
var tab = this.getTabForBrowser(browser);
|
|
|
|
return tab;
|
|
|
|
}
|
|
|
|
|
|
|
|
_callProgressListeners(aBrowser, aMethod, aArguments, aCallGlobalListeners, aCallTabsListeners) {
|
|
|
|
var rv = true;
|
|
|
|
|
|
|
|
function callListeners(listeners, args) {
|
|
|
|
for (let p of listeners) {
|
|
|
|
if (aMethod in p) {
|
|
|
|
try {
|
|
|
|
if (!p[aMethod].apply(p, args))
|
|
|
|
rv = false;
|
|
|
|
} catch (e) {
|
|
|
|
// don't inhibit other listeners
|
|
|
|
Components.utils.reportError(e);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aBrowser)
|
|
|
|
aBrowser = this.mCurrentBrowser;
|
|
|
|
|
|
|
|
// eslint-disable-next-line mozilla/no-compare-against-boolean-literals
|
|
|
|
if (aCallGlobalListeners != false &&
|
|
|
|
aBrowser == this.mCurrentBrowser) {
|
|
|
|
callListeners(this.mProgressListeners, aArguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line mozilla/no-compare-against-boolean-literals
|
|
|
|
if (aCallTabsListeners != false) {
|
|
|
|
aArguments.unshift(aBrowser);
|
|
|
|
|
|
|
|
callListeners(this.mTabsProgressListeners, aArguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
return rv;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine if a URI is an about: page pointing to a local resource.
|
|
|
|
*/
|
|
|
|
_isLocalAboutURI(aURI, aResolvedURI) {
|
|
|
|
if (!aURI.schemeIs("about")) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Specially handle about:blank as local
|
|
|
|
if (aURI.pathQueryRef === "blank") {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Use the passed in resolvedURI if we have one
|
|
|
|
const resolvedURI = aResolvedURI || Services.io.newChannelFromURI2(
|
|
|
|
aURI,
|
|
|
|
null, // loadingNode
|
|
|
|
Services.scriptSecurityManager.getSystemPrincipal(), // loadingPrincipal
|
|
|
|
null, // triggeringPrincipal
|
|
|
|
Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, // securityFlags
|
|
|
|
Ci.nsIContentPolicy.TYPE_OTHER // contentPolicyType
|
|
|
|
).URI;
|
|
|
|
return resolvedURI.schemeIs("jar") || resolvedURI.schemeIs("file");
|
|
|
|
} catch (ex) {
|
|
|
|
// aURI might be invalid.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A web progress listener object definition for a given tab.
|
|
|
|
*/
|
|
|
|
mTabProgressListener(aTab, aBrowser, aStartsBlank, aWasPreloadedBrowser, aOrigStateFlags) {
|
|
|
|
let stateFlags = aOrigStateFlags || 0;
|
|
|
|
// Initialize mStateFlags to non-zero e.g. when creating a progress
|
|
|
|
// listener for preloaded browsers as there was no progress listener
|
|
|
|
// around when the content started loading. If the content didn't
|
|
|
|
// quite finish loading yet, mStateFlags will very soon be overridden
|
|
|
|
// with the correct value and end up at STATE_STOP again.
|
|
|
|
if (aWasPreloadedBrowser) {
|
|
|
|
stateFlags = Ci.nsIWebProgressListener.STATE_STOP |
|
|
|
|
Ci.nsIWebProgressListener.STATE_IS_REQUEST;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ({
|
|
|
|
mTabBrowser: this,
|
|
|
|
mTab: aTab,
|
|
|
|
mBrowser: aBrowser,
|
|
|
|
mBlank: aStartsBlank,
|
|
|
|
|
|
|
|
// cache flags for correct status UI update after tab switching
|
|
|
|
mStateFlags: stateFlags,
|
|
|
|
mStatus: 0,
|
|
|
|
mMessage: "",
|
|
|
|
mTotalProgress: 0,
|
|
|
|
|
|
|
|
// count of open requests (should always be 0 or 1)
|
|
|
|
mRequestCount: 0,
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
delete this.mTab;
|
|
|
|
delete this.mBrowser;
|
|
|
|
delete this.mTabBrowser;
|
|
|
|
},
|
|
|
|
|
|
|
|
_callProgressListeners() {
|
|
|
|
Array.unshift(arguments, this.mBrowser);
|
|
|
|
return this.mTabBrowser._callProgressListeners.apply(this.mTabBrowser, arguments);
|
|
|
|
},
|
|
|
|
|
|
|
|
_shouldShowProgress(aRequest) {
|
|
|
|
if (this.mBlank)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Don't show progress indicators in tabs for about: URIs
|
|
|
|
// pointing to local resources.
|
|
|
|
if ((aRequest instanceof Ci.nsIChannel) &&
|
|
|
|
this.mTabBrowser._isLocalAboutURI(aRequest.originalURI, aRequest.URI)) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
return true;
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
_isForInitialAboutBlank(aWebProgress, aStateFlags, aLocation) {
|
|
|
|
if (!this.mBlank || !aWebProgress.isTopLevel) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If the state has STATE_STOP, and no requests were in flight, then this
|
|
|
|
// must be the initial "stop" for the initial about:blank document.
|
|
|
|
const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
|
|
|
|
if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
|
|
|
|
this.mRequestCount == 0 &&
|
|
|
|
!aLocation) {
|
|
|
|
return true;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let location = aLocation ? aLocation.spec : "";
|
|
|
|
return location == "about:blank";
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
onProgressChange(aWebProgress, aRequest,
|
|
|
|
aCurSelfProgress, aMaxSelfProgress,
|
|
|
|
aCurTotalProgress, aMaxTotalProgress) {
|
|
|
|
this.mTotalProgress = aMaxTotalProgress ? aCurTotalProgress / aMaxTotalProgress : 0;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (!this._shouldShowProgress(aRequest))
|
|
|
|
return;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.mTotalProgress && this.mTab.hasAttribute("busy"))
|
|
|
|
this.mTab.setAttribute("progress", "true");
|
|
|
|
|
|
|
|
this._callProgressListeners("onProgressChange",
|
|
|
|
[aWebProgress, aRequest,
|
|
|
|
aCurSelfProgress, aMaxSelfProgress,
|
|
|
|
aCurTotalProgress, aMaxTotalProgress]);
|
|
|
|
},
|
|
|
|
|
|
|
|
onProgressChange64(aWebProgress, aRequest,
|
|
|
|
aCurSelfProgress, aMaxSelfProgress,
|
|
|
|
aCurTotalProgress, aMaxTotalProgress) {
|
|
|
|
return this.onProgressChange(aWebProgress, aRequest,
|
|
|
|
aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress,
|
|
|
|
aMaxTotalProgress);
|
|
|
|
},
|
|
|
|
|
|
|
|
/* eslint-disable complexity */
|
|
|
|
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
|
|
|
|
if (!aRequest)
|
|
|
|
return;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
|
|
|
|
const nsIChannel = Components.interfaces.nsIChannel;
|
|
|
|
let location, originalLocation;
|
|
|
|
try {
|
|
|
|
aRequest.QueryInterface(nsIChannel);
|
|
|
|
location = aRequest.URI;
|
|
|
|
originalLocation = aRequest.originalURI;
|
|
|
|
} catch (ex) {}
|
|
|
|
|
|
|
|
let ignoreBlank = this._isForInitialAboutBlank(aWebProgress, aStateFlags,
|
|
|
|
location);
|
|
|
|
|
|
|
|
// If we were ignoring some messages about the initial about:blank, and we
|
|
|
|
// got the STATE_STOP for it, we'll want to pay attention to those messages
|
|
|
|
// from here forward. Similarly, if we conclude that this state change
|
|
|
|
// is one that we shouldn't be ignoring, then stop ignoring.
|
|
|
|
if ((ignoreBlank &&
|
|
|
|
aStateFlags & nsIWebProgressListener.STATE_STOP &&
|
|
|
|
aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) ||
|
|
|
|
!ignoreBlank && this.mBlank) {
|
|
|
|
this.mBlank = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aStateFlags & nsIWebProgressListener.STATE_START) {
|
|
|
|
this.mRequestCount++;
|
|
|
|
} else if (aStateFlags & nsIWebProgressListener.STATE_STOP) {
|
|
|
|
const NS_ERROR_UNKNOWN_HOST = 2152398878;
|
|
|
|
if (--this.mRequestCount > 0 && aStatus == NS_ERROR_UNKNOWN_HOST) {
|
|
|
|
// to prevent bug 235825: wait for the request handled
|
|
|
|
// by the automatic keyword resolver
|
|
|
|
return;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
// since we (try to) only handle STATE_STOP of the last request,
|
|
|
|
// the count of open requests should now be 0
|
|
|
|
this.mRequestCount = 0;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (aStateFlags & nsIWebProgressListener.STATE_START &&
|
|
|
|
aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
|
|
|
|
if (aWebProgress.isTopLevel) {
|
|
|
|
// Need to use originalLocation rather than location because things
|
|
|
|
// like about:home and about:privatebrowsing arrive with nsIRequest
|
|
|
|
// pointing to their resolved jar: or file: URIs.
|
|
|
|
if (!(originalLocation && gInitialPages.includes(originalLocation.spec) &&
|
|
|
|
originalLocation != "about:blank" &&
|
|
|
|
this.mBrowser.initialPageLoadedFromURLBar != originalLocation.spec &&
|
|
|
|
this.mBrowser.currentURI && this.mBrowser.currentURI.spec == "about:blank")) {
|
|
|
|
// Indicating that we started a load will allow the location
|
|
|
|
// bar to be cleared when the load finishes.
|
|
|
|
// In order to not overwrite user-typed content, we avoid it
|
|
|
|
// (see if condition above) in a very specific case:
|
|
|
|
// If the load is of an 'initial' page (e.g. about:privatebrowsing,
|
|
|
|
// about:newtab, etc.), was not explicitly typed in the location
|
|
|
|
// bar by the user, is not about:blank (because about:blank can be
|
|
|
|
// loaded by websites under their principal), and the current
|
|
|
|
// page in the browser is about:blank (indicating it is a newly
|
|
|
|
// created or re-created browser, e.g. because it just switched
|
|
|
|
// remoteness or is a new tab/window).
|
|
|
|
this.mBrowser.urlbarChangeTracker.startedLoad();
|
|
|
|
}
|
|
|
|
delete this.mBrowser.initialPageLoadedFromURLBar;
|
|
|
|
// If the browser is loading it must not be crashed anymore
|
|
|
|
this.mTab.removeAttribute("crashed");
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
|
|
|
|
if (this._shouldShowProgress(aRequest)) {
|
|
|
|
if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING) &&
|
|
|
|
aWebProgress && aWebProgress.isTopLevel) {
|
|
|
|
this.mTab.setAttribute("busy", "true");
|
|
|
|
this.mTab._notselectedsinceload = !this.mTab.selected;
|
|
|
|
SchedulePressure.startMonitoring(window, {
|
|
|
|
highPressureFn() {
|
|
|
|
// Only switch back to the SVG loading indicator after getting
|
|
|
|
// three consecutive low pressure callbacks. Used to prevent
|
|
|
|
// switching quickly between the SVG and APNG loading indicators.
|
|
|
|
gBrowser.tabContainer._schedulePressureCount = gBrowser.schedulePressureDefaultCount;
|
|
|
|
gBrowser.tabContainer.setAttribute("schedulepressure", "true");
|
|
|
|
},
|
|
|
|
lowPressureFn() {
|
|
|
|
if (!gBrowser.tabContainer._schedulePressureCount ||
|
|
|
|
--gBrowser.tabContainer._schedulePressureCount <= 0) {
|
|
|
|
gBrowser.tabContainer.removeAttribute("schedulepressure");
|
|
|
|
}
|
|
|
|
|
|
|
|
// If tabs are closed while they are loading we need to
|
|
|
|
// stop monitoring schedule pressure. We don't stop monitoring
|
|
|
|
// during high pressure times because we want to eventually
|
|
|
|
// return to the SVG tab loading animations.
|
|
|
|
let continueMonitoring = true;
|
|
|
|
if (!document.querySelector(".tabbrowser-tab[busy]")) {
|
|
|
|
SchedulePressure.stopMonitoring(window);
|
|
|
|
continueMonitoring = false;
|
|
|
|
}
|
|
|
|
return { continueMonitoring };
|
|
|
|
},
|
|
|
|
});
|
|
|
|
this.mTabBrowser.syncThrobberAnimations(this.mTab);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.mTab.selected) {
|
|
|
|
this.mTabBrowser.mIsBusy = true;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
} else if (aStateFlags & nsIWebProgressListener.STATE_STOP &&
|
|
|
|
aStateFlags & nsIWebProgressListener.STATE_IS_NETWORK) {
|
|
|
|
|
|
|
|
if (this.mTab.hasAttribute("busy")) {
|
|
|
|
this.mTab.removeAttribute("busy");
|
|
|
|
if (!document.querySelector(".tabbrowser-tab[busy]")) {
|
|
|
|
SchedulePressure.stopMonitoring(window);
|
|
|
|
this.mTabBrowser.tabContainer.removeAttribute("schedulepressure");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only animate the "burst" indicating the page has loaded if
|
|
|
|
// the top-level page is the one that finished loading.
|
|
|
|
if (aWebProgress.isTopLevel && !aWebProgress.isLoadingDocument &&
|
|
|
|
Components.isSuccessCode(aStatus) &&
|
|
|
|
!this.mTabBrowser.tabAnimationsInProgress &&
|
|
|
|
Services.prefs.getBoolPref("toolkit.cosmeticAnimations.enabled")) {
|
|
|
|
if (this.mTab._notselectedsinceload) {
|
|
|
|
this.mTab.setAttribute("notselectedsinceload", "true");
|
|
|
|
} else {
|
|
|
|
this.mTab.removeAttribute("notselectedsinceload");
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.mTab.setAttribute("bursting", "true");
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
|
|
|
|
if (!this.mTab.selected)
|
|
|
|
this.mTab.setAttribute("unread", "true");
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this.mTab.removeAttribute("progress");
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
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.
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.mBrowser.userTypedValue = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let inLoadURI = this.mBrowser.inLoadURI;
|
|
|
|
if (this.mTab.selected && gURLBar && !inLoadURI) {
|
|
|
|
URLBarSetURI();
|
|
|
|
}
|
|
|
|
} else if (isSuccessful) {
|
|
|
|
this.mBrowser.urlbarChangeTracker.finishedLoad();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Ignore initial about:blank to prevent flickering.
|
|
|
|
if (!this.mBrowser.mIconURL && !ignoreBlank) {
|
|
|
|
// Don't switch to the default icon on about:home or about:newtab,
|
|
|
|
// since these pages get their favicon set in browser code to
|
|
|
|
// improve perceived performance.
|
|
|
|
let isNewTab = originalLocation &&
|
|
|
|
(originalLocation.spec == "about:newtab" ||
|
|
|
|
originalLocation.spec == "about:privatebrowsing" ||
|
|
|
|
originalLocation.spec == "about:home");
|
|
|
|
if (!isNewTab) {
|
|
|
|
this.mTabBrowser.useDefaultIcon(this.mTab);
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// For keyword URIs clear the user typed value since they will be changed into real URIs
|
|
|
|
if (location.scheme == "keyword")
|
|
|
|
this.mBrowser.userTypedValue = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.mTab.selected)
|
|
|
|
this.mTabBrowser.mIsBusy = false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (ignoreBlank) {
|
|
|
|
this._callProgressListeners("onUpdateCurrentBrowser",
|
|
|
|
[aStateFlags, aStatus, "", 0],
|
|
|
|
true, false);
|
|
|
|
} else {
|
|
|
|
this._callProgressListeners("onStateChange",
|
|
|
|
[aWebProgress, aRequest, aStateFlags, aStatus],
|
|
|
|
true, false);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this._callProgressListeners("onStateChange",
|
|
|
|
[aWebProgress, aRequest, aStateFlags, aStatus],
|
|
|
|
false);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (aStateFlags & (nsIWebProgressListener.STATE_START |
|
|
|
|
nsIWebProgressListener.STATE_STOP)) {
|
|
|
|
// reset cached temporary values at beginning and end
|
|
|
|
this.mMessage = "";
|
|
|
|
this.mTotalProgress = 0;
|
|
|
|
}
|
|
|
|
this.mStateFlags = aStateFlags;
|
|
|
|
this.mStatus = aStatus;
|
|
|
|
},
|
|
|
|
/* eslint-enable complexity */
|
|
|
|
|
|
|
|
onLocationChange(aWebProgress, aRequest, aLocation,
|
|
|
|
aFlags) {
|
|
|
|
// OnLocationChange is called for both the top-level content
|
|
|
|
// and the subframes.
|
|
|
|
let topLevel = aWebProgress.isTopLevel;
|
|
|
|
|
|
|
|
if (topLevel) {
|
|
|
|
let isSameDocument = !!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT);
|
|
|
|
// We need to clear the typed value
|
|
|
|
// if the document failed to load, to make sure the urlbar reflects the
|
|
|
|
// failed URI (particularly for SSL errors). However, don't clear the value
|
|
|
|
// if the error page's URI is about:blank, because that causes complete
|
|
|
|
// loss of urlbar contents for invalid URI errors (see bug 867957).
|
|
|
|
// Another reason to clear the userTypedValue is if this was an anchor
|
|
|
|
// navigation initiated by the user.
|
|
|
|
if (this.mBrowser.didStartLoadSinceLastUserTyping() ||
|
|
|
|
((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
|
|
|
|
aLocation.spec != "about:blank") ||
|
|
|
|
(isSameDocument && this.mBrowser.inLoadURI)) {
|
|
|
|
this.mBrowser.userTypedValue = null;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If the tab has been set to "busy" outside the stateChange
|
|
|
|
// handler below (e.g. by sessionStore.navigateAndRestore), and
|
|
|
|
// the load results in an error page, it's possible that there
|
|
|
|
// isn't any (STATE_IS_NETWORK & STATE_STOP) state to cause busy
|
|
|
|
// attribute being removed. In this case we should remove the
|
|
|
|
// attribute here.
|
|
|
|
if ((aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) &&
|
|
|
|
this.mTab.hasAttribute("busy")) {
|
|
|
|
this.mTab.removeAttribute("busy");
|
|
|
|
this.mTabBrowser._tabAttrModified(this.mTab, ["busy"]);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If the browser was playing audio, we should remove the playing state.
|
|
|
|
if (this.mTab.hasAttribute("soundplaying") && !isSameDocument) {
|
|
|
|
clearTimeout(this.mTab._soundPlayingAttrRemovalTimer);
|
|
|
|
this.mTab._soundPlayingAttrRemovalTimer = 0;
|
|
|
|
this.mTab.removeAttribute("soundplaying");
|
|
|
|
this.mTabBrowser._tabAttrModified(this.mTab, ["soundplaying"]);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If the browser was previously muted, we should restore the muted state.
|
|
|
|
if (this.mTab.hasAttribute("muted")) {
|
|
|
|
this.mTab.linkedBrowser.mute();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.mTabBrowser.isFindBarInitialized(this.mTab)) {
|
|
|
|
let findBar = this.mTabBrowser.getFindBar(this.mTab);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Close the Find toolbar if we're in old-style TAF mode
|
|
|
|
if (findBar.findMode != findBar.FIND_NORMAL) {
|
|
|
|
findBar.close();
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.mTabBrowser.setTabTitle(this.mTab);
|
|
|
|
|
|
|
|
// Don't clear the favicon if this tab is in the pending
|
|
|
|
// state, as SessionStore will have set the icon for us even
|
|
|
|
// though we're pointed at an about:blank. Also don't clear it
|
|
|
|
// if onLocationChange was triggered by a pushState or a
|
|
|
|
// replaceState (bug 550565) or a hash change (bug 408415).
|
|
|
|
if (!this.mTab.hasAttribute("pending") &&
|
|
|
|
aWebProgress.isLoadingDocument &&
|
|
|
|
!isSameDocument) {
|
|
|
|
this.mBrowser.mIconURL = null;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let userContextId = this.mBrowser.getAttribute("usercontextid") || 0;
|
|
|
|
if (this.mBrowser.registeredOpenURI) {
|
|
|
|
this.mTabBrowser._unifiedComplete
|
|
|
|
.unregisterOpenPage(this.mBrowser.registeredOpenURI,
|
|
|
|
userContextId);
|
|
|
|
delete this.mBrowser.registeredOpenURI;
|
|
|
|
}
|
|
|
|
// Tabs in private windows aren't registered as "Open" so
|
|
|
|
// that they don't appear as switch-to-tab candidates.
|
|
|
|
if (!isBlankPageURL(aLocation.spec) &&
|
|
|
|
(!PrivateBrowsingUtils.isWindowPrivate(window) ||
|
|
|
|
PrivateBrowsingUtils.permanentPrivateBrowsing)) {
|
|
|
|
this.mTabBrowser._unifiedComplete
|
|
|
|
.registerOpenPage(aLocation, userContextId);
|
|
|
|
this.mBrowser.registeredOpenURI = aLocation;
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (!this.mBlank) {
|
|
|
|
this._callProgressListeners("onLocationChange",
|
|
|
|
[aWebProgress, aRequest, aLocation, aFlags]);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (topLevel) {
|
|
|
|
this.mBrowser.lastURI = aLocation;
|
|
|
|
this.mBrowser.lastLocationChange = Date.now();
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
|
|
|
|
if (this.mBlank)
|
|
|
|
return;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this._callProgressListeners("onStatusChange",
|
|
|
|
[aWebProgress, aRequest, aStatus, aMessage]);
|
|
|
|
|
|
|
|
this.mMessage = aMessage;
|
|
|
|
},
|
|
|
|
|
|
|
|
onSecurityChange(aWebProgress, aRequest, aState) {
|
|
|
|
this._callProgressListeners("onSecurityChange",
|
|
|
|
[aWebProgress, aRequest, aState]);
|
|
|
|
},
|
|
|
|
|
|
|
|
onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) {
|
|
|
|
return this._callProgressListeners("onRefreshAttempted",
|
|
|
|
[aWebProgress, aURI, aDelay, aSameURI]);
|
|
|
|
},
|
|
|
|
|
|
|
|
QueryInterface(aIID) {
|
|
|
|
if (aIID.equals(Components.interfaces.nsIWebProgressListener) ||
|
|
|
|
aIID.equals(Components.interfaces.nsIWebProgressListener2) ||
|
|
|
|
aIID.equals(Components.interfaces.nsISupportsWeakReference) ||
|
|
|
|
aIID.equals(Components.interfaces.nsISupports))
|
|
|
|
return this;
|
|
|
|
throw Components.results.NS_NOINTERFACE;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
storeIcon(aBrowser, aURI, aLoadingPrincipal, aRequestContextID) {
|
|
|
|
try {
|
|
|
|
if (!(aURI instanceof Ci.nsIURI)) {
|
|
|
|
aURI = makeURI(aURI);
|
|
|
|
}
|
|
|
|
PlacesUIUtils.loadFavicon(aBrowser, aLoadingPrincipal, aURI, aRequestContextID);
|
|
|
|
} catch (ex) {
|
|
|
|
Components.utils.reportError(ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setIcon(aTab, aURI, aLoadingPrincipal, aRequestContextID) {
|
|
|
|
let browser = this.getBrowserForTab(aTab);
|
|
|
|
browser.mIconURL = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
|
|
|
|
let loadingPrincipal = aLoadingPrincipal ||
|
|
|
|
Services.scriptSecurityManager.getSystemPrincipal();
|
|
|
|
let requestContextID = aRequestContextID || 0;
|
|
|
|
let sizedIconUrl = browser.mIconURL || "";
|
|
|
|
if (sizedIconUrl != aTab.getAttribute("image")) {
|
|
|
|
if (sizedIconUrl) {
|
|
|
|
if (!browser.mIconLoadingPrincipal ||
|
|
|
|
!browser.mIconLoadingPrincipal.equals(loadingPrincipal)) {
|
|
|
|
aTab.setAttribute("iconloadingprincipal",
|
|
|
|
this.serializationHelper.serializeToString(loadingPrincipal));
|
|
|
|
aTab.setAttribute("requestcontextid", requestContextID);
|
|
|
|
browser.mIconLoadingPrincipal = loadingPrincipal;
|
|
|
|
}
|
|
|
|
aTab.setAttribute("image", sizedIconUrl);
|
|
|
|
} else {
|
|
|
|
aTab.removeAttribute("iconloadingprincipal");
|
|
|
|
delete browser.mIconLoadingPrincipal;
|
|
|
|
aTab.removeAttribute("image");
|
|
|
|
}
|
|
|
|
this._tabAttrModified(aTab, ["image"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._callProgressListeners(browser, "onLinkIconAvailable", [browser.mIconURL]);
|
|
|
|
}
|
|
|
|
|
|
|
|
getIcon(aTab) {
|
|
|
|
let browser = aTab ? this.getBrowserForTab(aTab) : this.selectedBrowser;
|
|
|
|
return browser.mIconURL;
|
|
|
|
}
|
|
|
|
|
|
|
|
setPageInfo(aURL, aDescription, aPreviewImage) {
|
|
|
|
if (aURL) {
|
|
|
|
let pageInfo = { url: aURL, description: aDescription, previewImageURL: aPreviewImage };
|
|
|
|
PlacesUtils.history.update(pageInfo).catch(Components.utils.reportError);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
shouldLoadFavIcon(aURI) {
|
|
|
|
return (aURI &&
|
|
|
|
Services.prefs.getBoolPref("browser.chrome.site_icons") &&
|
|
|
|
Services.prefs.getBoolPref("browser.chrome.favicons") &&
|
|
|
|
("schemeIs" in aURI) && (aURI.schemeIs("http") || aURI.schemeIs("https")));
|
|
|
|
}
|
|
|
|
|
|
|
|
useDefaultIcon(aTab) {
|
|
|
|
let browser = this.getBrowserForTab(aTab);
|
|
|
|
let documentURI = browser.documentURI;
|
|
|
|
let requestContextID = browser.contentRequestContextID;
|
|
|
|
let loadingPrincipal = browser.contentPrincipal;
|
|
|
|
let 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) {
|
|
|
|
// Don't try to store the icon in Places, regardless it would
|
|
|
|
// be skipped (see Bug 403651).
|
|
|
|
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.storeIcon(browser, icon, loadingPrincipal, requestContextID);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setIcon(aTab, icon, loadingPrincipal, requestContextID);
|
|
|
|
}
|
|
|
|
|
|
|
|
isFailedIcon(aURI) {
|
|
|
|
if (!(aURI instanceof Ci.nsIURI))
|
|
|
|
aURI = makeURI(aURI);
|
|
|
|
return PlacesUtils.favicons.isFailedFavicon(aURI);
|
|
|
|
}
|
|
|
|
|
|
|
|
getWindowTitleForBrowser(aBrowser) {
|
|
|
|
var newTitle = "";
|
|
|
|
var docElement = this.ownerDocument.documentElement;
|
|
|
|
var sep = docElement.getAttribute("titlemenuseparator");
|
|
|
|
let tab = this.getTabForBrowser(aBrowser);
|
|
|
|
let docTitle;
|
|
|
|
|
|
|
|
if (tab._labelIsContentTitle) {
|
|
|
|
// Strip out any null bytes in the content title, since the
|
|
|
|
// underlying widget implementations of nsWindow::SetTitle pass
|
|
|
|
// null-terminated strings to system APIs.
|
|
|
|
docTitle = tab.getAttribute("label").replace(/\0/g, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!docTitle)
|
|
|
|
docTitle = docElement.getAttribute("titledefault");
|
|
|
|
|
|
|
|
var modifier = docElement.getAttribute("titlemodifier");
|
|
|
|
if (docTitle) {
|
|
|
|
newTitle += docElement.getAttribute("titlepreface");
|
|
|
|
newTitle += docTitle;
|
|
|
|
if (modifier)
|
|
|
|
newTitle += sep;
|
|
|
|
}
|
|
|
|
newTitle += modifier;
|
|
|
|
|
|
|
|
// If location bar is hidden and the URL type supports a host,
|
|
|
|
// add the scheme and host to the title to prevent spoofing.
|
|
|
|
// XXX https://bugzilla.mozilla.org/show_bug.cgi?id=22183#c239
|
|
|
|
try {
|
|
|
|
if (docElement.getAttribute("chromehidden").includes("location")) {
|
|
|
|
var uri = this.mURIFixup.createExposableURI(
|
|
|
|
aBrowser.currentURI);
|
|
|
|
if (uri.scheme == "about")
|
|
|
|
newTitle = uri.spec + sep + newTitle;
|
|
|
|
else
|
|
|
|
newTitle = uri.prePath + sep + newTitle;
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
|
|
|
|
return newTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTitlebar() {
|
|
|
|
this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
updateCurrentBrowser(aForceUpdate) {
|
|
|
|
var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
|
|
|
|
if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!aForceUpdate) {
|
|
|
|
document.commandDispatcher.lock();
|
|
|
|
|
|
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
// old way of measuring tab paint which is not valid with e10s.
|
|
|
|
// Waiting until the next MozAfterPaint ensures that we capture
|
|
|
|
// the time it takes to paint, upload the textures to the compositor,
|
|
|
|
// and then composite.
|
|
|
|
if (this._tabSwitchID) {
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let tabSwitchID = Symbol();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
|
|
|
|
this._tabSwitchID = tabSwitchID;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let onMozAfterPaint = () => {
|
|
|
|
if (this._tabSwitchID === tabSwitchID) {
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
|
|
|
|
this._tabSwitchID = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
window.removeEventListener("MozAfterPaint", onMozAfterPaint);
|
|
|
|
};
|
|
|
|
window.addEventListener("MozAfterPaint", onMozAfterPaint);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var oldTab = this.mCurrentTab;
|
|
|
|
|
|
|
|
// Preview mode should not reset the owner
|
|
|
|
if (!this._previewMode && !oldTab.selected)
|
|
|
|
oldTab.owner = null;
|
|
|
|
|
|
|
|
let lastRelatedTab = this._lastRelatedTabMap.get(oldTab);
|
|
|
|
if (lastRelatedTab) {
|
|
|
|
if (!lastRelatedTab.selected)
|
|
|
|
lastRelatedTab.owner = null;
|
|
|
|
}
|
|
|
|
this._lastRelatedTabMap = new WeakMap();
|
|
|
|
|
|
|
|
var oldBrowser = this.mCurrentBrowser;
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
oldBrowser.removeAttribute("primary");
|
|
|
|
oldBrowser.docShellIsActive = false;
|
|
|
|
newBrowser.setAttribute("primary", "true");
|
|
|
|
newBrowser.docShellIsActive =
|
|
|
|
(window.windowState != window.STATE_MINIMIZED &&
|
|
|
|
!window.isFullyOccluded);
|
|
|
|
}
|
|
|
|
|
|
|
|
var updateBlockedPopups = false;
|
|
|
|
if ((oldBrowser.blockedPopups && !newBrowser.blockedPopups) ||
|
|
|
|
(!oldBrowser.blockedPopups && newBrowser.blockedPopups))
|
|
|
|
updateBlockedPopups = true;
|
|
|
|
|
|
|
|
this.mCurrentBrowser = newBrowser;
|
|
|
|
this.mCurrentTab = this.tabContainer.selectedItem;
|
|
|
|
this.showTab(this.mCurrentTab);
|
|
|
|
|
|
|
|
gURLBar.setAttribute("switchingtabs", "true");
|
|
|
|
window.addEventListener("MozAfterPaint", function() {
|
|
|
|
gURLBar.removeAttribute("switchingtabs");
|
|
|
|
}, { once: true });
|
|
|
|
|
|
|
|
this._appendStatusPanel();
|
|
|
|
|
|
|
|
if (updateBlockedPopups)
|
|
|
|
this.mCurrentBrowser.updateBlockedPopups();
|
|
|
|
|
|
|
|
// Update the URL bar.
|
|
|
|
var loc = this.mCurrentBrowser.currentURI;
|
|
|
|
|
|
|
|
var webProgress = this.mCurrentBrowser.webProgress;
|
|
|
|
var securityUI = this.mCurrentBrowser.securityUI;
|
|
|
|
|
|
|
|
this._callProgressListeners(null, "onLocationChange",
|
|
|
|
[webProgress, null, loc, 0],
|
|
|
|
true, false);
|
|
|
|
|
|
|
|
if (securityUI) {
|
|
|
|
// Include the true final argument to indicate that this event is
|
|
|
|
// simulated (instead of being observed by the webProgressListener).
|
|
|
|
this._callProgressListeners(null, "onSecurityChange",
|
|
|
|
[webProgress, null, securityUI.state, true],
|
|
|
|
true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
var listener = this._tabListeners.get(this.mCurrentTab);
|
|
|
|
if (listener && listener.mStateFlags) {
|
|
|
|
this._callProgressListeners(null, "onUpdateCurrentBrowser",
|
|
|
|
[listener.mStateFlags, listener.mStatus,
|
|
|
|
listener.mMessage, listener.mTotalProgress],
|
|
|
|
true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this._previewMode) {
|
|
|
|
this.mCurrentTab.updateLastAccessed();
|
|
|
|
this.mCurrentTab.removeAttribute("unread");
|
|
|
|
oldTab.updateLastAccessed();
|
|
|
|
|
|
|
|
let oldFindBar = oldTab._findBar;
|
|
|
|
if (oldFindBar &&
|
|
|
|
oldFindBar.findMode == oldFindBar.FIND_NORMAL &&
|
|
|
|
!oldFindBar.hidden)
|
|
|
|
this._lastFindValue = oldFindBar._findField.value;
|
|
|
|
|
|
|
|
this.updateTitlebar();
|
|
|
|
|
|
|
|
this.mCurrentTab.removeAttribute("titlechanged");
|
|
|
|
this.mCurrentTab.removeAttribute("attention");
|
|
|
|
|
|
|
|
// The tab has been selected, it's not unselected anymore.
|
|
|
|
// (1) Call the current tab's finishUnselectedTabHoverTimer()
|
|
|
|
// to save a telemetry record.
|
|
|
|
// (2) Call the current browser's unselectedTabHover() with false
|
|
|
|
// to dispatch an event.
|
|
|
|
this.mCurrentTab.finishUnselectedTabHoverTimer();
|
|
|
|
this.mCurrentBrowser.unselectedTabHover(false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the new tab is busy, and our current state is not busy, then
|
|
|
|
// we need to fire a start to all progress listeners.
|
|
|
|
const nsIWebProgressListener = Components.interfaces.nsIWebProgressListener;
|
|
|
|
if (this.mCurrentTab.hasAttribute("busy") && !this.mIsBusy) {
|
|
|
|
this.mIsBusy = true;
|
|
|
|
this._callProgressListeners(null, "onStateChange",
|
|
|
|
[webProgress, null,
|
|
|
|
nsIWebProgressListener.STATE_START |
|
|
|
|
nsIWebProgressListener.STATE_IS_NETWORK, 0],
|
|
|
|
true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the new tab is not busy, and our current state is busy, then
|
|
|
|
// we need to fire a stop to all progress listeners.
|
|
|
|
if (!this.mCurrentTab.hasAttribute("busy") && this.mIsBusy) {
|
|
|
|
this.mIsBusy = false;
|
|
|
|
this._callProgressListeners(null, "onStateChange",
|
|
|
|
[webProgress, null,
|
|
|
|
nsIWebProgressListener.STATE_STOP |
|
|
|
|
nsIWebProgressListener.STATE_IS_NETWORK, 0],
|
|
|
|
true, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
// TabSelect events are suppressed during preview mode to avoid confusing extensions and other bits of code
|
|
|
|
// that might rely upon the other changes suppressed.
|
|
|
|
// Focus is suppressed in the event that the main browser window is minimized - focusing a tab would restore the window
|
|
|
|
if (!this._previewMode) {
|
|
|
|
// We've selected the new tab, so go ahead and notify listeners.
|
|
|
|
let event = new CustomEvent("TabSelect", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: false,
|
|
|
|
detail: {
|
|
|
|
previousTab: oldTab
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.mCurrentTab.dispatchEvent(event);
|
|
|
|
|
|
|
|
this._tabAttrModified(oldTab, ["selected"]);
|
|
|
|
this._tabAttrModified(this.mCurrentTab, ["selected"]);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (oldBrowser != newBrowser &&
|
|
|
|
oldBrowser.getInPermitUnload) {
|
|
|
|
oldBrowser.getInPermitUnload(inPermitUnload => {
|
|
|
|
if (!inPermitUnload) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Since the user is switching away from a tab that has
|
|
|
|
// a beforeunload prompt active, we remove the prompt.
|
|
|
|
// This prevents confusing user flows like the following:
|
|
|
|
// 1. User attempts to close Firefox
|
|
|
|
// 2. User switches tabs (ingoring a beforeunload prompt)
|
|
|
|
// 3. User returns to tab, presses "Leave page"
|
|
|
|
let promptBox = this.getTabModalPromptBox(oldBrowser);
|
|
|
|
let prompts = promptBox.listPrompts();
|
|
|
|
// There might not be any prompts here if the tab was closed
|
|
|
|
// while in an onbeforeunload prompt, which will have
|
|
|
|
// destroyed aforementioned prompt already, so check there's
|
|
|
|
// something to remove, first:
|
|
|
|
if (prompts.length) {
|
|
|
|
// NB: This code assumes that the beforeunload prompt
|
|
|
|
// is the top-most prompt on the tab.
|
|
|
|
prompts[prompts.length - 1].abortPrompt();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
this._adjustFocusBeforeTabSwitch(oldTab, this.mCurrentTab);
|
|
|
|
this._adjustFocusAfterTabSwitch(this.mCurrentTab);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateUserContextUIIndicator();
|
|
|
|
gIdentityHandler.updateSharingIndicator();
|
|
|
|
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
// Enable touch events to start a native dragging
|
|
|
|
// session to allow the user to easily drag the selected tab.
|
|
|
|
// This is currently only supported on Windows.
|
|
|
|
oldTab.removeAttribute("touchdownstartsdrag");
|
|
|
|
this.mCurrentTab.setAttribute("touchdownstartsdrag", "true");
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
document.commandDispatcher.unlock();
|
|
|
|
|
|
|
|
let event = new CustomEvent("TabSwitchDone", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: true
|
|
|
|
});
|
|
|
|
this.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aForceUpdate)
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_UPDATE_MS");
|
|
|
|
}
|
|
|
|
|
|
|
|
_adjustFocusBeforeTabSwitch(oldTab, newTab) {
|
|
|
|
if (this._previewMode) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let oldBrowser = oldTab.linkedBrowser;
|
|
|
|
let newBrowser = newTab.linkedBrowser;
|
|
|
|
|
|
|
|
oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
|
|
|
|
|
|
|
|
if (this.isFindBarInitialized(oldTab)) {
|
|
|
|
let findBar = this.getFindBar(oldTab);
|
|
|
|
oldTab._findBarFocused = (!findBar.hidden &&
|
|
|
|
findBar._findField.getAttribute("focused") == "true");
|
|
|
|
}
|
|
|
|
|
|
|
|
let activeEl = document.activeElement;
|
|
|
|
// If focus is on the old tab, move it to the new tab.
|
|
|
|
if (activeEl == oldTab) {
|
|
|
|
newTab.focus();
|
|
|
|
} else if (gMultiProcessBrowser && activeEl != newBrowser && activeEl != newTab) {
|
|
|
|
// In e10s, if focus isn't already in the tabstrip or on the new browser,
|
|
|
|
// and the new browser's previous focus wasn't in the url bar but focus is
|
|
|
|
// there now, we need to adjust focus further.
|
|
|
|
let keepFocusOnUrlBar = newBrowser &&
|
|
|
|
newBrowser._urlbarFocused &&
|
|
|
|
gURLBar &&
|
|
|
|
gURLBar.focused;
|
|
|
|
if (!keepFocusOnUrlBar) {
|
|
|
|
// Clear focus so that _adjustFocusAfterTabSwitch can detect if
|
|
|
|
// some element has been focused and respect that.
|
|
|
|
document.activeElement.blur();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_adjustFocusAfterTabSwitch(newTab) {
|
|
|
|
// Don't steal focus from the tab bar.
|
|
|
|
if (document.activeElement == newTab)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let newBrowser = this.getBrowserForTab(newTab);
|
|
|
|
|
|
|
|
// If there's a tabmodal prompt showing, focus it.
|
|
|
|
if (newBrowser.hasAttribute("tabmodalPromptShowing")) {
|
|
|
|
let XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
let prompts = newBrowser.parentNode.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
|
|
|
|
let prompt = prompts[prompts.length - 1];
|
|
|
|
prompt.Dialog.setDefaultFocus();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Focus the location bar if it was previously focused for that tab.
|
|
|
|
// In full screen mode, only bother making the location bar visible
|
|
|
|
// if the tab is a blank one.
|
|
|
|
if (newBrowser._urlbarFocused && gURLBar) {
|
|
|
|
// Explicitly close the popup if the URL bar retains focus
|
|
|
|
gURLBar.closePopup();
|
|
|
|
|
|
|
|
// If the user happened to type into the URL bar for this browser
|
|
|
|
// by the time we got here, focusing will cause the text to be
|
|
|
|
// selected which could cause them to overwrite what they've
|
|
|
|
// already typed in.
|
|
|
|
if (gURLBar.focused && newBrowser.userTypedValue) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!window.fullScreen || isTabEmpty(newTab)) {
|
|
|
|
focusAndSelectUrlBar();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Focus the find bar if it was previously focused for that tab.
|
|
|
|
if (gFindBarInitialized && !gFindBar.hidden &&
|
|
|
|
this.selectedTab._findBarFocused) {
|
|
|
|
gFindBar._findField.focus();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Don't focus the content area if something has been focused after the
|
|
|
|
// tab switch was initiated.
|
|
|
|
if (gMultiProcessBrowser &&
|
|
|
|
document.activeElement != document.documentElement)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// We're now committed to focusing the content area.
|
|
|
|
let fm = Services.focus;
|
|
|
|
let focusFlags = fm.FLAG_NOSCROLL;
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser) {
|
|
|
|
let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {});
|
|
|
|
|
|
|
|
// for anchors, use FLAG_SHOWRING so that it is clear what link was
|
|
|
|
// last clicked when switching back to that tab
|
|
|
|
if (newFocusedElement &&
|
|
|
|
(newFocusedElement instanceof HTMLAnchorElement ||
|
|
|
|
newFocusedElement.getAttributeNS("http://www.w3.org/1999/xlink", "type") == "simple"))
|
|
|
|
focusFlags |= fm.FLAG_SHOWRING;
|
|
|
|
}
|
|
|
|
|
|
|
|
fm.setFocus(newBrowser, focusFlags);
|
|
|
|
}
|
|
|
|
|
|
|
|
_tabAttrModified(aTab, aChanged) {
|
|
|
|
if (aTab.closing)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let event = new CustomEvent("TabAttrModified", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: false,
|
|
|
|
detail: {
|
|
|
|
changed: aChanged,
|
|
|
|
}
|
|
|
|
});
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
}
|
|
|
|
|
|
|
|
setBrowserSharing(aBrowser, aState) {
|
|
|
|
let tab = this.getTabForBrowser(aBrowser);
|
|
|
|
if (!tab)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (aState.sharing) {
|
|
|
|
tab._sharingState = aState;
|
|
|
|
if (aState.paused) {
|
|
|
|
tab.removeAttribute("sharing");
|
|
|
|
} else {
|
|
|
|
tab.setAttribute("sharing", aState.sharing);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
tab._sharingState = null;
|
|
|
|
tab.removeAttribute("sharing");
|
|
|
|
}
|
|
|
|
this._tabAttrModified(tab, ["sharing"]);
|
|
|
|
|
|
|
|
if (aBrowser == this.mCurrentBrowser)
|
|
|
|
gIdentityHandler.updateSharingIndicator();
|
|
|
|
}
|
|
|
|
|
|
|
|
getTabSharingState(aTab) {
|
|
|
|
// Normalize the state object for consumers (ie.extensions).
|
|
|
|
let state = Object.assign({}, aTab._sharingState);
|
|
|
|
return {
|
|
|
|
camera: !!state.camera,
|
|
|
|
microphone: !!state.microphone,
|
|
|
|
screen: state.screen && state.screen.replace("Paused", ""),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
setInitialTabTitle(aTab, aTitle, aOptions) {
|
|
|
|
if (aTitle) {
|
|
|
|
if (!aTab.getAttribute("label")) {
|
|
|
|
aTab._labelIsInitialTitle = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._setTabLabel(aTab, aTitle, aOptions);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setTabTitle(aTab) {
|
|
|
|
var browser = this.getBrowserForTab(aTab);
|
|
|
|
var title = browser.contentTitle;
|
|
|
|
|
|
|
|
// Don't replace an initially set label with the URL while the tab
|
|
|
|
// is loading.
|
|
|
|
if (aTab._labelIsInitialTitle) {
|
|
|
|
if (!title) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
delete aTab._labelIsInitialTitle;
|
|
|
|
}
|
|
|
|
|
|
|
|
let isContentTitle = false;
|
|
|
|
if (title) {
|
|
|
|
isContentTitle = true;
|
|
|
|
} else if (aTab.hasAttribute("customizemode")) {
|
|
|
|
let brandBundle = document.getElementById("bundle_brand");
|
|
|
|
let brandShortName = brandBundle.getString("brandShortName");
|
|
|
|
title = gNavigatorBundle.getFormattedString("customizeMode.tabTitle",
|
|
|
|
[brandShortName]);
|
|
|
|
isContentTitle = true;
|
|
|
|
} else {
|
|
|
|
if (browser.currentURI.displaySpec) {
|
|
|
|
try {
|
|
|
|
title = this.mURIFixup.createExposableURI(browser.currentURI).displaySpec;
|
|
|
|
} catch (ex) {
|
|
|
|
title = browser.currentURI.displaySpec;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (title && !isBlankPageURL(title)) {
|
|
|
|
// At this point, we now have a URI.
|
|
|
|
// Let's try to unescape it using a character set
|
|
|
|
// in case the URI is not ASCII.
|
|
|
|
try {
|
|
|
|
// If it's a long data: URI that uses base64 encoding, truncate to
|
|
|
|
// a reasonable length rather than trying to display the entire thing.
|
|
|
|
// We can't shorten arbitrary URIs like this, as bidi etc might mean
|
|
|
|
// we need the trailing characters for display. But a base64-encoded
|
|
|
|
// data-URI is plain ASCII, so this is OK for tab-title display.
|
|
|
|
// (See bug 1408854.)
|
|
|
|
if (title.length > 500 && title.match(/^data:[^,]+;base64,/)) {
|
|
|
|
title = title.substring(0, 500) + "\u2026";
|
|
|
|
} else {
|
|
|
|
var characterSet = browser.characterSet;
|
|
|
|
title = Services.textToSubURI.unEscapeNonAsciiURI(characterSet, title);
|
|
|
|
}
|
|
|
|
} catch (ex) { /* Do nothing. */ }
|
|
|
|
} else {
|
|
|
|
// Still no title? Fall back to our untitled string.
|
|
|
|
title = gTabBrowserBundle.GetStringFromName("tabs.emptyTabTitle");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._setTabLabel(aTab, title, { isContentTitle });
|
|
|
|
}
|
|
|
|
|
|
|
|
_setTabLabel(aTab, aLabel, aOptions) {
|
|
|
|
if (!aLabel) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
aTab._fullLabel = aLabel;
|
|
|
|
|
|
|
|
aOptions = aOptions || {};
|
|
|
|
if (!aOptions.isContentTitle) {
|
|
|
|
// Remove protocol and "www."
|
|
|
|
if (!("_regex_shortenURLForTabLabel" in this)) {
|
|
|
|
this._regex_shortenURLForTabLabel = /^[^:]+:\/\/(?:www\.)?/;
|
|
|
|
}
|
|
|
|
aLabel = aLabel.replace(this._regex_shortenURLForTabLabel, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
aTab._labelIsContentTitle = aOptions.isContentTitle;
|
|
|
|
|
|
|
|
if (aTab.getAttribute("label") == aLabel) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let dwu = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
|
|
let isRTL = dwu.getDirectionFromText(aLabel) == Ci.nsIDOMWindowUtils.DIRECTION_RTL;
|
|
|
|
|
|
|
|
aTab.setAttribute("label", aLabel);
|
|
|
|
aTab.setAttribute("labeldirection", isRTL ? "rtl" : "ltr");
|
|
|
|
|
|
|
|
// Dispatch TabAttrModified event unless we're setting the label
|
|
|
|
// before the TabOpen event was dispatched.
|
|
|
|
if (!aOptions.beforeTabOpen) {
|
|
|
|
this._tabAttrModified(aTab, ["label"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aTab.selected) {
|
|
|
|
this.updateTitlebar();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
loadOneTab(aURI, aReferrerURI, aCharset, aPostData, aLoadInBackground, aAllowThirdPartyFixup) {
|
|
|
|
var aTriggeringPrincipal;
|
|
|
|
var aReferrerPolicy;
|
|
|
|
var aFromExternal;
|
|
|
|
var aRelatedToCurrent;
|
|
|
|
var aAllowMixedContent;
|
|
|
|
var aSkipAnimation;
|
|
|
|
var aForceNotRemote;
|
|
|
|
var aPreferredRemoteType;
|
|
|
|
var aNoReferrer;
|
|
|
|
var aUserContextId;
|
|
|
|
var aSameProcessAsFrameLoader;
|
|
|
|
var aOriginPrincipal;
|
|
|
|
var aOpener;
|
|
|
|
var aOpenerBrowser;
|
|
|
|
var aCreateLazyBrowser;
|
|
|
|
var aNextTabParentId;
|
|
|
|
var aFocusUrlBar;
|
|
|
|
var aName;
|
|
|
|
if (arguments.length == 2 &&
|
|
|
|
typeof arguments[1] == "object" &&
|
|
|
|
!(arguments[1] instanceof Ci.nsIURI)) {
|
|
|
|
let params = arguments[1];
|
|
|
|
aTriggeringPrincipal = params.triggeringPrincipal;
|
|
|
|
aReferrerURI = params.referrerURI;
|
|
|
|
aReferrerPolicy = params.referrerPolicy;
|
|
|
|
aCharset = params.charset;
|
|
|
|
aPostData = params.postData;
|
|
|
|
aLoadInBackground = params.inBackground;
|
|
|
|
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
|
|
|
|
aFromExternal = params.fromExternal;
|
|
|
|
aRelatedToCurrent = params.relatedToCurrent;
|
|
|
|
aAllowMixedContent = params.allowMixedContent;
|
|
|
|
aSkipAnimation = params.skipAnimation;
|
|
|
|
aForceNotRemote = params.forceNotRemote;
|
|
|
|
aPreferredRemoteType = params.preferredRemoteType;
|
|
|
|
aNoReferrer = params.noReferrer;
|
|
|
|
aUserContextId = params.userContextId;
|
|
|
|
aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader;
|
|
|
|
aOriginPrincipal = params.originPrincipal;
|
|
|
|
aOpener = params.opener;
|
|
|
|
aOpenerBrowser = params.openerBrowser;
|
|
|
|
aCreateLazyBrowser = params.createLazyBrowser;
|
|
|
|
aNextTabParentId = params.nextTabParentId;
|
|
|
|
aFocusUrlBar = params.focusUrlBar;
|
|
|
|
aName = params.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
var bgLoad = (aLoadInBackground != null) ? aLoadInBackground :
|
|
|
|
Services.prefs.getBoolPref("browser.tabs.loadInBackground");
|
|
|
|
var owner = bgLoad ? null : this.selectedTab;
|
|
|
|
|
|
|
|
var tab = this.addTab(aURI, {
|
|
|
|
triggeringPrincipal: aTriggeringPrincipal,
|
|
|
|
referrerURI: aReferrerURI,
|
|
|
|
referrerPolicy: aReferrerPolicy,
|
|
|
|
charset: aCharset,
|
|
|
|
postData: aPostData,
|
|
|
|
ownerTab: owner,
|
|
|
|
allowThirdPartyFixup: aAllowThirdPartyFixup,
|
|
|
|
fromExternal: aFromExternal,
|
|
|
|
relatedToCurrent: aRelatedToCurrent,
|
|
|
|
skipAnimation: aSkipAnimation,
|
|
|
|
allowMixedContent: aAllowMixedContent,
|
|
|
|
forceNotRemote: aForceNotRemote,
|
|
|
|
createLazyBrowser: aCreateLazyBrowser,
|
|
|
|
preferredRemoteType: aPreferredRemoteType,
|
|
|
|
noReferrer: aNoReferrer,
|
|
|
|
userContextId: aUserContextId,
|
|
|
|
originPrincipal: aOriginPrincipal,
|
|
|
|
sameProcessAsFrameLoader: aSameProcessAsFrameLoader,
|
|
|
|
opener: aOpener,
|
|
|
|
openerBrowser: aOpenerBrowser,
|
|
|
|
nextTabParentId: aNextTabParentId,
|
|
|
|
focusUrlBar: aFocusUrlBar,
|
|
|
|
name: aName
|
|
|
|
});
|
|
|
|
if (!bgLoad)
|
|
|
|
this.selectedTab = tab;
|
|
|
|
|
|
|
|
return tab;
|
|
|
|
}
|
|
|
|
|
|
|
|
loadTabs(aURIs, aLoadInBackground, aReplace) {
|
|
|
|
let aTriggeringPrincipal;
|
|
|
|
let aAllowThirdPartyFixup;
|
|
|
|
let aTargetTab;
|
|
|
|
let aNewIndex = -1;
|
|
|
|
let aPostDatas = [];
|
|
|
|
let aUserContextId;
|
|
|
|
if (arguments.length == 2 &&
|
|
|
|
typeof arguments[1] == "object") {
|
|
|
|
let params = arguments[1];
|
|
|
|
aLoadInBackground = params.inBackground;
|
|
|
|
aReplace = params.replace;
|
|
|
|
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
|
|
|
|
aTargetTab = params.targetTab;
|
|
|
|
aNewIndex = typeof params.newIndex === "number" ?
|
|
|
|
params.newIndex : aNewIndex;
|
|
|
|
aPostDatas = params.postDatas || aPostDatas;
|
|
|
|
aUserContextId = params.userContextId;
|
|
|
|
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;
|
|
|
|
if (aTargetTab) {
|
|
|
|
browser = this.getBrowserForTab(aTargetTab);
|
|
|
|
targetTabIndex = aTargetTab._tPos;
|
|
|
|
} else {
|
|
|
|
browser = this.mCurrentBrowser;
|
|
|
|
targetTabIndex = this.tabContainer.selectedIndex;
|
|
|
|
}
|
|
|
|
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
|
|
if (aAllowThirdPartyFixup) {
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
|
|
|
|
Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
browser.loadURIWithFlags(aURIs[0], {
|
|
|
|
flags,
|
|
|
|
postData: aPostDatas[0],
|
|
|
|
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,
|
|
|
|
skipAnimation: multiple,
|
|
|
|
allowThirdPartyFixup: aAllowThirdPartyFixup,
|
|
|
|
postData: aPostDatas[0],
|
|
|
|
userContextId: aUserContextId,
|
|
|
|
triggeringPrincipal: aTriggeringPrincipal,
|
|
|
|
});
|
|
|
|
if (aNewIndex !== -1) {
|
|
|
|
this.moveTabTo(firstTabAdded, aNewIndex);
|
|
|
|
targetTabIndex = firstTabAdded._tPos;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let tabNum = targetTabIndex;
|
|
|
|
for (let i = 1; i < aURIs.length; ++i) {
|
|
|
|
let tab = this.addTab(aURIs[i], {
|
|
|
|
skipAnimation: true,
|
|
|
|
allowThirdPartyFixup: aAllowThirdPartyFixup,
|
|
|
|
postData: aPostDatas[i],
|
|
|
|
userContextId: aUserContextId,
|
|
|
|
triggeringPrincipal: aTriggeringPrincipal,
|
|
|
|
});
|
|
|
|
if (targetTabIndex !== -1)
|
|
|
|
this.moveTabTo(tab, ++tabNum);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (firstTabAdded && !aLoadInBackground) {
|
|
|
|
this.selectedTab = firstTabAdded;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateBrowserRemoteness(aBrowser, aShouldBeRemote, aOptions) {
|
|
|
|
aOptions = aOptions || {};
|
|
|
|
let isRemote = aBrowser.getAttribute("remote") == "true";
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser && aShouldBeRemote) {
|
|
|
|
throw new Error("Cannot switch to remote browser in a window " +
|
|
|
|
"without the remote tabs load context.");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default values for remoteType
|
|
|
|
if (!aOptions.remoteType) {
|
|
|
|
aOptions.remoteType = aShouldBeRemote ? E10SUtils.DEFAULT_REMOTE_TYPE : E10SUtils.NOT_REMOTE;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we are passed an opener, we must be making the browser non-remote, and
|
|
|
|
// if the browser is _currently_ non-remote, we need the openers to match,
|
|
|
|
// because it is already too late to change it.
|
|
|
|
if (aOptions.opener) {
|
|
|
|
if (aShouldBeRemote) {
|
|
|
|
throw new Error("Cannot set an opener on a browser which should be remote!");
|
|
|
|
}
|
|
|
|
if (!isRemote && aBrowser.contentWindow.opener != aOptions.opener) {
|
|
|
|
throw new Error("Cannot change opener on an already non-remote browser!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Abort if we're not going to change anything
|
|
|
|
let currentRemoteType = aBrowser.getAttribute("remoteType");
|
|
|
|
if (isRemote == aShouldBeRemote && !aOptions.newFrameloader &&
|
|
|
|
(!isRemote || currentRemoteType == aOptions.remoteType)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let tab = this.getTabForBrowser(aBrowser);
|
|
|
|
// aBrowser needs to be inserted now if it hasn't been already.
|
|
|
|
this._insertBrowser(tab);
|
|
|
|
|
|
|
|
let evt = document.createEvent("Events");
|
|
|
|
evt.initEvent("BeforeTabRemotenessChange", true, false);
|
|
|
|
tab.dispatchEvent(evt);
|
|
|
|
|
|
|
|
let wasActive = document.activeElement == aBrowser;
|
|
|
|
|
|
|
|
// Unmap the old outerWindowID.
|
|
|
|
this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
|
|
|
|
|
|
|
|
// Unhook our progress listener.
|
|
|
|
let filter = this._tabFilters.get(tab);
|
|
|
|
let listener = this._tabListeners.get(tab);
|
|
|
|
aBrowser.webProgress.removeProgressListener(filter);
|
|
|
|
filter.removeProgressListener(listener);
|
|
|
|
|
|
|
|
// We'll be creating a new listener, so destroy the old one.
|
|
|
|
listener.destroy();
|
|
|
|
|
|
|
|
let oldUserTypedValue = aBrowser.userTypedValue;
|
|
|
|
let hadStartedLoad = aBrowser.didStartLoadSinceLastUserTyping();
|
|
|
|
|
|
|
|
// Make sure the browser is destroyed so it unregisters from observer notifications
|
|
|
|
aBrowser.destroy();
|
|
|
|
|
|
|
|
// Make sure to restore the original droppedLinkHandler and
|
|
|
|
// sameProcessAsFrameLoader.
|
|
|
|
let droppedLinkHandler = aBrowser.droppedLinkHandler;
|
|
|
|
let sameProcessAsFrameLoader = aBrowser.sameProcessAsFrameLoader;
|
|
|
|
|
|
|
|
// Change the "remote" attribute.
|
|
|
|
let parent = aBrowser.parentNode;
|
|
|
|
parent.removeChild(aBrowser);
|
|
|
|
if (aShouldBeRemote) {
|
|
|
|
aBrowser.setAttribute("remote", "true");
|
|
|
|
aBrowser.setAttribute("remoteType", aOptions.remoteType);
|
|
|
|
} else {
|
|
|
|
aBrowser.setAttribute("remote", "false");
|
|
|
|
aBrowser.removeAttribute("remoteType");
|
|
|
|
}
|
|
|
|
|
|
|
|
// NB: This works with the hack in the browser constructor that
|
|
|
|
// turns this normal property into a field.
|
|
|
|
if (aOptions.sameProcessAsFrameLoader) {
|
|
|
|
// Always set sameProcessAsFrameLoader when passed in aOptions.
|
|
|
|
aBrowser.sameProcessAsFrameLoader = aOptions.sameProcessAsFrameLoader;
|
|
|
|
} else if (!aShouldBeRemote || currentRemoteType == aOptions.remoteType) {
|
|
|
|
// Only copy existing sameProcessAsFrameLoader when not switching
|
|
|
|
// remote type otherwise it would stop the switch.
|
|
|
|
aBrowser.sameProcessAsFrameLoader = sameProcessAsFrameLoader;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aOptions.opener) {
|
|
|
|
// Set the opener window on the browser, such that when the frame
|
|
|
|
// loader is created the opener is set correctly.
|
|
|
|
aBrowser.presetOpenerWindow(aOptions.opener);
|
|
|
|
}
|
|
|
|
|
|
|
|
parent.appendChild(aBrowser);
|
|
|
|
|
|
|
|
aBrowser.userTypedValue = oldUserTypedValue;
|
|
|
|
if (hadStartedLoad) {
|
|
|
|
aBrowser.urlbarChangeTracker.startedLoad();
|
|
|
|
}
|
|
|
|
|
|
|
|
aBrowser.droppedLinkHandler = droppedLinkHandler;
|
|
|
|
|
|
|
|
// Switching a browser's remoteness will create a new frameLoader.
|
|
|
|
// As frameLoaders start out with an active docShell we have to
|
|
|
|
// deactivate it if this is not the selected tab's browser or the
|
|
|
|
// browser window is minimized.
|
|
|
|
aBrowser.docShellIsActive = this.shouldActivateDocShell(aBrowser);
|
|
|
|
|
|
|
|
// Create a new tab progress listener for the new browser we just injected,
|
|
|
|
// since tab progress listeners have logic for handling the initial about:blank
|
|
|
|
// load
|
|
|
|
listener = this.mTabProgressListener(tab, aBrowser, true, false);
|
|
|
|
this._tabListeners.set(tab, listener);
|
|
|
|
filter.addProgressListener(listener, Ci.nsIWebProgress.NOTIFY_ALL);
|
|
|
|
|
|
|
|
// Restore the progress listener.
|
|
|
|
aBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
|
|
|
|
|
|
|
|
// Restore the securityUI state.
|
|
|
|
let securityUI = aBrowser.securityUI;
|
|
|
|
let state = securityUI ? securityUI.state :
|
|
|
|
Ci.nsIWebProgressListener.STATE_IS_INSECURE;
|
|
|
|
// Include the true final argument to indicate that this event is
|
|
|
|
// simulated (instead of being observed by the webProgressListener).
|
|
|
|
this._callProgressListeners(aBrowser, "onSecurityChange",
|
|
|
|
[aBrowser.webProgress, null, state, true],
|
|
|
|
true, false);
|
|
|
|
|
|
|
|
if (aShouldBeRemote) {
|
|
|
|
// Switching the browser to be remote will connect to a new child
|
|
|
|
// process so the browser can no longer be considered to be
|
|
|
|
// crashed.
|
|
|
|
tab.removeAttribute("crashed");
|
|
|
|
} else {
|
|
|
|
aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
|
|
|
|
|
|
|
|
// Register the new outerWindowID.
|
|
|
|
this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wasActive)
|
|
|
|
aBrowser.focus();
|
|
|
|
|
|
|
|
// If the findbar has been initialised, reset its browser reference.
|
|
|
|
if (this.isFindBarInitialized(tab)) {
|
|
|
|
this.getFindBar(tab).browser = aBrowser;
|
|
|
|
}
|
|
|
|
|
|
|
|
evt = document.createEvent("Events");
|
|
|
|
evt.initEvent("TabRemotenessChange", true, false);
|
|
|
|
tab.dispatchEvent(evt);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateBrowserRemotenessByURL(aBrowser, aURL, aOptions) {
|
|
|
|
aOptions = aOptions || {};
|
|
|
|
|
|
|
|
if (!gMultiProcessBrowser)
|
|
|
|
return this.updateBrowserRemoteness(aBrowser, false);
|
|
|
|
|
|
|
|
let currentRemoteType = aBrowser.getAttribute("remoteType") || null;
|
|
|
|
|
|
|
|
aOptions.remoteType =
|
|
|
|
E10SUtils.getRemoteTypeForURI(aURL,
|
|
|
|
gMultiProcessBrowser,
|
|
|
|
currentRemoteType,
|
|
|
|
aBrowser.currentURI);
|
|
|
|
|
|
|
|
// If this URL can't load in the current browser then flip it to the
|
|
|
|
// correct type.
|
|
|
|
if (currentRemoteType != aOptions.remoteType ||
|
|
|
|
aOptions.newFrameloader) {
|
|
|
|
let remote = aOptions.remoteType != E10SUtils.NOT_REMOTE;
|
|
|
|
return this.updateBrowserRemoteness(aBrowser, remote, aOptions);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
removePreloadedBrowser() {
|
|
|
|
if (!this._isPreloadingEnabled()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let browser = this._getPreloadedBrowser();
|
|
|
|
|
|
|
|
if (browser) {
|
|
|
|
browser.remove();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_getPreloadedBrowser() {
|
|
|
|
if (!this._isPreloadingEnabled()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The preloaded browser might be null.
|
|
|
|
let browser = this._preloadedBrowser;
|
|
|
|
|
|
|
|
// Consume the browser.
|
|
|
|
this._preloadedBrowser = null;
|
|
|
|
|
|
|
|
// Attach the nsIFormFillController now that we know the browser
|
|
|
|
// will be used. If we do that before and the preloaded browser
|
|
|
|
// won't be consumed until shutdown then we leak a docShell.
|
|
|
|
// Also, we do not need to take care of attaching nsIFormFillControllers
|
|
|
|
// in the case that the browser is remote, as remote browsers take
|
|
|
|
// care of that themselves.
|
|
|
|
if (browser) {
|
|
|
|
browser.setAttribute("preloadedState", "consumed");
|
|
|
|
if (this.hasAttribute("autocompletepopup")) {
|
|
|
|
browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return browser;
|
|
|
|
}
|
|
|
|
|
|
|
|
_isPreloadingEnabled() {
|
|
|
|
// Preloading for the newtab page is enabled when the pref is true
|
|
|
|
// and the URL is "about:newtab". We do not support preloading for
|
|
|
|
// custom newtab URLs.
|
|
|
|
return Services.prefs.getBoolPref("browser.newtab.preload") &&
|
|
|
|
!aboutNewTabService.overridden;
|
|
|
|
}
|
|
|
|
|
|
|
|
_createPreloadBrowser() {
|
|
|
|
// Do nothing if we have a preloaded browser already
|
|
|
|
// or preloading of newtab pages is disabled.
|
|
|
|
if (this._preloadedBrowser || !this._isPreloadingEnabled()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let remoteType =
|
|
|
|
E10SUtils.getRemoteTypeForURI(BROWSER_NEW_TAB_URL,
|
|
|
|
gMultiProcessBrowser);
|
|
|
|
let browser = this._createBrowser({ isPreloadBrowser: true, remoteType });
|
|
|
|
this._preloadedBrowser = browser;
|
|
|
|
|
|
|
|
let notificationbox = this.getNotificationBox(browser);
|
|
|
|
this.mPanelContainer.appendChild(notificationbox);
|
|
|
|
|
|
|
|
if (remoteType != E10SUtils.NOT_REMOTE) {
|
|
|
|
// For remote browsers, we need to make sure that the webProgress is
|
|
|
|
// instantiated, otherwise the parent won't get informed about the state
|
|
|
|
// of the preloaded browser until it gets attached to a tab.
|
|
|
|
browser.webProgress;
|
|
|
|
}
|
|
|
|
|
|
|
|
browser.loadURI(BROWSER_NEW_TAB_URL);
|
|
|
|
browser.docShellIsActive = false;
|
|
|
|
|
|
|
|
// Make sure the preloaded browser is loaded with desired zoom level
|
|
|
|
let tabURI = Services.io.newURI(BROWSER_NEW_TAB_URL);
|
|
|
|
FullZoom.onLocationChange(tabURI, false, browser);
|
|
|
|
}
|
|
|
|
|
|
|
|
_createBrowser(aParams) {
|
|
|
|
// Supported parameters:
|
|
|
|
// userContextId, remote, remoteType, isPreloadBrowser,
|
|
|
|
// uriIsAboutBlank, sameProcessAsFrameLoader
|
|
|
|
|
|
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
|
|
|
|
let b = document.createElementNS(NS_XUL, "browser");
|
|
|
|
b.permanentKey = {};
|
|
|
|
b.setAttribute("type", "content");
|
|
|
|
b.setAttribute("message", "true");
|
|
|
|
b.setAttribute("messagemanagergroup", "browsers");
|
|
|
|
b.setAttribute("contextmenu", this.getAttribute("contentcontextmenu"));
|
|
|
|
b.setAttribute("tooltip", this.getAttribute("contenttooltip"));
|
|
|
|
|
|
|
|
if (aParams.userContextId) {
|
|
|
|
b.setAttribute("usercontextid", aParams.userContextId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// remote parameter used by some addons, use default in this case.
|
|
|
|
if (aParams.remote && !aParams.remoteType) {
|
|
|
|
aParams.remoteType = E10SUtils.DEFAULT_REMOTE_TYPE;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aParams.remoteType) {
|
|
|
|
b.setAttribute("remoteType", aParams.remoteType);
|
|
|
|
b.setAttribute("remote", "true");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aParams.openerWindow) {
|
|
|
|
if (aParams.remoteType) {
|
|
|
|
throw new Error("Cannot set opener window on a remote browser!");
|
|
|
|
}
|
|
|
|
b.presetOpenerWindow(aParams.openerWindow);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aParams.isPreloadBrowser && this.hasAttribute("autocompletepopup")) {
|
|
|
|
b.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This attribute is meant to describe if the browser is the
|
|
|
|
* preloaded browser. There are 2 defined states: "preloaded" or
|
|
|
|
* "consumed". The order of events goes as follows:
|
|
|
|
* 1. The preloaded browser is created and the 'preloadedState'
|
|
|
|
* attribute for that browser is set to "preloaded".
|
|
|
|
* 2. When a new tab is opened and it is time to show that
|
|
|
|
* preloaded browser, the 'preloadedState' attribute for that
|
|
|
|
* browser is set to "consumed"
|
|
|
|
* 3. When we then navigate away from about:newtab, the "consumed"
|
|
|
|
* browsers will attempt to switch to a new content process,
|
|
|
|
* therefore the 'preloadedState' attribute is removed from
|
|
|
|
* that browser altogether
|
|
|
|
* See more details on Bug 1420285.
|
|
|
|
*/
|
|
|
|
if (aParams.isPreloadBrowser) {
|
|
|
|
b.setAttribute("preloadedState", "preloaded");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.hasAttribute("selectmenulist"))
|
|
|
|
b.setAttribute("selectmenulist", this.getAttribute("selectmenulist"));
|
|
|
|
|
|
|
|
if (this.hasAttribute("datetimepicker")) {
|
|
|
|
b.setAttribute("datetimepicker", this.getAttribute("datetimepicker"));
|
|
|
|
}
|
|
|
|
|
|
|
|
b.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
|
|
|
|
|
|
|
|
if (aParams.nextTabParentId) {
|
|
|
|
if (!aParams.remoteType) {
|
|
|
|
throw new Error("Cannot have nextTabParentId without a remoteType");
|
|
|
|
}
|
|
|
|
// Gecko is going to read this attribute and use it.
|
|
|
|
b.setAttribute("nextTabParentId", aParams.nextTabParentId.toString());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aParams.sameProcessAsFrameLoader) {
|
|
|
|
b.sameProcessAsFrameLoader = aParams.sameProcessAsFrameLoader;
|
|
|
|
}
|
|
|
|
|
|
|
|
// This will be used by gecko to control the name of the opened
|
|
|
|
// window.
|
|
|
|
if (aParams.name) {
|
|
|
|
// XXX: The `name` property is special in HTML and XUL. Should
|
|
|
|
// we use a different attribute name for this?
|
|
|
|
b.setAttribute("name", aParams.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the browserStack container
|
|
|
|
var stack = document.createElementNS(NS_XUL, "stack");
|
|
|
|
stack.className = "browserStack";
|
|
|
|
stack.appendChild(b);
|
|
|
|
stack.setAttribute("flex", "1");
|
|
|
|
|
|
|
|
// Create the browserContainer
|
|
|
|
var browserContainer = document.createElementNS(NS_XUL, "vbox");
|
|
|
|
browserContainer.className = "browserContainer";
|
|
|
|
browserContainer.appendChild(stack);
|
|
|
|
browserContainer.setAttribute("flex", "1");
|
|
|
|
|
|
|
|
// Create the sidebar container
|
|
|
|
var browserSidebarContainer = document.createElementNS(NS_XUL,
|
|
|
|
"hbox");
|
|
|
|
browserSidebarContainer.className = "browserSidebarContainer";
|
|
|
|
browserSidebarContainer.appendChild(browserContainer);
|
|
|
|
browserSidebarContainer.setAttribute("flex", "1");
|
|
|
|
|
|
|
|
// Add the Message and the Browser to the box
|
|
|
|
var notificationbox = document.createElementNS(NS_XUL,
|
|
|
|
"notificationbox");
|
|
|
|
notificationbox.setAttribute("flex", "1");
|
|
|
|
notificationbox.setAttribute("notificationside", "top");
|
|
|
|
notificationbox.appendChild(browserSidebarContainer);
|
|
|
|
|
|
|
|
// Prevent the superfluous initial load of a blank document
|
|
|
|
// if we're going to load something other than about:blank.
|
|
|
|
if (!aParams.uriIsAboutBlank) {
|
|
|
|
b.setAttribute("nodefaultsrc", "true");
|
|
|
|
}
|
|
|
|
|
|
|
|
return b;
|
|
|
|
}
|
|
|
|
|
|
|
|
_createLazyBrowser(aTab) {
|
|
|
|
let browser = aTab.linkedBrowser;
|
|
|
|
|
|
|
|
let names = this._browserBindingProperties;
|
|
|
|
|
|
|
|
for (let i = 0; i < names.length; i++) {
|
|
|
|
let name = names[i];
|
|
|
|
let getter;
|
|
|
|
let setter;
|
|
|
|
switch (name) {
|
|
|
|
case "audioMuted":
|
|
|
|
getter = () => false;
|
|
|
|
break;
|
|
|
|
case "contentTitle":
|
|
|
|
getter = () => SessionStore.getLazyTabValue(aTab, "title");
|
|
|
|
break;
|
|
|
|
case "currentURI":
|
|
|
|
getter = () => {
|
|
|
|
let url = SessionStore.getLazyTabValue(aTab, "url");
|
|
|
|
return Services.io.newURI(url);
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case "didStartLoadSinceLastUserTyping":
|
|
|
|
getter = () => () => false;
|
|
|
|
break;
|
|
|
|
case "fullZoom":
|
|
|
|
case "textZoom":
|
|
|
|
getter = () => 1;
|
|
|
|
break;
|
|
|
|
case "getTabBrowser":
|
|
|
|
getter = () => () => this;
|
|
|
|
break;
|
|
|
|
case "isRemoteBrowser":
|
|
|
|
getter = () => browser.getAttribute("remote") == "true";
|
|
|
|
break;
|
|
|
|
case "permitUnload":
|
|
|
|
getter = () => () => ({ permitUnload: true, timedOut: false });
|
|
|
|
break;
|
|
|
|
case "reload":
|
|
|
|
case "reloadWithFlags":
|
|
|
|
getter = () =>
|
|
|
|
params => {
|
|
|
|
// Wait for load handler to be instantiated before
|
|
|
|
// initializing the reload.
|
|
|
|
aTab.addEventListener("SSTabRestoring", () => {
|
|
|
|
browser[name](params);
|
|
|
|
}, { once: true });
|
|
|
|
gBrowser._insertBrowser(aTab);
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case "resumeMedia":
|
|
|
|
getter = () =>
|
|
|
|
() => {
|
|
|
|
// No need to insert a browser, so we just call the browser's
|
|
|
|
// method.
|
|
|
|
aTab.addEventListener("SSTabRestoring", () => {
|
|
|
|
browser[name]();
|
|
|
|
}, { once: true });
|
|
|
|
};
|
|
|
|
break;
|
|
|
|
case "userTypedValue":
|
|
|
|
case "userTypedClear":
|
|
|
|
case "mediaBlocked":
|
|
|
|
getter = () => SessionStore.getLazyTabValue(aTab, name);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
getter = () => {
|
|
|
|
if (AppConstants.NIGHTLY_BUILD) {
|
|
|
|
let message =
|
|
|
|
`[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
|
|
|
|
console.log(message + new Error().stack);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this._insertBrowser(aTab);
|
|
|
|
return browser[name];
|
|
|
|
};
|
|
|
|
setter = value => {
|
|
|
|
if (AppConstants.NIGHTLY_BUILD) {
|
|
|
|
let message =
|
|
|
|
`[bug 1345098] Lazy browser prematurely inserted via '${name}' property access:\n`;
|
|
|
|
console.log(message + new Error().stack);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this._insertBrowser(aTab);
|
|
|
|
return browser[name] = value;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
Object.defineProperty(browser, name, {
|
|
|
|
get: getter,
|
|
|
|
set: setter,
|
|
|
|
configurable: true,
|
|
|
|
enumerable: true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_insertBrowser(aTab, aInsertedOnTabCreation) {
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
// If browser is already inserted or window is closed don't do anything.
|
|
|
|
if (aTab.linkedPanel || window.closed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let browser = aTab.linkedBrowser;
|
|
|
|
|
|
|
|
// If browser is a lazy browser, delete the substitute properties.
|
|
|
|
if (this._browserBindingProperties[0] in browser) {
|
|
|
|
for (let name of this._browserBindingProperties) {
|
|
|
|
delete browser[name];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let { uriIsAboutBlank, remoteType, usingPreloadedContent } =
|
|
|
|
aTab._browserParams;
|
|
|
|
delete aTab._browserParams;
|
|
|
|
|
|
|
|
let notificationbox = this.getNotificationBox(browser);
|
|
|
|
let uniqueId = this._generateUniquePanelID();
|
|
|
|
notificationbox.id = uniqueId;
|
|
|
|
aTab.linkedPanel = uniqueId;
|
|
|
|
|
|
|
|
// Inject the <browser> into the DOM if necessary.
|
|
|
|
if (!notificationbox.parentNode) {
|
|
|
|
// NB: this appendChild call causes us to run constructors for the
|
|
|
|
// browser element, which fires off a bunch of notifications. Some
|
|
|
|
// of those notifications can cause code to run that inspects our
|
|
|
|
// state, so it is important that the tab element is fully
|
|
|
|
// initialized by this point.
|
|
|
|
this.mPanelContainer.appendChild(notificationbox);
|
|
|
|
}
|
|
|
|
|
|
|
|
// wire up a progress listener for the new browser object.
|
|
|
|
let tabListener = this.mTabProgressListener(aTab, browser, uriIsAboutBlank, usingPreloadedContent);
|
|
|
|
const filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
|
|
|
|
.createInstance(Ci.nsIWebProgress);
|
|
|
|
filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
|
|
|
|
browser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
|
|
|
|
this._tabListeners.set(aTab, tabListener);
|
|
|
|
this._tabFilters.set(aTab, filter);
|
|
|
|
|
|
|
|
browser.droppedLinkHandler = handleDroppedLink;
|
|
|
|
|
|
|
|
// We start our browsers out as inactive, and then maintain
|
|
|
|
// activeness in the tab switcher.
|
|
|
|
browser.docShellIsActive = false;
|
|
|
|
|
|
|
|
// When addTab() is called with an URL that is not "about:blank" we
|
|
|
|
// set the "nodefaultsrc" attribute that prevents a frameLoader
|
|
|
|
// from being created as soon as the linked <browser> is inserted
|
|
|
|
// into the DOM. We thus have to register the new outerWindowID
|
|
|
|
// for non-remote browsers after we have called browser.loadURI().
|
|
|
|
if (remoteType == E10SUtils.NOT_REMOTE) {
|
|
|
|
this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
|
|
|
|
}
|
|
|
|
|
|
|
|
var evt = new CustomEvent("TabBrowserInserted", { bubbles: true, detail: { insertedOnTabCreation: aInsertedOnTabCreation } });
|
|
|
|
aTab.dispatchEvent(evt);
|
|
|
|
}
|
|
|
|
|
|
|
|
discardBrowser(aBrowser, aForceDiscard) {
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
let tab = this.getTabForBrowser(aBrowser);
|
|
|
|
|
|
|
|
let permitUnloadFlags = aForceDiscard ? aBrowser.dontPromptAndUnload : aBrowser.dontPromptAndDontUnload;
|
|
|
|
|
|
|
|
if (!tab ||
|
|
|
|
tab.selected ||
|
|
|
|
tab.closing ||
|
|
|
|
this._windowIsClosing ||
|
|
|
|
!aBrowser.isConnected ||
|
|
|
|
!aBrowser.isRemoteBrowser ||
|
|
|
|
!aBrowser.permitUnload(permitUnloadFlags).permitUnload) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set browser parameters for when browser is restored. Also remove
|
|
|
|
// listeners and set up lazy restore data in SessionStore. This must
|
|
|
|
// be done before aBrowser is destroyed and removed from the document.
|
|
|
|
tab._browserParams = {
|
|
|
|
uriIsAboutBlank: aBrowser.currentURI.spec == "about:blank",
|
|
|
|
remoteType: aBrowser.remoteType,
|
|
|
|
usingPreloadedContent: false
|
|
|
|
};
|
|
|
|
|
|
|
|
SessionStore.resetBrowserToLazyState(tab);
|
|
|
|
|
|
|
|
this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
|
|
|
|
|
|
|
|
// Remove the tab's filter and progress listener.
|
|
|
|
let filter = this._tabFilters.get(tab);
|
|
|
|
let listener = this._tabListeners.get(tab);
|
|
|
|
aBrowser.webProgress.removeProgressListener(filter);
|
|
|
|
filter.removeProgressListener(listener);
|
|
|
|
listener.destroy();
|
|
|
|
|
|
|
|
this._tabListeners.delete(tab);
|
|
|
|
this._tabFilters.delete(tab);
|
|
|
|
|
|
|
|
// Reset the findbar and remove it if it is attached to the tab.
|
|
|
|
if (tab._findBar) {
|
|
|
|
tab._findBar.close(true);
|
|
|
|
tab._findBar.remove();
|
|
|
|
delete tab._findBar;
|
|
|
|
}
|
|
|
|
|
|
|
|
aBrowser.destroy();
|
|
|
|
|
|
|
|
let notificationbox = this.getNotificationBox(aBrowser);
|
|
|
|
this.mPanelContainer.removeChild(notificationbox);
|
|
|
|
tab.removeAttribute("linkedpanel");
|
|
|
|
|
|
|
|
this._createLazyBrowser(tab);
|
|
|
|
|
|
|
|
let evt = new CustomEvent("TabBrowserDiscarded", { bubbles: true });
|
|
|
|
tab.dispatchEvent(evt);
|
|
|
|
}
|
|
|
|
|
|
|
|
// eslint-disable-next-line complexity
|
|
|
|
addTab(aURI, aReferrerURI, aCharset, aPostData, aOwner, aAllowThirdPartyFixup) {
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
|
|
var aTriggeringPrincipal;
|
|
|
|
var aReferrerPolicy;
|
|
|
|
var aFromExternal;
|
|
|
|
var aRelatedToCurrent;
|
|
|
|
var aSkipAnimation;
|
|
|
|
var aAllowMixedContent;
|
|
|
|
var aForceNotRemote;
|
|
|
|
var aPreferredRemoteType;
|
|
|
|
var aNoReferrer;
|
|
|
|
var aUserContextId;
|
|
|
|
var aEventDetail;
|
|
|
|
var aSameProcessAsFrameLoader;
|
|
|
|
var aOriginPrincipal;
|
|
|
|
var aDisallowInheritPrincipal;
|
|
|
|
var aOpener;
|
|
|
|
var aOpenerBrowser;
|
|
|
|
var aCreateLazyBrowser;
|
|
|
|
var aSkipBackgroundNotify;
|
|
|
|
var aNextTabParentId;
|
|
|
|
var aNoInitialLabel;
|
|
|
|
var aFocusUrlBar;
|
|
|
|
var aName;
|
|
|
|
if (arguments.length == 2 &&
|
|
|
|
typeof arguments[1] == "object" &&
|
|
|
|
!(arguments[1] instanceof Ci.nsIURI)) {
|
|
|
|
let params = arguments[1];
|
|
|
|
aTriggeringPrincipal = params.triggeringPrincipal;
|
|
|
|
aReferrerURI = params.referrerURI;
|
|
|
|
aReferrerPolicy = params.referrerPolicy;
|
|
|
|
aCharset = params.charset;
|
|
|
|
aPostData = params.postData;
|
|
|
|
aOwner = params.ownerTab;
|
|
|
|
aAllowThirdPartyFixup = params.allowThirdPartyFixup;
|
|
|
|
aFromExternal = params.fromExternal;
|
|
|
|
aRelatedToCurrent = params.relatedToCurrent;
|
|
|
|
aSkipAnimation = params.skipAnimation;
|
|
|
|
aAllowMixedContent = params.allowMixedContent;
|
|
|
|
aForceNotRemote = params.forceNotRemote;
|
|
|
|
aPreferredRemoteType = params.preferredRemoteType;
|
|
|
|
aNoReferrer = params.noReferrer;
|
|
|
|
aUserContextId = params.userContextId;
|
|
|
|
aEventDetail = params.eventDetail;
|
|
|
|
aSameProcessAsFrameLoader = params.sameProcessAsFrameLoader;
|
|
|
|
aOriginPrincipal = params.originPrincipal;
|
|
|
|
aDisallowInheritPrincipal = params.disallowInheritPrincipal;
|
|
|
|
aOpener = params.opener;
|
|
|
|
aOpenerBrowser = params.openerBrowser;
|
|
|
|
aCreateLazyBrowser = params.createLazyBrowser;
|
|
|
|
aSkipBackgroundNotify = params.skipBackgroundNotify;
|
|
|
|
aNextTabParentId = params.nextTabParentId;
|
|
|
|
aNoInitialLabel = params.noInitialLabel;
|
|
|
|
aFocusUrlBar = params.focusUrlBar;
|
|
|
|
aName = params.name;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we're adding tabs, we're past interrupt mode, ditch the owner
|
|
|
|
if (this.mCurrentTab.owner)
|
|
|
|
this.mCurrentTab.owner = null;
|
|
|
|
|
|
|
|
// Find the tab that opened this one, if any. This is used for
|
|
|
|
// determining positioning, and inherited attributes such as the
|
|
|
|
// user context ID.
|
|
|
|
//
|
|
|
|
// If we have a browser opener (which is usually the browser
|
|
|
|
// element from a remote window.open() call), use that.
|
|
|
|
//
|
|
|
|
// Otherwise, if the tab is related to the current tab (e.g.,
|
|
|
|
// because it was opened by a link click), use the selected tab as
|
|
|
|
// the owner. If aReferrerURI is set, and we don't have an
|
|
|
|
// explicit relatedToCurrent arg, we assume that the tab is
|
|
|
|
// related to the current tab, since aReferrerURI is null or
|
|
|
|
// undefined if the tab is opened from an external application or
|
|
|
|
// bookmark (i.e. somewhere other than an existing tab).
|
|
|
|
let relatedToCurrent = aRelatedToCurrent == null ? !!aReferrerURI : aRelatedToCurrent;
|
|
|
|
let openerTab = ((aOpenerBrowser && this.getTabForBrowser(aOpenerBrowser)) ||
|
|
|
|
(relatedToCurrent && this.selectedTab));
|
|
|
|
|
|
|
|
var t = document.createElementNS(NS_XUL, "tab");
|
|
|
|
|
|
|
|
t.openerTab = openerTab;
|
|
|
|
|
|
|
|
aURI = aURI || "about:blank";
|
|
|
|
let aURIObject = null;
|
|
|
|
try {
|
|
|
|
aURIObject = Services.io.newURI(aURI);
|
|
|
|
} catch (ex) { /* we'll try to fix up this URL later */ }
|
|
|
|
|
|
|
|
let lazyBrowserURI;
|
|
|
|
if (aCreateLazyBrowser && aURI != "about:blank") {
|
|
|
|
lazyBrowserURI = aURIObject;
|
|
|
|
aURI = "about:blank";
|
|
|
|
}
|
|
|
|
|
|
|
|
var uriIsAboutBlank = aURI == "about:blank";
|
|
|
|
|
|
|
|
if (!aNoInitialLabel) {
|
|
|
|
if (isBlankPageURL(aURI)) {
|
|
|
|
t.setAttribute("label", gTabBrowserBundle.GetStringFromName("tabs.emptyTabTitle"));
|
|
|
|
} else {
|
|
|
|
// Set URL as label so that the tab isn't empty initially.
|
|
|
|
this.setInitialTabTitle(t, aURI, { beforeTabOpen: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Related tab inherits current tab's user context unless a different
|
|
|
|
// usercontextid is specified
|
|
|
|
if (aUserContextId == null && openerTab) {
|
|
|
|
aUserContextId = openerTab.getAttribute("usercontextid") || 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aUserContextId) {
|
|
|
|
t.setAttribute("usercontextid", aUserContextId);
|
|
|
|
ContextualIdentityService.setTabStyle(t);
|
|
|
|
}
|
|
|
|
|
|
|
|
t.setAttribute("onerror", "this.removeAttribute('image');");
|
|
|
|
|
|
|
|
if (aSkipBackgroundNotify) {
|
|
|
|
t.setAttribute("skipbackgroundnotify", true);
|
|
|
|
}
|
|
|
|
|
|
|
|
t.className = "tabbrowser-tab";
|
|
|
|
|
|
|
|
this.tabContainer._unlockTabSizing();
|
|
|
|
|
|
|
|
// When overflowing, new tabs are scrolled into view smoothly, which
|
|
|
|
// doesn't go well together with the width transition. So we skip the
|
|
|
|
// transition in that case.
|
|
|
|
let animate = !aSkipAnimation &&
|
|
|
|
this.tabContainer.getAttribute("overflow") != "true" &&
|
|
|
|
this.animationsEnabled;
|
|
|
|
if (!animate) {
|
|
|
|
t.setAttribute("fadein", "true");
|
|
|
|
|
|
|
|
// Call _handleNewTab asynchronously as it needs to know if the
|
|
|
|
// new tab is selected.
|
|
|
|
setTimeout(function(tabContainer) {
|
|
|
|
tabContainer._handleNewTab(t);
|
|
|
|
}, 0, this.tabContainer);
|
|
|
|
}
|
|
|
|
|
|
|
|
// invalidate cache
|
|
|
|
this._visibleTabs = null;
|
|
|
|
|
|
|
|
this.tabContainer.appendChild(t);
|
|
|
|
|
|
|
|
let usingPreloadedContent = false;
|
|
|
|
let b;
|
|
|
|
|
|
|
|
try {
|
|
|
|
// If this new tab is owned by another, assert that relationship
|
|
|
|
if (aOwner)
|
|
|
|
t.owner = aOwner;
|
|
|
|
|
|
|
|
var position = this.tabs.length - 1;
|
|
|
|
t._tPos = position;
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
this.tabContainer.updateVisibility();
|
|
|
|
|
|
|
|
// If we don't have a preferred remote type, and we have a remote
|
|
|
|
// opener, use the opener's remote type.
|
|
|
|
if (!aPreferredRemoteType && aOpenerBrowser) {
|
|
|
|
aPreferredRemoteType = aOpenerBrowser.remoteType;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If URI is about:blank and we don't have a preferred remote type,
|
|
|
|
// then we need to use the referrer, if we have one, to get the
|
|
|
|
// correct remote type for the new tab.
|
|
|
|
if (uriIsAboutBlank && !aPreferredRemoteType && aReferrerURI) {
|
|
|
|
aPreferredRemoteType =
|
|
|
|
E10SUtils.getRemoteTypeForURI(aReferrerURI.spec,
|
|
|
|
gMultiProcessBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
let remoteType =
|
|
|
|
aForceNotRemote ? E10SUtils.NOT_REMOTE :
|
|
|
|
E10SUtils.getRemoteTypeForURI(aURI, gMultiProcessBrowser,
|
|
|
|
aPreferredRemoteType);
|
|
|
|
|
|
|
|
// If we open a new tab with the newtab URL in the default
|
|
|
|
// userContext, check if there is a preloaded browser ready.
|
|
|
|
// Private windows are not included because both the label and the
|
|
|
|
// icon for the tab would be set incorrectly (see bug 1195981).
|
|
|
|
if (aURI == BROWSER_NEW_TAB_URL &&
|
|
|
|
!aUserContextId &&
|
|
|
|
!PrivateBrowsingUtils.isWindowPrivate(window)) {
|
|
|
|
b = this._getPreloadedBrowser();
|
|
|
|
if (b) {
|
|
|
|
usingPreloadedContent = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!b) {
|
|
|
|
// No preloaded browser found, create one.
|
|
|
|
b = this._createBrowser({
|
|
|
|
remoteType,
|
|
|
|
uriIsAboutBlank,
|
|
|
|
userContextId: aUserContextId,
|
|
|
|
sameProcessAsFrameLoader: aSameProcessAsFrameLoader,
|
|
|
|
openerWindow: aOpener,
|
|
|
|
nextTabParentId: aNextTabParentId,
|
|
|
|
name: aName
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
t.linkedBrowser = b;
|
|
|
|
|
|
|
|
if (aFocusUrlBar) {
|
|
|
|
b._urlbarFocused = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._tabForBrowser.set(b, t);
|
|
|
|
t.permanentKey = b.permanentKey;
|
|
|
|
t._browserParams = {
|
|
|
|
uriIsAboutBlank,
|
|
|
|
remoteType,
|
|
|
|
usingPreloadedContent
|
|
|
|
};
|
|
|
|
|
|
|
|
// If the caller opts in, create a lazy browser.
|
|
|
|
if (aCreateLazyBrowser) {
|
|
|
|
this._createLazyBrowser(t);
|
|
|
|
|
|
|
|
if (lazyBrowserURI) {
|
|
|
|
// Lazy browser must be explicitly registered so tab will appear as
|
|
|
|
// a switch-to-tab candidate in autocomplete.
|
|
|
|
this._unifiedComplete.registerOpenPage(lazyBrowserURI, aUserContextId);
|
|
|
|
b.registeredOpenURI = lazyBrowserURI;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
this._insertBrowser(t, true);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
Cu.reportError("Failed to create tab");
|
|
|
|
Cu.reportError(e);
|
|
|
|
t.remove();
|
|
|
|
if (t.linkedBrowser) {
|
|
|
|
this._tabFilters.delete(t);
|
|
|
|
this._tabListeners.delete(t);
|
|
|
|
let notificationbox = this.getNotificationBox(t.linkedBrowser);
|
|
|
|
notificationbox.remove();
|
|
|
|
}
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Hack to ensure that the about:newtab favicon is loaded
|
|
|
|
// instantaneously, to avoid flickering and improve perceived performance.
|
|
|
|
if (aURI == "about:newtab") {
|
|
|
|
this.setIcon(t, "chrome://branding/content/icon32.png");
|
|
|
|
} else if (aURI == "about:privatebrowsing") {
|
|
|
|
this.setIcon(t, "chrome://browser/skin/privatebrowsing/favicon.svg");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dispatch a new tab notification. We do this once we're
|
|
|
|
// entirely done, so that things are in a consistent state
|
|
|
|
// even if the event listener opens or closes tabs.
|
|
|
|
var detail = aEventDetail || {};
|
|
|
|
var evt = new CustomEvent("TabOpen", { bubbles: true, detail });
|
|
|
|
t.dispatchEvent(evt);
|
|
|
|
|
|
|
|
if (!usingPreloadedContent && aOriginPrincipal && aURI) {
|
|
|
|
let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
|
|
|
|
// Unless we know for sure we're not inheriting principals,
|
|
|
|
// force the about:blank viewer to have the right principal:
|
|
|
|
if (!aURIObject ||
|
|
|
|
(doGetProtocolFlags(aURIObject) & URI_INHERITS_SECURITY_CONTEXT)) {
|
|
|
|
b.createAboutBlankContentViewer(aOriginPrincipal);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we didn't swap docShells with a preloaded browser
|
|
|
|
// then let's just continue loading the page normally.
|
|
|
|
if (!usingPreloadedContent && (!uriIsAboutBlank || aDisallowInheritPrincipal)) {
|
|
|
|
// pretend the user typed this so it'll be available till
|
|
|
|
// the document successfully loads
|
|
|
|
if (aURI && !gInitialPages.includes(aURI))
|
|
|
|
b.userTypedValue = aURI;
|
|
|
|
|
|
|
|
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
|
|
|
|
if (aAllowThirdPartyFixup) {
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
|
|
|
|
}
|
|
|
|
if (aFromExternal)
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL;
|
|
|
|
if (aAllowMixedContent)
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
|
|
|
|
if (aDisallowInheritPrincipal)
|
|
|
|
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
|
|
|
|
try {
|
|
|
|
b.loadURIWithFlags(aURI, {
|
|
|
|
flags,
|
|
|
|
triggeringPrincipal: aTriggeringPrincipal,
|
|
|
|
referrerURI: aNoReferrer ? null : aReferrerURI,
|
|
|
|
referrerPolicy: aReferrerPolicy,
|
|
|
|
charset: aCharset,
|
|
|
|
postData: aPostData,
|
|
|
|
});
|
|
|
|
} catch (ex) {
|
|
|
|
Cu.reportError(ex);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're opening a tab related to the an existing tab, move it
|
|
|
|
// to a position after that tab.
|
|
|
|
if (openerTab &&
|
|
|
|
Services.prefs.getBoolPref("browser.tabs.insertRelatedAfterCurrent")) {
|
|
|
|
|
|
|
|
let lastRelatedTab = this._lastRelatedTabMap.get(openerTab);
|
|
|
|
let newTabPos = (lastRelatedTab || openerTab)._tPos + 1;
|
|
|
|
if (lastRelatedTab)
|
|
|
|
lastRelatedTab.owner = null;
|
|
|
|
else
|
|
|
|
t.owner = openerTab;
|
|
|
|
this.moveTabTo(t, newTabPos, true);
|
|
|
|
this._lastRelatedTabMap.set(openerTab, t);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This field is updated regardless if we actually animate
|
|
|
|
// since it's important that we keep this count correct in all cases.
|
|
|
|
this.tabAnimationsInProgress++;
|
|
|
|
|
|
|
|
if (animate) {
|
|
|
|
requestAnimationFrame(function() {
|
|
|
|
// kick the animation off
|
|
|
|
t.setAttribute("fadein", "true");
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return t;
|
|
|
|
}
|
|
|
|
|
|
|
|
warnAboutClosingTabs(aCloseTabs, aTab) {
|
|
|
|
var tabsToClose;
|
|
|
|
switch (aCloseTabs) {
|
|
|
|
case this.closingTabsEnum.ALL:
|
|
|
|
tabsToClose = this.tabs.length - this._removingTabs.length -
|
|
|
|
gBrowser._numPinnedTabs;
|
|
|
|
break;
|
|
|
|
case this.closingTabsEnum.OTHER:
|
|
|
|
tabsToClose = this.visibleTabs.length - 1 - gBrowser._numPinnedTabs;
|
|
|
|
break;
|
|
|
|
case this.closingTabsEnum.TO_END:
|
|
|
|
if (!aTab)
|
|
|
|
throw new Error("Required argument missing: aTab");
|
|
|
|
|
|
|
|
tabsToClose = this.getTabsToTheEndFrom(aTab).length;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error("Invalid argument: " + aCloseTabs);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tabsToClose <= 1)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
const pref = aCloseTabs == this.closingTabsEnum.ALL ?
|
|
|
|
"browser.tabs.warnOnClose" : "browser.tabs.warnOnCloseOtherTabs";
|
|
|
|
var shouldPrompt = Services.prefs.getBoolPref(pref);
|
|
|
|
if (!shouldPrompt)
|
|
|
|
return true;
|
|
|
|
|
|
|
|
var ps = Services.prompt;
|
|
|
|
|
|
|
|
// default to true: if it were false, we wouldn't get this far
|
|
|
|
var warnOnClose = { value: true };
|
|
|
|
|
|
|
|
// focus the window before prompting.
|
|
|
|
// this will raise any minimized window, which will
|
|
|
|
// make it obvious which window the prompt is for and will
|
|
|
|
// solve the problem of windows "obscuring" the prompt.
|
|
|
|
// see bug #350299 for more details
|
|
|
|
window.focus();
|
|
|
|
var warningMessage =
|
|
|
|
PluralForm.get(tabsToClose, gTabBrowserBundle.GetStringFromName("tabs.closeWarningMultiple"))
|
|
|
|
.replace("#1", tabsToClose);
|
|
|
|
var buttonPressed =
|
|
|
|
ps.confirmEx(window,
|
|
|
|
gTabBrowserBundle.GetStringFromName("tabs.closeWarningTitle"),
|
|
|
|
warningMessage,
|
|
|
|
(ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
|
|
|
|
(ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1),
|
|
|
|
gTabBrowserBundle.GetStringFromName("tabs.closeButtonMultiple"),
|
|
|
|
null, null,
|
|
|
|
aCloseTabs == this.closingTabsEnum.ALL ?
|
|
|
|
gTabBrowserBundle.GetStringFromName("tabs.closeWarningPromptMe") : null,
|
|
|
|
warnOnClose);
|
|
|
|
var reallyClose = (buttonPressed == 0);
|
|
|
|
|
|
|
|
// don't set the pref unless they press OK and it's false
|
|
|
|
if (aCloseTabs == this.closingTabsEnum.ALL && reallyClose && !warnOnClose.value)
|
|
|
|
Services.prefs.setBoolPref(pref, false);
|
|
|
|
|
|
|
|
return reallyClose;
|
|
|
|
}
|
|
|
|
|
|
|
|
getTabsToTheEndFrom(aTab) {
|
|
|
|
let tabsToEnd = [];
|
|
|
|
let tabs = this.visibleTabs;
|
|
|
|
for (let i = tabs.length - 1; i >= 0; --i) {
|
|
|
|
if (tabs[i] == aTab || tabs[i].pinned) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
tabsToEnd.push(tabs[i]);
|
|
|
|
}
|
|
|
|
return tabsToEnd;
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTabsToTheEndFrom(aTab, aParams) {
|
|
|
|
if (!this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab))
|
|
|
|
return;
|
|
|
|
|
|
|
|
let removeTab = tab => {
|
|
|
|
// Avoid changing the selected browser several times.
|
|
|
|
if (tab.selected)
|
|
|
|
this.selectedTab = aTab;
|
|
|
|
|
|
|
|
this.removeTab(tab, aParams);
|
|
|
|
};
|
|
|
|
|
|
|
|
let tabs = this.getTabsToTheEndFrom(aTab);
|
|
|
|
let tabsWithBeforeUnload = [];
|
|
|
|
for (let i = tabs.length - 1; i >= 0; --i) {
|
|
|
|
let tab = tabs[i];
|
|
|
|
if (this._hasBeforeUnload(tab))
|
|
|
|
tabsWithBeforeUnload.push(tab);
|
|
|
|
else
|
|
|
|
removeTab(tab);
|
|
|
|
}
|
|
|
|
tabsWithBeforeUnload.forEach(removeTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeAllTabsBut(aTab) {
|
|
|
|
if (!this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let tabs = this.visibleTabs.reverse();
|
|
|
|
this.selectedTab = aTab;
|
|
|
|
|
|
|
|
let tabsWithBeforeUnload = [];
|
|
|
|
for (let i = tabs.length - 1; i >= 0; --i) {
|
|
|
|
let tab = tabs[i];
|
|
|
|
if (tab != aTab && !tab.pinned) {
|
|
|
|
if (this._hasBeforeUnload(tab))
|
|
|
|
tabsWithBeforeUnload.push(tab);
|
|
|
|
else
|
|
|
|
this.removeTab(tab, { animate: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (let tab of tabsWithBeforeUnload) {
|
|
|
|
this.removeTab(tab, { animate: true });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
removeCurrentTab(aParams) {
|
|
|
|
this.removeTab(this.mCurrentTab, aParams);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTab(aTab, aParams) {
|
|
|
|
if (aParams) {
|
|
|
|
var animate = aParams.animate;
|
|
|
|
var byMouse = aParams.byMouse;
|
|
|
|
var skipPermitUnload = aParams.skipPermitUnload;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Telemetry stopwatches may already be running if removeTab gets
|
|
|
|
// called again for an already closing tab.
|
|
|
|
if (!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_ANIM_MS", aTab) &&
|
|
|
|
!TelemetryStopwatch.running("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab)) {
|
|
|
|
// Speculatevely start both stopwatches now. We'll cancel one of
|
|
|
|
// the two later depending on whether we're animating.
|
|
|
|
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
|
|
|
|
TelemetryStopwatch.start("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
|
|
|
|
}
|
|
|
|
window.maybeRecordAbandonmentTelemetry(aTab, "tabClosed");
|
|
|
|
|
|
|
|
// Handle requests for synchronously removing an already
|
|
|
|
// asynchronously closing tab.
|
|
|
|
if (!animate &&
|
|
|
|
aTab.closing) {
|
|
|
|
this._endRemoveTab(aTab);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
|
|
|
|
|
|
|
|
if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload)) {
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
|
|
|
|
this.tabContainer._lockTabSizing(aTab);
|
|
|
|
else
|
|
|
|
this.tabContainer._unlockTabSizing();
|
|
|
|
|
|
|
|
if (!animate /* the caller didn't opt in */ ||
|
|
|
|
isLastTab ||
|
|
|
|
aTab.pinned ||
|
|
|
|
aTab.hidden ||
|
|
|
|
this._removingTabs.length > 3 /* don't want lots of concurrent animations */ ||
|
|
|
|
aTab.getAttribute("fadein") != "true" /* fade-in transition hasn't been triggered yet */ ||
|
|
|
|
window.getComputedStyle(aTab).maxWidth == "0.1px" /* fade-in transition hasn't moved yet */ ||
|
|
|
|
!this.animationsEnabled) {
|
|
|
|
// We're not animating, so we can cancel the animation stopwatch.
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
|
|
|
|
this._endRemoveTab(aTab);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We're animating, so we can cancel the non-animation stopwatch.
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
|
|
|
|
|
|
|
|
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
|
|
|
|
aTab.removeAttribute("fadein");
|
|
|
|
aTab.removeAttribute("bursting");
|
|
|
|
|
|
|
|
setTimeout(function(tab, tabbrowser) {
|
|
|
|
if (tab.parentNode &&
|
|
|
|
window.getComputedStyle(tab).maxWidth == "0.1px") {
|
|
|
|
NS_ASSERT(false, "Giving up waiting for the tab closing animation to finish (bug 608589)");
|
|
|
|
tabbrowser._endRemoveTab(tab);
|
|
|
|
}
|
|
|
|
}, 3000, aTab, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
_hasBeforeUnload(aTab) {
|
|
|
|
let browser = aTab.linkedBrowser;
|
|
|
|
return browser.isRemoteBrowser && browser.frameLoader &&
|
|
|
|
browser.frameLoader.tabParent &&
|
|
|
|
browser.frameLoader.tabParent.hasBeforeUnload;
|
|
|
|
}
|
|
|
|
|
|
|
|
_beginRemoveTab(aTab, aAdoptedByTab, aCloseWindowWithLastTab, aCloseWindowFastpath, aSkipPermitUnload) {
|
|
|
|
if (aTab.closing ||
|
|
|
|
this._windowIsClosing)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
var browser = this.getBrowserForTab(aTab);
|
|
|
|
if (!aSkipPermitUnload && !aAdoptedByTab &&
|
|
|
|
aTab.linkedPanel && !aTab._pendingPermitUnload &&
|
|
|
|
(!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))) {
|
|
|
|
TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
|
|
|
|
|
|
|
|
// We need to block while calling permitUnload() because it
|
|
|
|
// processes the event queue and may lead to another removeTab()
|
|
|
|
// call before permitUnload() returns.
|
|
|
|
aTab._pendingPermitUnload = true;
|
|
|
|
let { permitUnload, timedOut } = browser.permitUnload();
|
|
|
|
delete aTab._pendingPermitUnload;
|
|
|
|
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
|
|
|
|
|
|
|
|
// If we were closed during onbeforeunload, we return false now
|
|
|
|
// so we don't (try to) close the same tab again. Of course, we
|
|
|
|
// also stop if the unload was cancelled by the user:
|
|
|
|
if (aTab.closing || (!timedOut && !permitUnload)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this._blurTab(aTab);
|
|
|
|
|
|
|
|
var closeWindow = false;
|
|
|
|
var newTab = false;
|
|
|
|
if (this.tabs.length - this._removingTabs.length == 1) {
|
|
|
|
closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
|
|
|
|
!window.toolbar.visible ||
|
|
|
|
Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
|
|
|
|
|
|
|
|
if (closeWindow) {
|
|
|
|
// We've already called beforeunload on all the relevant tabs if we get here,
|
|
|
|
// so avoid calling it again:
|
|
|
|
window.skipNextCanClose = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Closing the tab and replacing it with a blank one is notably slower
|
|
|
|
// than closing the window right away. If the caller opts in, take
|
|
|
|
// the fast path.
|
|
|
|
if (closeWindow &&
|
|
|
|
aCloseWindowFastpath &&
|
|
|
|
this._removingTabs.length == 0) {
|
|
|
|
// This call actually closes the window, unless the user
|
|
|
|
// cancels the operation. We are finished here in both cases.
|
|
|
|
this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
newTab = true;
|
|
|
|
}
|
|
|
|
aTab._endRemoveArgs = [closeWindow, newTab];
|
|
|
|
|
|
|
|
// swapBrowsersAndCloseOther will take care of closing the window without animation.
|
|
|
|
if (closeWindow && aAdoptedByTab) {
|
|
|
|
// Remove the tab's filter to avoid leaking.
|
|
|
|
if (aTab.linkedPanel) {
|
|
|
|
this._tabFilters.delete(aTab);
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!aTab._fullyOpen) {
|
|
|
|
// If the opening tab animation hasn't finished before we start closing the
|
|
|
|
// tab, decrement the animation count since _handleNewTab will not get called.
|
|
|
|
this.tabAnimationsInProgress--;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tabAnimationsInProgress++;
|
|
|
|
|
|
|
|
// Mute audio immediately to improve perceived speed of tab closure.
|
|
|
|
if (!aAdoptedByTab && aTab.hasAttribute("soundplaying")) {
|
|
|
|
// Don't persist the muted state as this wasn't a user action.
|
|
|
|
// This lets undo-close-tab return it to an unmuted state.
|
|
|
|
aTab.linkedBrowser.mute(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
aTab.closing = true;
|
|
|
|
this._removingTabs.push(aTab);
|
|
|
|
this._visibleTabs = null; // invalidate cache
|
|
|
|
|
|
|
|
// Invalidate hovered tab state tracking for this closing tab.
|
|
|
|
if (this.tabContainer._hoveredTab == aTab)
|
|
|
|
aTab._mouseleave();
|
|
|
|
|
|
|
|
if (newTab)
|
|
|
|
this.addTab(BROWSER_NEW_TAB_URL, { skipAnimation: true });
|
|
|
|
else
|
|
|
|
this.tabContainer.updateVisibility();
|
|
|
|
|
|
|
|
// We're committed to closing the tab now.
|
|
|
|
// Dispatch a notification.
|
|
|
|
// We dispatch it before any teardown so that event listeners can
|
|
|
|
// inspect the tab that's about to close.
|
|
|
|
var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } });
|
|
|
|
aTab.dispatchEvent(evt);
|
|
|
|
|
|
|
|
if (aTab.linkedPanel) {
|
|
|
|
if (!aAdoptedByTab && !gMultiProcessBrowser) {
|
|
|
|
// Prevent this tab from showing further dialogs, since we're closing it
|
|
|
|
var windowUtils = browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
|
|
windowUtils.disableDialogs();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the tab's filter and progress listener.
|
|
|
|
const filter = this._tabFilters.get(aTab);
|
|
|
|
|
|
|
|
browser.webProgress.removeProgressListener(filter);
|
|
|
|
|
|
|
|
const listener = this._tabListeners.get(aTab);
|
|
|
|
filter.removeProgressListener(listener);
|
|
|
|
listener.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (browser.registeredOpenURI && !aAdoptedByTab) {
|
|
|
|
this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
|
|
|
|
browser.getAttribute("usercontextid") || 0);
|
|
|
|
delete browser.registeredOpenURI;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We are no longer the primary content area.
|
|
|
|
browser.removeAttribute("primary");
|
|
|
|
|
|
|
|
// Remove this tab as the owner of any other tabs, since it's going away.
|
|
|
|
for (let tab of this.tabs) {
|
|
|
|
if ("owner" in tab && tab.owner == aTab)
|
|
|
|
// |tab| is a child of the tab we're removing, make it an orphan
|
|
|
|
tab.owner = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
_endRemoveTab(aTab) {
|
|
|
|
if (!aTab || !aTab._endRemoveArgs)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var [aCloseWindow, aNewTab] = aTab._endRemoveArgs;
|
|
|
|
aTab._endRemoveArgs = null;
|
|
|
|
|
|
|
|
if (this._windowIsClosing) {
|
|
|
|
aCloseWindow = false;
|
|
|
|
aNewTab = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tabAnimationsInProgress--;
|
|
|
|
|
|
|
|
this._lastRelatedTabMap = new WeakMap();
|
|
|
|
|
|
|
|
// update the UI early for responsiveness
|
|
|
|
aTab.collapsed = true;
|
|
|
|
this._blurTab(aTab);
|
|
|
|
|
|
|
|
this._removingTabs.splice(this._removingTabs.indexOf(aTab), 1);
|
|
|
|
|
|
|
|
if (aCloseWindow) {
|
|
|
|
this._windowIsClosing = true;
|
|
|
|
while (this._removingTabs.length)
|
|
|
|
this._endRemoveTab(this._removingTabs[0]);
|
|
|
|
} else if (!this._windowIsClosing) {
|
|
|
|
if (aNewTab)
|
|
|
|
focusAndSelectUrlBar();
|
|
|
|
|
|
|
|
// workaround for bug 345399
|
|
|
|
this.tabContainer.arrowScrollbox._updateScrollButtonsDisabledState();
|
|
|
|
}
|
|
|
|
|
|
|
|
// We're going to remove the tab and the browser now.
|
|
|
|
this._tabFilters.delete(aTab);
|
|
|
|
this._tabListeners.delete(aTab);
|
|
|
|
|
|
|
|
var browser = this.getBrowserForTab(aTab);
|
|
|
|
|
|
|
|
if (aTab.linkedPanel) {
|
|
|
|
this._outerWindowIDBrowserMap.delete(browser.outerWindowID);
|
|
|
|
|
|
|
|
// Because of the way XBL works (fields just set JS
|
|
|
|
// properties on the element) and the code we have in place
|
|
|
|
// to preserve the JS objects for any elements that have
|
|
|
|
// JS properties set on them, the browser element won't be
|
|
|
|
// destroyed until the document goes away. So we force a
|
|
|
|
// cleanup ourselves.
|
|
|
|
// This has to happen before we remove the child so that the
|
|
|
|
// XBL implementation of nsIObserver still works.
|
|
|
|
browser.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
var wasPinned = aTab.pinned;
|
|
|
|
|
|
|
|
// Remove the tab ...
|
|
|
|
this.tabContainer.removeChild(aTab);
|
|
|
|
|
|
|
|
// ... and fix up the _tPos properties immediately.
|
|
|
|
for (let i = aTab._tPos; i < this.tabs.length; i++)
|
|
|
|
this.tabs[i]._tPos = i;
|
|
|
|
|
|
|
|
if (!this._windowIsClosing) {
|
|
|
|
if (wasPinned)
|
|
|
|
this.tabContainer._positionPinnedTabs();
|
|
|
|
|
|
|
|
// update tab close buttons state
|
|
|
|
this.tabContainer._updateCloseButtons();
|
|
|
|
|
|
|
|
setTimeout(function(tabs) {
|
|
|
|
tabs._lastTabClosedByMouse = false;
|
|
|
|
}, 0, this.tabContainer);
|
|
|
|
}
|
|
|
|
|
|
|
|
// update tab positional properties and attributes
|
|
|
|
this.selectedTab._selected = true;
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
// Removing the panel requires fixing up selectedPanel immediately
|
|
|
|
// (see below), which would be hindered by the potentially expensive
|
|
|
|
// browser removal. So we remove the browser and the panel in two
|
|
|
|
// steps.
|
|
|
|
|
|
|
|
var panel = this.getNotificationBox(browser);
|
|
|
|
|
|
|
|
// In the multi-process case, it's possible an asynchronous tab switch
|
|
|
|
// is still underway. If so, then it's possible that the last visible
|
|
|
|
// browser is the one we're in the process of removing. There's the
|
|
|
|
// risk of displaying preloaded browsers that are at the end of the
|
|
|
|
// deck if we remove the browser before the switch is complete, so
|
|
|
|
// we alert the switcher in order to show a spinner instead.
|
|
|
|
if (this._switcher) {
|
|
|
|
this._switcher.onTabRemoved(aTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This will unload the document. An unload handler could remove
|
|
|
|
// dependant tabs, so it's important that the tabbrowser is now in
|
|
|
|
// a consistent state (tab removed, tab positions updated, etc.).
|
|
|
|
browser.remove();
|
|
|
|
|
|
|
|
// Release the browser in case something is erroneously holding a
|
|
|
|
// reference to the tab after its removal.
|
|
|
|
this._tabForBrowser.delete(aTab.linkedBrowser);
|
|
|
|
aTab.linkedBrowser = null;
|
|
|
|
|
|
|
|
panel.remove();
|
|
|
|
|
|
|
|
// closeWindow might wait an arbitrary length of time if we're supposed
|
|
|
|
// to warn about closing the window, so we'll just stop the tab close
|
|
|
|
// stopwatches here instead.
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_CLOSE_TIME_ANIM_MS", aTab,
|
|
|
|
true /* aCanceledOkay */ );
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab,
|
|
|
|
true /* aCanceledOkay */ );
|
|
|
|
|
|
|
|
if (aCloseWindow)
|
|
|
|
this._windowIsClosing = closeWindow(true, window.warnAboutClosingWindow);
|
|
|
|
}
|
|
|
|
|
|
|
|
_findTabToBlurTo(aTab) {
|
|
|
|
if (!aTab.selected) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aTab.owner &&
|
|
|
|
!aTab.owner.hidden &&
|
|
|
|
!aTab.owner.closing &&
|
|
|
|
Services.prefs.getBoolPref("browser.tabs.selectOwnerOnClose")) {
|
|
|
|
return aTab.owner;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Switch to a visible tab unless there aren't any others remaining
|
|
|
|
let remainingTabs = this.visibleTabs;
|
|
|
|
let numTabs = remainingTabs.length;
|
|
|
|
if (numTabs == 0 || numTabs == 1 && remainingTabs[0] == aTab) {
|
|
|
|
remainingTabs = Array.filter(this.tabs, function(tab) {
|
|
|
|
return !tab.closing;
|
|
|
|
}, this);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try to find a remaining tab that comes after the given tab
|
|
|
|
let tab = aTab;
|
|
|
|
do {
|
|
|
|
tab = tab.nextSibling;
|
|
|
|
} while (tab && !remainingTabs.includes(tab));
|
|
|
|
|
|
|
|
if (!tab) {
|
|
|
|
tab = aTab;
|
|
|
|
|
|
|
|
do {
|
|
|
|
tab = tab.previousSibling;
|
|
|
|
} while (tab && !remainingTabs.includes(tab));
|
|
|
|
}
|
|
|
|
|
|
|
|
return tab;
|
|
|
|
}
|
|
|
|
|
|
|
|
_blurTab(aTab) {
|
|
|
|
this.selectedTab = this._findTabToBlurTo(aTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
|
|
|
|
// Do not allow transfering a private tab to a non-private window
|
|
|
|
// and vice versa.
|
|
|
|
if (PrivateBrowsingUtils.isWindowPrivate(window) !=
|
|
|
|
PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal))
|
|
|
|
return;
|
|
|
|
|
|
|
|
let ourBrowser = this.getBrowserForTab(aOurTab);
|
|
|
|
let otherBrowser = aOtherTab.linkedBrowser;
|
|
|
|
|
|
|
|
// Can't swap between chrome and content processes.
|
|
|
|
if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Keep the userContextId if set on other browser
|
|
|
|
if (otherBrowser.hasAttribute("usercontextid")) {
|
|
|
|
ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
|
|
|
|
}
|
|
|
|
|
|
|
|
// That's gBrowser for the other window, not the tab's browser!
|
|
|
|
var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
|
|
|
|
var isPending = aOtherTab.hasAttribute("pending");
|
|
|
|
|
|
|
|
let otherTabListener = remoteBrowser._tabListeners.get(aOtherTab);
|
|
|
|
let stateFlags = otherTabListener.mStateFlags;
|
|
|
|
|
|
|
|
// Expedite the removal of the icon if it was already scheduled.
|
|
|
|
if (aOtherTab._soundPlayingAttrRemovalTimer) {
|
|
|
|
clearTimeout(aOtherTab._soundPlayingAttrRemovalTimer);
|
|
|
|
aOtherTab._soundPlayingAttrRemovalTimer = 0;
|
|
|
|
aOtherTab.removeAttribute("soundplaying");
|
|
|
|
remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, start teardown of the other browser. Make sure to not
|
|
|
|
// fire the beforeunload event in the process. Close the other
|
|
|
|
// window if this was its last tab.
|
|
|
|
if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true))
|
|
|
|
return;
|
|
|
|
|
|
|
|
// If this is the last tab of the window, hide the window
|
|
|
|
// immediately without animation before the docshell swap, to avoid
|
|
|
|
// about:blank being painted.
|
|
|
|
let [closeWindow] = aOtherTab._endRemoveArgs;
|
|
|
|
if (closeWindow) {
|
|
|
|
let win = aOtherTab.ownerGlobal;
|
|
|
|
let dwu = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils);
|
|
|
|
dwu.suppressAnimation(true);
|
|
|
|
// Only suppressing window animations isn't enough to avoid
|
|
|
|
// an empty content area being painted.
|
|
|
|
let baseWin = win.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDocShell)
|
|
|
|
.QueryInterface(Ci.nsIDocShellTreeItem)
|
|
|
|
.treeOwner
|
|
|
|
.QueryInterface(Ci.nsIBaseWindow);
|
|
|
|
baseWin.visibility = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let modifiedAttrs = [];
|
|
|
|
if (aOtherTab.hasAttribute("muted")) {
|
|
|
|
aOurTab.setAttribute("muted", "true");
|
|
|
|
aOurTab.muteReason = aOtherTab.muteReason;
|
|
|
|
ourBrowser.mute();
|
|
|
|
modifiedAttrs.push("muted");
|
|
|
|
}
|
|
|
|
if (aOtherTab.hasAttribute("soundplaying")) {
|
|
|
|
aOurTab.setAttribute("soundplaying", "true");
|
|
|
|
modifiedAttrs.push("soundplaying");
|
|
|
|
}
|
|
|
|
if (aOtherTab.hasAttribute("usercontextid")) {
|
|
|
|
aOurTab.setUserContextId(aOtherTab.getAttribute("usercontextid"));
|
|
|
|
modifiedAttrs.push("usercontextid");
|
|
|
|
}
|
|
|
|
if (aOtherTab.hasAttribute("sharing")) {
|
|
|
|
aOurTab.setAttribute("sharing", aOtherTab.getAttribute("sharing"));
|
|
|
|
modifiedAttrs.push("sharing");
|
|
|
|
aOurTab._sharingState = aOtherTab._sharingState;
|
|
|
|
webrtcUI.swapBrowserForNotification(otherBrowser, ourBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
SitePermissions.copyTemporaryPermissions(otherBrowser, ourBrowser);
|
|
|
|
|
|
|
|
// If the other tab is pending (i.e. has not been restored, yet)
|
|
|
|
// then do not switch docShells but retrieve the other tab's state
|
|
|
|
// and apply it to our tab.
|
|
|
|
if (isPending) {
|
|
|
|
SessionStore.setTabState(aOurTab, SessionStore.getTabState(aOtherTab));
|
|
|
|
|
|
|
|
// Make sure to unregister any open URIs.
|
|
|
|
this._swapRegisteredOpenURIs(ourBrowser, otherBrowser);
|
|
|
|
} else {
|
|
|
|
// Workarounds for bug 458697
|
|
|
|
// Icon might have been set on DOMLinkAdded, don't override that.
|
|
|
|
if (!ourBrowser.mIconURL && otherBrowser.mIconURL)
|
|
|
|
this.setIcon(aOurTab, otherBrowser.mIconURL, otherBrowser.contentPrincipal, otherBrowser.contentRequestContextID);
|
|
|
|
var isBusy = aOtherTab.hasAttribute("busy");
|
|
|
|
if (isBusy) {
|
|
|
|
aOurTab.setAttribute("busy", "true");
|
|
|
|
modifiedAttrs.push("busy");
|
|
|
|
if (aOurTab.selected)
|
|
|
|
this.mIsBusy = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._swapBrowserDocShells(aOurTab, otherBrowser, Ci.nsIBrowser.SWAP_DEFAULT, stateFlags);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unregister the previously opened URI
|
|
|
|
if (otherBrowser.registeredOpenURI) {
|
|
|
|
this._unifiedComplete.unregisterOpenPage(otherBrowser.registeredOpenURI,
|
|
|
|
otherBrowser.getAttribute("usercontextid") || 0);
|
|
|
|
delete otherBrowser.registeredOpenURI;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle findbar data (if any)
|
|
|
|
let otherFindBar = aOtherTab._findBar;
|
|
|
|
if (otherFindBar &&
|
|
|
|
otherFindBar.findMode == otherFindBar.FIND_NORMAL) {
|
|
|
|
let ourFindBar = this.getFindBar(aOurTab);
|
|
|
|
ourFindBar._findField.value = otherFindBar._findField.value;
|
|
|
|
if (!otherFindBar.hidden)
|
|
|
|
ourFindBar.onFindCommand();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finish tearing down the tab that's going away.
|
|
|
|
if (closeWindow) {
|
|
|
|
aOtherTab.ownerGlobal.close();
|
|
|
|
} else {
|
|
|
|
remoteBrowser._endRemoveTab(aOtherTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setTabTitle(aOurTab);
|
|
|
|
|
|
|
|
// If the tab was already selected (this happpens in the scenario
|
|
|
|
// of replaceTabWithWindow), notify onLocationChange, etc.
|
|
|
|
if (aOurTab.selected)
|
|
|
|
this.updateCurrentBrowser(true);
|
|
|
|
|
|
|
|
if (modifiedAttrs.length) {
|
|
|
|
this._tabAttrModified(aOurTab, modifiedAttrs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
swapBrowsers(aOurTab, aOtherTab, aFlags) {
|
|
|
|
let otherBrowser = aOtherTab.linkedBrowser;
|
|
|
|
let otherTabBrowser = otherBrowser.getTabBrowser();
|
|
|
|
|
|
|
|
// We aren't closing the other tab so, we also need to swap its tablisteners.
|
|
|
|
let filter = otherTabBrowser._tabFilters.get(aOtherTab);
|
|
|
|
let tabListener = otherTabBrowser._tabListeners.get(aOtherTab);
|
|
|
|
otherBrowser.webProgress.removeProgressListener(filter);
|
|
|
|
filter.removeProgressListener(tabListener);
|
|
|
|
|
|
|
|
// Perform the docshell swap through the common mechanism.
|
|
|
|
this._swapBrowserDocShells(aOurTab, otherBrowser, aFlags);
|
|
|
|
|
|
|
|
// Restore the listeners for the swapped in tab.
|
|
|
|
tabListener = otherTabBrowser.mTabProgressListener(aOtherTab, otherBrowser, false, false);
|
|
|
|
otherTabBrowser._tabListeners.set(aOtherTab, tabListener);
|
|
|
|
|
|
|
|
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
|
|
|
|
filter.addProgressListener(tabListener, notifyAll);
|
|
|
|
otherBrowser.webProgress.addProgressListener(filter, notifyAll);
|
|
|
|
}
|
|
|
|
|
|
|
|
_swapBrowserDocShells(aOurTab, aOtherBrowser, aFlags, aStateFlags) {
|
|
|
|
// aOurTab's browser needs to be inserted now if it hasn't already.
|
|
|
|
this._insertBrowser(aOurTab);
|
|
|
|
|
|
|
|
// Unhook our progress listener
|
|
|
|
const filter = this._tabFilters.get(aOurTab);
|
|
|
|
let tabListener = this._tabListeners.get(aOurTab);
|
|
|
|
let ourBrowser = this.getBrowserForTab(aOurTab);
|
|
|
|
ourBrowser.webProgress.removeProgressListener(filter);
|
|
|
|
filter.removeProgressListener(tabListener);
|
|
|
|
|
|
|
|
// Make sure to unregister any open URIs.
|
|
|
|
this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
|
|
|
|
|
|
|
|
// Unmap old outerWindowIDs.
|
|
|
|
this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID);
|
|
|
|
let remoteBrowser = aOtherBrowser.ownerGlobal.gBrowser;
|
|
|
|
if (remoteBrowser) {
|
|
|
|
remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If switcher is active, it will intercept swap events and
|
|
|
|
// react as needed.
|
|
|
|
if (!this._switcher) {
|
|
|
|
aOtherBrowser.docShellIsActive = this.shouldActivateDocShell(ourBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Swap the docshells
|
|
|
|
ourBrowser.swapDocShells(aOtherBrowser);
|
|
|
|
|
|
|
|
if (ourBrowser.isRemoteBrowser) {
|
|
|
|
// Switch outerWindowIDs for remote browsers.
|
|
|
|
let ourOuterWindowID = ourBrowser._outerWindowID;
|
|
|
|
ourBrowser._outerWindowID = aOtherBrowser._outerWindowID;
|
|
|
|
aOtherBrowser._outerWindowID = ourOuterWindowID;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register new outerWindowIDs.
|
|
|
|
this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser);
|
|
|
|
if (remoteBrowser) {
|
|
|
|
remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!(aFlags & Ci.nsIBrowser.SWAP_KEEP_PERMANENT_KEY)) {
|
|
|
|
// Swap permanentKey properties.
|
|
|
|
let ourPermanentKey = ourBrowser.permanentKey;
|
|
|
|
ourBrowser.permanentKey = aOtherBrowser.permanentKey;
|
|
|
|
aOtherBrowser.permanentKey = ourPermanentKey;
|
|
|
|
aOurTab.permanentKey = ourBrowser.permanentKey;
|
|
|
|
if (remoteBrowser) {
|
|
|
|
let otherTab = remoteBrowser.getTabForBrowser(aOtherBrowser);
|
|
|
|
if (otherTab) {
|
|
|
|
otherTab.permanentKey = aOtherBrowser.permanentKey;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Restore the progress listener
|
|
|
|
tabListener = this.mTabProgressListener(aOurTab, ourBrowser, false, false,
|
|
|
|
aStateFlags);
|
|
|
|
this._tabListeners.set(aOurTab, tabListener);
|
|
|
|
|
|
|
|
const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
|
|
|
|
filter.addProgressListener(tabListener, notifyAll);
|
|
|
|
ourBrowser.webProgress.addProgressListener(filter, notifyAll);
|
|
|
|
}
|
|
|
|
|
|
|
|
_swapRegisteredOpenURIs(aOurBrowser, aOtherBrowser) {
|
|
|
|
// Swap the registeredOpenURI properties of the two browsers
|
|
|
|
let tmp = aOurBrowser.registeredOpenURI;
|
|
|
|
delete aOurBrowser.registeredOpenURI;
|
|
|
|
if (aOtherBrowser.registeredOpenURI) {
|
|
|
|
aOurBrowser.registeredOpenURI = aOtherBrowser.registeredOpenURI;
|
|
|
|
delete aOtherBrowser.registeredOpenURI;
|
|
|
|
}
|
|
|
|
if (tmp) {
|
|
|
|
aOtherBrowser.registeredOpenURI = tmp;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadAllTabs() {
|
|
|
|
let tabs = this.visibleTabs;
|
|
|
|
let l = tabs.length;
|
|
|
|
for (var i = 0; i < l; i++) {
|
|
|
|
try {
|
|
|
|
this.getBrowserForTab(tabs[i]).reload();
|
|
|
|
} catch (e) {
|
|
|
|
// ignore failure to reload so others will be reloaded
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
reloadTab(aTab) {
|
|
|
|
let browser = this.getBrowserForTab(aTab);
|
|
|
|
// Reset temporary permissions on the current tab. This is done here
|
|
|
|
// because we only want to reset permissions on user reload.
|
|
|
|
SitePermissions.clearTemporaryPermissions(browser);
|
|
|
|
browser.reload();
|
|
|
|
}
|
|
|
|
|
|
|
|
addProgressListener(aListener) {
|
|
|
|
if (arguments.length != 1) {
|
|
|
|
Components.utils.reportError("gBrowser.addProgressListener was " +
|
|
|
|
"called with a second argument, " +
|
|
|
|
"which is not supported. See bug " +
|
|
|
|
"608628. Call stack: " + new Error().stack);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.mProgressListeners.push(aListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeProgressListener(aListener) {
|
|
|
|
this.mProgressListeners =
|
|
|
|
this.mProgressListeners.filter(l => l != aListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
addTabsProgressListener(aListener) {
|
|
|
|
this.mTabsProgressListeners.push(aListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeTabsProgressListener(aListener) {
|
|
|
|
this.mTabsProgressListeners =
|
|
|
|
this.mTabsProgressListeners.filter(l => l != aListener);
|
|
|
|
}
|
|
|
|
|
|
|
|
getBrowserForTab(aTab) {
|
|
|
|
return aTab.linkedBrowser;
|
|
|
|
}
|
|
|
|
|
|
|
|
showOnlyTheseTabs(aTabs) {
|
|
|
|
for (let tab of this.tabs) {
|
|
|
|
if (!aTabs.includes(tab))
|
|
|
|
this.hideTab(tab);
|
|
|
|
else
|
|
|
|
this.showTab(tab);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.tabContainer._handleTabSelect(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
showTab(aTab) {
|
|
|
|
if (aTab.hidden) {
|
|
|
|
aTab.removeAttribute("hidden");
|
|
|
|
this._visibleTabs = null; // invalidate cache
|
|
|
|
|
|
|
|
this.tabContainer._updateCloseButtons();
|
|
|
|
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
let event = document.createEvent("Events");
|
|
|
|
event.initEvent("TabShow", true, false);
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
SessionStore.deleteTabValue(aTab, "hiddenBy");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hideTab(aTab, aSource) {
|
|
|
|
if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
|
|
|
|
!aTab.closing && !aTab._sharingState) {
|
|
|
|
aTab.setAttribute("hidden", "true");
|
|
|
|
this._visibleTabs = null; // invalidate cache
|
|
|
|
|
|
|
|
this.tabContainer._updateCloseButtons();
|
|
|
|
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
let event = document.createEvent("Events");
|
|
|
|
event.initEvent("TabHide", true, false);
|
|
|
|
aTab.dispatchEvent(event);
|
|
|
|
if (aSource) {
|
|
|
|
SessionStore.setTabValue(aTab, "hiddenBy", aSource);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
selectTabAtIndex(aIndex, aEvent) {
|
|
|
|
let tabs = this.visibleTabs;
|
|
|
|
|
|
|
|
// count backwards for aIndex < 0
|
|
|
|
if (aIndex < 0) {
|
|
|
|
aIndex += tabs.length;
|
|
|
|
// clamp at index 0 if still negative.
|
|
|
|
if (aIndex < 0)
|
|
|
|
aIndex = 0;
|
|
|
|
} else if (aIndex >= tabs.length) {
|
|
|
|
// clamp at right-most tab if out of range.
|
|
|
|
aIndex = tabs.length - 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.selectedTab = tabs[aIndex];
|
|
|
|
|
|
|
|
if (aEvent) {
|
|
|
|
aEvent.preventDefault();
|
|
|
|
aEvent.stopPropagation();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Moves a tab to a new browser window, unless it's already the only tab
|
|
|
|
* in the current window, in which case this will do nothing.
|
|
|
|
*/
|
|
|
|
replaceTabWithWindow(aTab, aOptions) {
|
|
|
|
if (this.tabs.length == 1)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
var options = "chrome,dialog=no,all";
|
|
|
|
for (var name in aOptions)
|
|
|
|
options += "," + name + "=" + aOptions[name];
|
|
|
|
|
|
|
|
// Play the tab closing animation to give immediate feedback while
|
|
|
|
// waiting for the new window to appear.
|
|
|
|
// content area when the docshells are swapped.
|
|
|
|
if (this.animationsEnabled) {
|
|
|
|
aTab.style.maxWidth = ""; // ensure that fade-out transition happens
|
|
|
|
aTab.removeAttribute("fadein");
|
|
|
|
}
|
|
|
|
|
|
|
|
// tell a new window to take the "dropped" tab
|
|
|
|
return window.openDialog(getBrowserURL(), "_blank", options, aTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabTo(aTab, aIndex, aKeepRelatedTabs) {
|
|
|
|
var oldPosition = aTab._tPos;
|
|
|
|
if (oldPosition == aIndex)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't allow mixing pinned and unpinned tabs.
|
|
|
|
if (aTab.pinned)
|
|
|
|
aIndex = Math.min(aIndex, this._numPinnedTabs - 1);
|
|
|
|
else
|
|
|
|
aIndex = Math.max(aIndex, this._numPinnedTabs);
|
|
|
|
if (oldPosition == aIndex)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!aKeepRelatedTabs) {
|
|
|
|
this._lastRelatedTabMap = new WeakMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
let wasFocused = (document.activeElement == this.mCurrentTab);
|
|
|
|
|
|
|
|
aIndex = aIndex < aTab._tPos ? aIndex : aIndex + 1;
|
|
|
|
|
|
|
|
// invalidate cache
|
|
|
|
this._visibleTabs = null;
|
|
|
|
|
|
|
|
// use .item() instead of [] because dragging to the end of the strip goes out of
|
|
|
|
// bounds: .item() returns null (so it acts like appendChild), but [] throws
|
|
|
|
this.tabContainer.insertBefore(aTab, this.tabs.item(aIndex));
|
|
|
|
|
|
|
|
for (let i = 0; i < this.tabs.length; i++) {
|
|
|
|
this.tabs[i]._tPos = i;
|
|
|
|
this.tabs[i]._selected = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we're in the midst of an async tab switch while calling
|
|
|
|
// moveTabTo, we can get into a case where _visuallySelected
|
|
|
|
// is set to true on two different tabs.
|
|
|
|
//
|
|
|
|
// What we want to do in moveTabTo is to remove logical selection
|
|
|
|
// from all tabs, and then re-add logical selection to mCurrentTab
|
|
|
|
// (and visual selection as well if we're not running with e10s, which
|
|
|
|
// setting _selected will do automatically).
|
|
|
|
//
|
|
|
|
// If we're running with e10s, then the visual selection will not
|
|
|
|
// be changed, which is fine, since if we weren't in the midst of a
|
|
|
|
// tab switch, the previously visually selected tab should still be
|
|
|
|
// correct, and if we are in the midst of a tab switch, then the async
|
|
|
|
// tab switcher will set the visually selected tab once the tab switch
|
|
|
|
// has completed.
|
|
|
|
this.mCurrentTab._selected = true;
|
|
|
|
|
|
|
|
if (wasFocused)
|
|
|
|
this.mCurrentTab.focus();
|
|
|
|
|
|
|
|
this.tabContainer._handleTabSelect(true);
|
|
|
|
|
|
|
|
if (aTab.pinned)
|
|
|
|
this.tabContainer._positionPinnedTabs();
|
|
|
|
|
|
|
|
this.tabContainer._setPositionalAttributes();
|
|
|
|
|
|
|
|
var evt = document.createEvent("UIEvents");
|
|
|
|
evt.initUIEvent("TabMove", true, false, window, oldPosition);
|
|
|
|
aTab.dispatchEvent(evt);
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabForward() {
|
|
|
|
let nextTab = this.mCurrentTab.nextSibling;
|
|
|
|
while (nextTab && nextTab.hidden)
|
|
|
|
nextTab = nextTab.nextSibling;
|
|
|
|
|
|
|
|
if (nextTab)
|
|
|
|
this.moveTabTo(this.mCurrentTab, nextTab._tPos);
|
|
|
|
else if (this.arrowKeysShouldWrap)
|
|
|
|
this.moveTabToStart();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adopts a tab from another browser window, and inserts it at aIndex
|
|
|
|
*/
|
|
|
|
adoptTab(aTab, aIndex, aSelectTab) {
|
|
|
|
// Swap the dropped tab with a new one we create and then close
|
|
|
|
// it in the other window (making it seem to have moved between
|
|
|
|
// windows). We also ensure that the tab we create to swap into has
|
|
|
|
// the same remote type and process as the one we're swapping in.
|
|
|
|
// This makes sure we don't get a short-lived process for the new tab.
|
|
|
|
let linkedBrowser = aTab.linkedBrowser;
|
|
|
|
let params = {
|
|
|
|
eventDetail: { adoptedTab: aTab },
|
|
|
|
preferredRemoteType: linkedBrowser.remoteType,
|
|
|
|
sameProcessAsFrameLoader: linkedBrowser.frameLoader,
|
|
|
|
skipAnimation: true
|
|
|
|
};
|
|
|
|
if (aTab.hasAttribute("usercontextid")) {
|
|
|
|
// new tab must have the same usercontextid as the old one
|
|
|
|
params.userContextId = aTab.getAttribute("usercontextid");
|
|
|
|
}
|
|
|
|
let newTab = this.addTab("about:blank", params);
|
|
|
|
let newBrowser = this.getBrowserForTab(newTab);
|
|
|
|
|
|
|
|
// Stop the about:blank load.
|
|
|
|
newBrowser.stop();
|
|
|
|
// Make sure it has a docshell.
|
|
|
|
newBrowser.docShell;
|
|
|
|
|
|
|
|
let numPinned = this._numPinnedTabs;
|
|
|
|
if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
|
|
|
|
this.pinTab(newTab);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.moveTabTo(newTab, aIndex);
|
|
|
|
|
|
|
|
// We need to select the tab before calling swapBrowsersAndCloseOther
|
|
|
|
// so that window.content in chrome windows points to the right tab
|
|
|
|
// when pagehide/show events are fired. This is no longer necessary
|
|
|
|
// for any exiting browser code, but it may be necessary for add-on
|
|
|
|
// compatibility.
|
|
|
|
if (aSelectTab) {
|
|
|
|
this.selectedTab = newTab;
|
|
|
|
}
|
|
|
|
|
|
|
|
aTab.parentNode._finishAnimateTabMove();
|
|
|
|
this.swapBrowsersAndCloseOther(newTab, aTab);
|
|
|
|
|
|
|
|
if (aSelectTab) {
|
|
|
|
// Call updateCurrentBrowser to make sure the URL bar is up to date
|
|
|
|
// for our new tab after we've done swapBrowsersAndCloseOther.
|
|
|
|
this.updateCurrentBrowser(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
return newTab;
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabBackward() {
|
|
|
|
let previousTab = this.mCurrentTab.previousSibling;
|
|
|
|
while (previousTab && previousTab.hidden)
|
|
|
|
previousTab = previousTab.previousSibling;
|
|
|
|
|
|
|
|
if (previousTab)
|
|
|
|
this.moveTabTo(this.mCurrentTab, previousTab._tPos);
|
|
|
|
else if (this.arrowKeysShouldWrap)
|
|
|
|
this.moveTabToEnd();
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabToStart() {
|
|
|
|
var tabPos = this.mCurrentTab._tPos;
|
|
|
|
if (tabPos > 0)
|
|
|
|
this.moveTabTo(this.mCurrentTab, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabToEnd() {
|
|
|
|
var tabPos = this.mCurrentTab._tPos;
|
|
|
|
if (tabPos < this.browsers.length - 1)
|
|
|
|
this.moveTabTo(this.mCurrentTab, this.browsers.length - 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
moveTabOver(aEvent) {
|
|
|
|
var direction = window.getComputedStyle(this.container.parentNode).direction;
|
|
|
|
if ((direction == "ltr" && aEvent.keyCode == KeyEvent.DOM_VK_RIGHT) ||
|
|
|
|
(direction == "rtl" && aEvent.keyCode == KeyEvent.DOM_VK_LEFT))
|
|
|
|
this.moveTabForward();
|
|
|
|
else
|
|
|
|
this.moveTabBackward();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param aTab
|
|
|
|
* Can be from a different window as well
|
|
|
|
* @param aRestoreTabImmediately
|
|
|
|
* Can defer loading of the tab contents
|
|
|
|
*/
|
|
|
|
duplicateTab(aTab, aRestoreTabImmediately) {
|
|
|
|
return SessionStore.duplicateTab(window, aTab, 0, aRestoreTabImmediately);
|
|
|
|
}
|
|
|
|
|
|
|
|
activateBrowserForPrintPreview(aBrowser) {
|
|
|
|
this._printPreviewBrowsers.add(aBrowser);
|
|
|
|
if (this._switcher) {
|
|
|
|
this._switcher.activateBrowserForPrintPreview(aBrowser);
|
|
|
|
}
|
|
|
|
aBrowser.docShellIsActive = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
deactivatePrintPreviewBrowsers() {
|
|
|
|
let browsers = this._printPreviewBrowsers;
|
|
|
|
this._printPreviewBrowsers = new Set();
|
|
|
|
for (let browser of browsers) {
|
|
|
|
browser.docShellIsActive = this.shouldActivateDocShell(browser);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if a given browser's docshell should be active.
|
|
|
|
*/
|
|
|
|
shouldActivateDocShell(aBrowser) {
|
|
|
|
if (this._switcher) {
|
|
|
|
return this._switcher.shouldActivateDocShell(aBrowser);
|
|
|
|
}
|
|
|
|
return (aBrowser == this.selectedBrowser &&
|
|
|
|
window.windowState != window.STATE_MINIMIZED &&
|
|
|
|
!window.isFullyOccluded) ||
|
|
|
|
this._printPreviewBrowsers.has(aBrowser);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The tab switcher is responsible for asynchronously switching
|
|
|
|
* tabs in e10s. It waits until the new tab is ready (i.e., the
|
|
|
|
* layer tree is available) before switching to it. Then it
|
|
|
|
* unloads the layer tree for the old tab.
|
|
|
|
*
|
|
|
|
* The tab switcher is a state machine. For each tab, it
|
|
|
|
* maintains state about whether the layer tree for the tab is
|
|
|
|
* available, being loaded, being unloaded, or unavailable. It
|
|
|
|
* also keeps track of the tab currently being displayed, the tab
|
|
|
|
* it's trying to load, and the tab the user has asked to switch
|
|
|
|
* to. The switcher object is created upon tab switch. It is
|
|
|
|
* released when there are no pending tabs to load or unload.
|
|
|
|
*
|
|
|
|
* The following general principles have guided the design:
|
|
|
|
*
|
|
|
|
* 1. We only request one layer tree at a time. If the user
|
|
|
|
* switches to a different tab while waiting, we don't request
|
|
|
|
* the new layer tree until the old tab has loaded or timed out.
|
|
|
|
*
|
|
|
|
* 2. If loading the layers for a tab times out, we show the
|
|
|
|
* spinner and possibly request the layer tree for another tab if
|
|
|
|
* the user has requested one.
|
|
|
|
*
|
|
|
|
* 3. We discard layer trees on a delay. This way, if the user is
|
|
|
|
* switching among the same tabs frequently, we don't continually
|
|
|
|
* load the same tabs.
|
|
|
|
*
|
|
|
|
* It's important that we always show either the spinner or a tab
|
|
|
|
* whose layers are available. Otherwise the compositor will draw
|
|
|
|
* an entirely black frame, which is very jarring. To ensure this
|
|
|
|
* never happens when switching away from a tab, we assume the
|
|
|
|
* old tab might still be drawn until a MozAfterPaint event
|
|
|
|
* occurs. Because layout and compositing happen asynchronously,
|
|
|
|
* we don't have any other way of knowing when the switch
|
|
|
|
* actually takes place. Therefore, we don't unload the old tab
|
|
|
|
* until the next MozAfterPaint event.
|
|
|
|
*/
|
|
|
|
_getSwitcher() {
|
|
|
|
if (this._switcher) {
|
|
|
|
return this._switcher;
|
|
|
|
}
|
|
|
|
|
|
|
|
let switcher = {
|
|
|
|
// How long to wait for a tab's layers to load. After this
|
|
|
|
// time elapses, we're free to put up the spinner and start
|
|
|
|
// trying to load a different tab.
|
|
|
|
TAB_SWITCH_TIMEOUT: 400 /* ms */,
|
|
|
|
|
|
|
|
// When the user hasn't switched tabs for this long, we unload
|
|
|
|
// layers for all tabs that aren't in use.
|
|
|
|
UNLOAD_DELAY: 300 /* ms */,
|
|
|
|
|
|
|
|
// The next three tabs form the principal state variables.
|
|
|
|
// See the assertions in postActions for their invariants.
|
|
|
|
|
|
|
|
// Tab the user requested most recently.
|
|
|
|
requestedTab: this.selectedTab,
|
|
|
|
|
|
|
|
// Tab we're currently trying to load.
|
|
|
|
loadingTab: null,
|
|
|
|
|
|
|
|
// We show this tab in case the requestedTab hasn't loaded yet.
|
|
|
|
lastVisibleTab: this.selectedTab,
|
|
|
|
|
|
|
|
// Auxilliary state variables:
|
|
|
|
|
|
|
|
visibleTab: this.selectedTab, // Tab that's on screen.
|
|
|
|
spinnerTab: null, // Tab showing a spinner.
|
|
|
|
blankTab: null, // Tab showing blank.
|
|
|
|
lastPrimaryTab: this.selectedTab, // Tab with primary="true"
|
|
|
|
|
|
|
|
tabbrowser: this, // Reference to gBrowser.
|
|
|
|
loadTimer: null, // TAB_SWITCH_TIMEOUT nsITimer instance.
|
|
|
|
unloadTimer: null, // UNLOAD_DELAY nsITimer instance.
|
|
|
|
|
|
|
|
// Map from tabs to STATE_* (below).
|
|
|
|
tabState: new Map(),
|
|
|
|
|
|
|
|
// True if we're in the midst of switching tabs.
|
|
|
|
switchInProgress: false,
|
|
|
|
|
|
|
|
// Keep an exact list of content processes (tabParent) in which
|
|
|
|
// we're actively suppressing the display port. This gives a robust
|
|
|
|
// way to make sure we don't forget to un-suppress.
|
|
|
|
activeSuppressDisplayport: new Set(),
|
|
|
|
|
|
|
|
// Set of tabs that might be visible right now. We maintain
|
|
|
|
// this set because we can't be sure when a tab is actually
|
|
|
|
// drawn. A tab is added to this set when we ask to make it
|
|
|
|
// visible. All tabs but the most recently shown tab are
|
|
|
|
// removed from the set upon MozAfterPaint.
|
|
|
|
maybeVisibleTabs: new Set([this.selectedTab]),
|
|
|
|
|
|
|
|
// This holds onto the set of tabs that we've been asked to warm up.
|
|
|
|
// This is used only for Telemetry and logging, and (in order to not
|
|
|
|
// over-complicate the async tab switcher any further) has nothing to do
|
|
|
|
// with how warmed tabs are loaded and unloaded.
|
|
|
|
warmingTabs: new WeakSet(),
|
|
|
|
|
|
|
|
STATE_UNLOADED: 0,
|
|
|
|
STATE_LOADING: 1,
|
|
|
|
STATE_LOADED: 2,
|
|
|
|
STATE_UNLOADING: 3,
|
|
|
|
|
|
|
|
// re-entrancy guard:
|
|
|
|
_processing: false,
|
|
|
|
|
|
|
|
// Wraps nsITimer. Must not use the vanilla setTimeout and
|
|
|
|
// clearTimeout, because they will be blocked by nsIPromptService
|
|
|
|
// dialogs.
|
|
|
|
setTimer(callback, timeout) {
|
|
|
|
let event = {
|
|
|
|
notify: callback
|
|
|
|
};
|
|
|
|
|
|
|
|
var timer = Cc["@mozilla.org/timer;1"]
|
|
|
|
.createInstance(Components.interfaces.nsITimer);
|
|
|
|
timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
|
|
|
|
return timer;
|
|
|
|
},
|
|
|
|
|
|
|
|
clearTimer(timer) {
|
|
|
|
timer.cancel();
|
|
|
|
},
|
|
|
|
|
|
|
|
getTabState(tab) {
|
|
|
|
let state = this.tabState.get(tab);
|
|
|
|
|
|
|
|
// As an optimization, we lazily evaluate the state of tabs
|
|
|
|
// that we've never seen before. Once we've figured it out,
|
|
|
|
// we stash it in our state map.
|
|
|
|
if (state === undefined) {
|
|
|
|
state = this.STATE_UNLOADED;
|
|
|
|
|
|
|
|
if (tab && tab.linkedPanel) {
|
|
|
|
let b = tab.linkedBrowser;
|
|
|
|
if (b.renderLayers && b.hasLayers) {
|
|
|
|
state = this.STATE_LOADED;
|
|
|
|
} else if (b.renderLayers && !b.hasLayers) {
|
|
|
|
state = this.STATE_LOADING;
|
|
|
|
} else if (!b.renderLayers && b.hasLayers) {
|
|
|
|
state = this.STATE_UNLOADING;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.setTabStateNoAction(tab, state);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
return state;
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
setTabStateNoAction(tab, state) {
|
|
|
|
if (state == this.STATE_UNLOADED) {
|
|
|
|
this.tabState.delete(tab);
|
|
|
|
} else {
|
|
|
|
this.tabState.set(tab, state);
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
setTabState(tab, state) {
|
|
|
|
if (state == this.getTabState(tab)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.setTabStateNoAction(tab, state);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let browser = tab.linkedBrowser;
|
|
|
|
let { tabParent } = browser.frameLoader;
|
|
|
|
if (state == this.STATE_LOADING) {
|
|
|
|
this.assert(!this.minimizedOrFullyOccluded);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (!this.tabbrowser.tabWarmingEnabled) {
|
|
|
|
browser.docShellIsActive = true;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (tabParent) {
|
|
|
|
browser.renderLayers = true;
|
|
|
|
} else {
|
|
|
|
this.onLayersReady(browser);
|
|
|
|
}
|
|
|
|
} else if (state == this.STATE_UNLOADING) {
|
|
|
|
this.unwarmTab(tab);
|
|
|
|
// Setting the docShell to be inactive will also cause it
|
|
|
|
// to stop rendering layers.
|
|
|
|
browser.docShellIsActive = false;
|
|
|
|
if (!tabParent) {
|
|
|
|
this.onLayersCleared(browser);
|
|
|
|
}
|
|
|
|
} else if (state == this.STATE_LOADED) {
|
|
|
|
this.maybeActivateDocShell(tab);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (!tab.linkedBrowser.isRemoteBrowser) {
|
|
|
|
// setTabState is potentially re-entrant in the non-remote case,
|
|
|
|
// so we must re-get the state for this assertion.
|
|
|
|
let nonRemoteState = this.getTabState(tab);
|
|
|
|
// Non-remote tabs can never stay in the STATE_LOADING
|
|
|
|
// or STATE_UNLOADING states. By the time this function
|
|
|
|
// exits, a non-remote tab must be in STATE_LOADED or
|
|
|
|
// STATE_UNLOADED, since the painting and the layer
|
|
|
|
// upload happen synchronously.
|
|
|
|
this.assert(nonRemoteState == this.STATE_UNLOADED ||
|
|
|
|
nonRemoteState == this.STATE_LOADED);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
get minimizedOrFullyOccluded() {
|
|
|
|
return window.windowState == window.STATE_MINIMIZED ||
|
|
|
|
window.isFullyOccluded;
|
|
|
|
},
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this.log("START");
|
|
|
|
|
|
|
|
window.addEventListener("MozAfterPaint", this);
|
|
|
|
window.addEventListener("MozLayerTreeReady", this);
|
|
|
|
window.addEventListener("MozLayerTreeCleared", this);
|
|
|
|
window.addEventListener("TabRemotenessChange", this);
|
|
|
|
window.addEventListener("sizemodechange", this);
|
|
|
|
window.addEventListener("occlusionstatechange", this);
|
|
|
|
window.addEventListener("SwapDocShells", this, true);
|
|
|
|
window.addEventListener("EndSwapDocShells", this, true);
|
|
|
|
|
|
|
|
let initialTab = this.requestedTab;
|
|
|
|
let initialBrowser = initialTab.linkedBrowser;
|
|
|
|
|
|
|
|
let tabIsLoaded = !initialBrowser.isRemoteBrowser ||
|
|
|
|
initialBrowser.frameLoader.tabParent.hasLayers;
|
|
|
|
|
|
|
|
// If we minimized the window before the switcher was activated,
|
|
|
|
// we might have set the preserveLayers flag for the current
|
|
|
|
// browser. Let's clear it.
|
|
|
|
initialBrowser.preserveLayers(false);
|
|
|
|
|
|
|
|
if (!this.minimizedOrFullyOccluded) {
|
|
|
|
this.log("Initial tab is loaded?: " + tabIsLoaded);
|
|
|
|
this.setTabState(initialTab, tabIsLoaded ? this.STATE_LOADED
|
|
|
|
: this.STATE_LOADING);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) {
|
|
|
|
let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser);
|
|
|
|
let state = ppBrowser.hasLayers ? this.STATE_LOADED
|
|
|
|
: this.STATE_LOADING;
|
|
|
|
this.setTabState(ppTab, state);
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
destroy() {
|
|
|
|
if (this.unloadTimer) {
|
|
|
|
this.clearTimer(this.unloadTimer);
|
|
|
|
this.unloadTimer = null;
|
|
|
|
}
|
|
|
|
if (this.loadTimer) {
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
window.removeEventListener("MozAfterPaint", this);
|
|
|
|
window.removeEventListener("MozLayerTreeReady", this);
|
|
|
|
window.removeEventListener("MozLayerTreeCleared", this);
|
|
|
|
window.removeEventListener("TabRemotenessChange", this);
|
|
|
|
window.removeEventListener("sizemodechange", this);
|
|
|
|
window.removeEventListener("occlusionstatechange", this);
|
|
|
|
window.removeEventListener("SwapDocShells", this, true);
|
|
|
|
window.removeEventListener("EndSwapDocShells", this, true);
|
|
|
|
|
|
|
|
this.tabbrowser._switcher = null;
|
|
|
|
|
|
|
|
this.activeSuppressDisplayport.forEach(function(tabParent) {
|
|
|
|
tabParent.suppressDisplayport(false);
|
|
|
|
});
|
|
|
|
this.activeSuppressDisplayport.clear();
|
|
|
|
},
|
|
|
|
|
|
|
|
finish() {
|
|
|
|
this.log("FINISH");
|
|
|
|
|
|
|
|
this.assert(this.tabbrowser._switcher);
|
|
|
|
this.assert(this.tabbrowser._switcher === this);
|
|
|
|
this.assert(!this.spinnerTab);
|
|
|
|
this.assert(!this.blankTab);
|
|
|
|
this.assert(!this.loadTimer);
|
|
|
|
this.assert(!this.loadingTab);
|
|
|
|
this.assert(this.lastVisibleTab === this.requestedTab);
|
|
|
|
this.assert(this.minimizedOrFullyOccluded ||
|
|
|
|
this.getTabState(this.requestedTab) == this.STATE_LOADED);
|
|
|
|
|
|
|
|
this.destroy();
|
|
|
|
|
|
|
|
document.commandDispatcher.unlock();
|
|
|
|
|
|
|
|
let event = new CustomEvent("TabSwitchDone", {
|
|
|
|
bubbles: true,
|
|
|
|
cancelable: true
|
|
|
|
});
|
|
|
|
this.tabbrowser.dispatchEvent(event);
|
|
|
|
},
|
|
|
|
|
|
|
|
// This function is called after all the main state changes to
|
|
|
|
// make sure we display the right tab.
|
|
|
|
updateDisplay() {
|
|
|
|
let requestedTabState = this.getTabState(this.requestedTab);
|
|
|
|
let requestedBrowser = this.requestedTab.linkedBrowser;
|
|
|
|
|
|
|
|
// It is often more desirable to show a blank tab when appropriate than
|
|
|
|
// the tab switch spinner - especially since the spinner is usually
|
|
|
|
// preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
|
|
|
|
// tab switch. We can hide this lag, and hide the time being spent
|
|
|
|
// constructing TabChild's, layer trees, etc, by showing a blank
|
|
|
|
// tab instead and focusing it immediately.
|
|
|
|
let shouldBeBlank = false;
|
|
|
|
if (requestedBrowser.isRemoteBrowser) {
|
|
|
|
// If a tab is remote and the window is not minimized, we can show a
|
|
|
|
// blank tab instead of a spinner in the following cases:
|
|
|
|
//
|
|
|
|
// 1. The tab has just crashed, and we haven't started showing the
|
|
|
|
// tab crashed page yet (in this case, the TabParent is null)
|
|
|
|
// 2. The tab has never presented, and has not finished loading
|
|
|
|
// a non-local-about: page.
|
|
|
|
//
|
|
|
|
// For (2), "finished loading a non-local-about: page" is
|
|
|
|
// determined by the busy state on the tab element and checking
|
|
|
|
// if the loaded URI is local.
|
|
|
|
let hasSufficientlyLoaded = !this.requestedTab.hasAttribute("busy") &&
|
|
|
|
!this.tabbrowser._isLocalAboutURI(requestedBrowser.currentURI);
|
|
|
|
|
|
|
|
let fl = requestedBrowser.frameLoader;
|
|
|
|
shouldBeBlank = !this.minimizedOrFullyOccluded &&
|
|
|
|
(!fl.tabParent ||
|
|
|
|
(!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.log("Tab should be blank: " + shouldBeBlank);
|
|
|
|
this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
|
|
|
|
|
|
|
|
// Figure out which tab we actually want visible right now.
|
|
|
|
let showTab = null;
|
|
|
|
if (requestedTabState != this.STATE_LOADED &&
|
|
|
|
this.lastVisibleTab && this.loadTimer && !shouldBeBlank) {
|
|
|
|
// If we can't show the requestedTab, and lastVisibleTab is
|
|
|
|
// available, show it.
|
|
|
|
showTab = this.lastVisibleTab;
|
|
|
|
} else {
|
|
|
|
// Show the requested tab. If it's not available, we'll show the spinner or a blank tab.
|
|
|
|
showTab = this.requestedTab;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// First, let's deal with blank tabs, which we show instead
|
|
|
|
// of the spinner when the tab is not currently set up
|
|
|
|
// properly in the content process.
|
|
|
|
if (!shouldBeBlank && this.blankTab) {
|
|
|
|
this.blankTab.linkedBrowser.removeAttribute("blank");
|
|
|
|
this.blankTab = null;
|
|
|
|
} else if (shouldBeBlank && this.blankTab !== showTab) {
|
|
|
|
if (this.blankTab) {
|
|
|
|
this.blankTab.linkedBrowser.removeAttribute("blank");
|
|
|
|
}
|
|
|
|
this.blankTab = showTab;
|
|
|
|
this.blankTab.linkedBrowser.setAttribute("blank", "true");
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Show or hide the spinner as needed.
|
|
|
|
let needSpinner = this.getTabState(showTab) != this.STATE_LOADED &&
|
|
|
|
!this.minimizedOrFullyOccluded &&
|
|
|
|
!shouldBeBlank;
|
|
|
|
|
|
|
|
if (!needSpinner && this.spinnerTab) {
|
|
|
|
this.spinnerHidden();
|
|
|
|
this.tabbrowser.removeAttribute("pendingpaint");
|
|
|
|
this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
|
|
|
|
this.spinnerTab = null;
|
|
|
|
} else if (needSpinner && this.spinnerTab !== showTab) {
|
|
|
|
if (this.spinnerTab) {
|
|
|
|
this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint");
|
|
|
|
} else {
|
|
|
|
this.spinnerDisplayed();
|
|
|
|
}
|
|
|
|
this.spinnerTab = showTab;
|
|
|
|
this.tabbrowser.setAttribute("pendingpaint", "true");
|
|
|
|
this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true");
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Switch to the tab we've decided to make visible.
|
|
|
|
if (this.visibleTab !== showTab) {
|
|
|
|
this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab);
|
|
|
|
this.visibleTab = showTab;
|
|
|
|
|
|
|
|
this.maybeVisibleTabs.add(showTab);
|
|
|
|
|
|
|
|
let tabs = this.tabbrowser.tabbox.tabs;
|
|
|
|
let tabPanel = this.tabbrowser.mPanelContainer;
|
|
|
|
let showPanel = tabs.getRelatedElement(showTab);
|
|
|
|
let index = Array.indexOf(tabPanel.childNodes, showPanel);
|
|
|
|
if (index != -1) {
|
|
|
|
this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`);
|
|
|
|
tabPanel.setAttribute("selectedIndex", index);
|
|
|
|
if (showTab === this.requestedTab) {
|
|
|
|
if (this._requestingTab) {
|
|
|
|
/*
|
|
|
|
* If _requestingTab is set, that means that we're switching the
|
|
|
|
* visibility of the tab synchronously, and we need to wait for
|
|
|
|
* the "select" event before shifting focus so that
|
|
|
|
* _adjustFocusAfterTabSwitch runs with the right information for
|
|
|
|
* the tab switch.
|
|
|
|
*/
|
|
|
|
this.tabbrowser.addEventListener("select", () => {
|
|
|
|
this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
|
|
|
|
}, { once: true });
|
|
|
|
} else {
|
|
|
|
this.tabbrowser._adjustFocusAfterTabSwitch(showTab);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.maybeActivateDocShell(this.requestedTab);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// This doesn't necessarily exist if we're a new window and haven't switched tabs yet
|
|
|
|
if (this.lastVisibleTab)
|
|
|
|
this.lastVisibleTab._visuallySelected = false;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.visibleTab._visuallySelected = true;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.lastVisibleTab = this.visibleTab;
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
assert(cond) {
|
|
|
|
if (!cond) {
|
|
|
|
dump("Assertion failure\n" + Error().stack);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Don't break a user's browser if an assertion fails.
|
|
|
|
if (AppConstants.DEBUG) {
|
|
|
|
throw new Error("Assertion failure");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// We've decided to try to load requestedTab.
|
|
|
|
loadRequestedTab() {
|
|
|
|
this.assert(!this.loadTimer);
|
|
|
|
this.assert(!this.minimizedOrFullyOccluded);
|
|
|
|
|
|
|
|
// loadingTab can be non-null here if we timed out loading the current tab.
|
|
|
|
// In that case we just overwrite it with a different tab; it's had its chance.
|
|
|
|
this.loadingTab = this.requestedTab;
|
|
|
|
this.log("Loading tab " + this.tinfo(this.loadingTab));
|
|
|
|
|
|
|
|
this.loadTimer = this.setTimer(() => this.onLoadTimeout(), this.TAB_SWITCH_TIMEOUT);
|
|
|
|
this.setTabState(this.requestedTab, this.STATE_LOADING);
|
|
|
|
},
|
|
|
|
|
|
|
|
maybeActivateDocShell(tab) {
|
|
|
|
// If we've reached the point where the requested tab has entered
|
|
|
|
// the loaded state, but the DocShell is still not yet active, we
|
|
|
|
// should activate it.
|
|
|
|
let browser = tab.linkedBrowser;
|
|
|
|
let state = this.getTabState(tab);
|
|
|
|
let canCheckDocShellState = !browser.mDestroyed &&
|
|
|
|
(browser.docShell || browser.frameLoader.tabParent);
|
|
|
|
if (tab == this.requestedTab &&
|
|
|
|
canCheckDocShellState &&
|
|
|
|
state == this.STATE_LOADED &&
|
|
|
|
!browser.docShellIsActive &&
|
|
|
|
!this.minimizedOrFullyOccluded) {
|
|
|
|
browser.docShellIsActive = true;
|
|
|
|
this.logState("Set requested tab docshell to active and preserveLayers to false");
|
|
|
|
// If we minimized the window before the switcher was activated,
|
|
|
|
// we might have set the preserveLayers flag for the current
|
|
|
|
// browser. Let's clear it.
|
|
|
|
browser.preserveLayers(false);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// This function runs before every event. It fixes up the state
|
|
|
|
// to account for closed tabs.
|
|
|
|
preActions() {
|
|
|
|
this.assert(this.tabbrowser._switcher);
|
|
|
|
this.assert(this.tabbrowser._switcher === this);
|
|
|
|
|
|
|
|
for (let [tab, ] of this.tabState) {
|
|
|
|
if (!tab.linkedBrowser) {
|
|
|
|
this.tabState.delete(tab);
|
|
|
|
this.unwarmTab(tab);
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) {
|
|
|
|
this.lastVisibleTab = null;
|
|
|
|
}
|
|
|
|
if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) {
|
|
|
|
this.lastPrimaryTab = null;
|
|
|
|
}
|
|
|
|
if (this.blankTab && !this.blankTab.linkedBrowser) {
|
|
|
|
this.blankTab = null;
|
|
|
|
}
|
|
|
|
if (this.spinnerTab && !this.spinnerTab.linkedBrowser) {
|
|
|
|
this.spinnerHidden();
|
|
|
|
this.spinnerTab = null;
|
|
|
|
}
|
|
|
|
if (this.loadingTab && !this.loadingTab.linkedBrowser) {
|
|
|
|
this.loadingTab = null;
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// This code runs after we've responded to an event or requested a new
|
|
|
|
// tab. It's expected that we've already updated all the principal
|
|
|
|
// state variables. This function takes care of updating any auxilliary
|
|
|
|
// state.
|
|
|
|
postActions() {
|
|
|
|
// Once we finish loading loadingTab, we null it out. So the state should
|
|
|
|
// always be LOADING.
|
|
|
|
this.assert(!this.loadingTab ||
|
|
|
|
this.getTabState(this.loadingTab) == this.STATE_LOADING);
|
|
|
|
|
|
|
|
// We guarantee that loadingTab is non-null iff loadTimer is non-null. So
|
|
|
|
// the timer is set only when we're loading something.
|
|
|
|
this.assert(!this.loadTimer || this.loadingTab);
|
|
|
|
this.assert(!this.loadingTab || this.loadTimer);
|
|
|
|
|
|
|
|
// If we're switching to a non-remote tab, there's no need to wait
|
|
|
|
// for it to send layers to the compositor, as this will happen
|
|
|
|
// synchronously. Clearing this here means that in the next step,
|
|
|
|
// we can load the non-remote browser immediately.
|
|
|
|
if (!this.requestedTab.linkedBrowser.isRemoteBrowser) {
|
|
|
|
this.loadingTab = null;
|
|
|
|
if (this.loadTimer) {
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If we're not loading anything, try loading the requested tab.
|
|
|
|
let stateOfRequestedTab = this.getTabState(this.requestedTab);
|
|
|
|
if (!this.loadTimer && !this.minimizedOrFullyOccluded &&
|
|
|
|
(stateOfRequestedTab == this.STATE_UNLOADED ||
|
|
|
|
stateOfRequestedTab == this.STATE_UNLOADING ||
|
|
|
|
this.warmingTabs.has(this.requestedTab))) {
|
|
|
|
this.assert(stateOfRequestedTab != this.STATE_LOADED);
|
|
|
|
this.loadRequestedTab();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// See how many tabs still have work to do.
|
|
|
|
let numPending = 0;
|
|
|
|
let numWarming = 0;
|
|
|
|
for (let [tab, state] of this.tabState) {
|
|
|
|
// Skip print preview browsers since they shouldn't affect tab switching.
|
|
|
|
if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (state == this.STATE_LOADED && tab !== this.requestedTab) {
|
|
|
|
numPending++;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (tab !== this.visibleTab) {
|
|
|
|
numWarming++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) {
|
|
|
|
numPending++;
|
|
|
|
}
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.updateDisplay();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// It's possible for updateDisplay to trigger one of our own event
|
|
|
|
// handlers, which might cause finish() to already have been called.
|
|
|
|
// Check for that before calling finish() again.
|
|
|
|
if (!this.tabbrowser._switcher) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.maybeFinishTabSwitch();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (numWarming > this.tabbrowser.tabWarmingMax) {
|
|
|
|
this.logState("Hit tabWarmingMax");
|
|
|
|
if (this.unloadTimer) {
|
|
|
|
this.clearTimer(this.unloadTimer);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this.unloadNonRequiredTabs();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (numPending == 0) {
|
|
|
|
this.finish();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.logState("done");
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Fires when we're ready to unload unused tabs.
|
|
|
|
onUnloadTimeout() {
|
|
|
|
this.logState("onUnloadTimeout");
|
|
|
|
this.preActions();
|
|
|
|
this.unloadTimer = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.unloadNonRequiredTabs();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.postActions();
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If there are any non-visible and non-requested tabs in
|
|
|
|
// STATE_LOADED, sets them to STATE_UNLOADING. Also queues
|
|
|
|
// up the unloadTimer to run onUnloadTimeout if there are still
|
|
|
|
// tabs in the process of unloading.
|
|
|
|
unloadNonRequiredTabs() {
|
|
|
|
this.warmingTabs = new WeakSet();
|
|
|
|
let numPending = 0;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Unload any tabs that can be unloaded.
|
|
|
|
for (let [tab, state] of this.tabState) {
|
|
|
|
if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
|
|
|
|
continue;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (state == this.STATE_LOADED &&
|
|
|
|
!this.maybeVisibleTabs.has(tab) &&
|
|
|
|
tab !== this.lastVisibleTab &&
|
|
|
|
tab !== this.loadingTab &&
|
|
|
|
tab !== this.requestedTab) {
|
|
|
|
this.setTabState(tab, this.STATE_UNLOADING);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (state != this.STATE_UNLOADED && tab !== this.requestedTab) {
|
|
|
|
numPending++;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (numPending) {
|
|
|
|
// Keep the timer going since there may be more tabs to unload.
|
|
|
|
this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), this.UNLOAD_DELAY);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Fires when an ongoing load has taken too long.
|
|
|
|
onLoadTimeout() {
|
|
|
|
this.logState("onLoadTimeout");
|
|
|
|
this.preActions();
|
|
|
|
this.loadTimer = null;
|
|
|
|
this.loadingTab = null;
|
|
|
|
this.postActions();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Fires when the layers become available for a tab.
|
|
|
|
onLayersReady(browser) {
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
|
|
if (!tab) {
|
|
|
|
// We probably got a layer update from a tab that got before
|
|
|
|
// the switcher was created, or for browser that's not being
|
|
|
|
// tracked by the async tab switcher (like the preloaded about:newtab).
|
2018-02-27 17:47:52 +00:00
|
|
|
return;
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.assert(this.getTabState(tab) == this.STATE_LOADING ||
|
|
|
|
this.getTabState(tab) == this.STATE_LOADED);
|
|
|
|
this.setTabState(tab, this.STATE_LOADED);
|
|
|
|
this.unwarmTab(tab);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.loadingTab === tab) {
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
|
|
|
this.loadingTab = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
// Fires when we paint the screen. Any tab switches we initiated
|
|
|
|
// previously are done, so there's no need to keep the old layers
|
|
|
|
// around.
|
|
|
|
onPaint() {
|
|
|
|
this.maybeVisibleTabs.clear();
|
|
|
|
},
|
|
|
|
|
|
|
|
// Called when we're done clearing the layers for a tab.
|
|
|
|
onLayersCleared(browser) {
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
|
|
if (tab) {
|
|
|
|
this.logState(`onLayersCleared(${tab._tPos})`);
|
|
|
|
this.assert(this.getTabState(tab) == this.STATE_UNLOADING ||
|
|
|
|
this.getTabState(tab) == this.STATE_UNLOADED);
|
|
|
|
this.setTabState(tab, this.STATE_UNLOADED);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Called when a tab switches from remote to non-remote. In this case
|
|
|
|
// a MozLayerTreeReady notification that we requested may never fire,
|
|
|
|
// so we need to simulate it.
|
|
|
|
onRemotenessChange(tab) {
|
|
|
|
this.logState(`onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})`);
|
|
|
|
if (!tab.linkedBrowser.isRemoteBrowser) {
|
|
|
|
if (this.getTabState(tab) == this.STATE_LOADING) {
|
|
|
|
this.onLayersReady(tab.linkedBrowser);
|
|
|
|
} else if (this.getTabState(tab) == this.STATE_UNLOADING) {
|
|
|
|
this.onLayersCleared(tab.linkedBrowser);
|
|
|
|
}
|
|
|
|
} else if (this.getTabState(tab) == this.STATE_LOADED) {
|
|
|
|
// A tab just changed from non-remote to remote, which means
|
|
|
|
// that it's gone back into the STATE_LOADING state until
|
|
|
|
// it sends up a layer tree.
|
|
|
|
this.setTabState(tab, this.STATE_LOADING);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
// Called when a tab has been removed, and the browser node is
|
|
|
|
// about to be removed from the DOM.
|
|
|
|
onTabRemoved(tab) {
|
|
|
|
if (this.lastVisibleTab == tab) {
|
|
|
|
// The browser that was being presented to the user is
|
|
|
|
// going to be removed during this tick of the event loop.
|
|
|
|
// This will cause us to show a tab spinner instead.
|
|
|
|
this.preActions();
|
|
|
|
this.lastVisibleTab = null;
|
|
|
|
this.postActions();
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
onSizeModeOrOcclusionStateChange() {
|
|
|
|
if (this.minimizedOrFullyOccluded) {
|
|
|
|
for (let [tab, state] of this.tabState) {
|
|
|
|
// Skip print preview browsers since they shouldn't affect tab switching.
|
|
|
|
if (this.tabbrowser._printPreviewBrowsers.has(tab.linkedBrowser)) {
|
|
|
|
continue;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (state == this.STATE_LOADING || state == this.STATE_LOADED) {
|
|
|
|
this.setTabState(tab, this.STATE_UNLOADING);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.loadTimer) {
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this.loadingTab = null;
|
|
|
|
} else {
|
|
|
|
// We're no longer minimized or occluded. This means we might want
|
|
|
|
// to activate the current tab's docShell.
|
|
|
|
this.maybeActivateDocShell(gBrowser.selectedTab);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onSwapDocShells(ourBrowser, otherBrowser) {
|
|
|
|
// This event fires before the swap. ourBrowser is from
|
|
|
|
// our window. We save the state of otherBrowser since ourBrowser
|
|
|
|
// needs to take on that state at the end of the swap.
|
|
|
|
|
|
|
|
let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser;
|
|
|
|
let otherState;
|
|
|
|
if (otherTabbrowser && otherTabbrowser._switcher) {
|
|
|
|
let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser);
|
|
|
|
let otherSwitcher = otherTabbrowser._switcher;
|
|
|
|
otherState = otherSwitcher.getTabState(otherTab);
|
|
|
|
} else {
|
|
|
|
otherState = otherBrowser.docShellIsActive ? this.STATE_LOADED : this.STATE_UNLOADED;
|
|
|
|
}
|
|
|
|
if (!this.swapMap) {
|
|
|
|
this.swapMap = new WeakMap();
|
|
|
|
}
|
|
|
|
this.swapMap.set(otherBrowser, {
|
|
|
|
state: otherState,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
onEndSwapDocShells(ourBrowser, otherBrowser) {
|
|
|
|
// The swap has happened. We reset the loadingTab in
|
|
|
|
// case it has been swapped. We also set ourBrowser's state
|
|
|
|
// to whatever otherBrowser's state was before the swap.
|
|
|
|
|
|
|
|
if (this.loadTimer) {
|
|
|
|
// Clearing the load timer means that we will
|
|
|
|
// immediately display a spinner if ourBrowser isn't
|
|
|
|
// ready yet. Typically it will already be ready
|
|
|
|
// though. If it's not, we're probably in a new window,
|
|
|
|
// in which case we have no other tabs to display anyway.
|
|
|
|
this.clearTimer(this.loadTimer);
|
|
|
|
this.loadTimer = null;
|
|
|
|
}
|
|
|
|
this.loadingTab = null;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let { state: otherState } = this.swapMap.get(otherBrowser);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.swapMap.delete(otherBrowser);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser);
|
|
|
|
if (ourTab) {
|
|
|
|
this.setTabStateNoAction(ourTab, otherState);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
shouldActivateDocShell(browser) {
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
|
|
let state = this.getTabState(tab);
|
|
|
|
return state == this.STATE_LOADING || state == this.STATE_LOADED;
|
|
|
|
},
|
|
|
|
|
|
|
|
activateBrowserForPrintPreview(browser) {
|
|
|
|
let tab = this.tabbrowser.getTabForBrowser(browser);
|
|
|
|
let state = this.getTabState(tab);
|
|
|
|
if (state != this.STATE_LOADING &&
|
|
|
|
state != this.STATE_LOADED) {
|
|
|
|
this.setTabState(tab, this.STATE_LOADING);
|
|
|
|
this.logState("Activated browser " + this.tinfo(tab) + " for print preview");
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
canWarmTab(tab) {
|
|
|
|
if (!this.tabbrowser.tabWarmingEnabled) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (!tab) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// If the tab is not yet inserted, closing, not remote,
|
|
|
|
// crashed, already visible, or already requested, warming
|
|
|
|
// up the tab makes no sense.
|
|
|
|
if (this.minimizedOrFullyOccluded ||
|
|
|
|
!tab.linkedPanel ||
|
|
|
|
tab.closing ||
|
|
|
|
!tab.linkedBrowser.isRemoteBrowser ||
|
|
|
|
!tab.linkedBrowser.frameLoader.tabParent) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Similarly, if the tab is already in STATE_LOADING or
|
|
|
|
// STATE_LOADED somehow, there's no point in trying to
|
|
|
|
// warm it up.
|
|
|
|
let state = this.getTabState(tab);
|
|
|
|
if (state === this.STATE_LOADING ||
|
|
|
|
state === this.STATE_LOADED) {
|
|
|
|
return false;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
return true;
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
unwarmTab(tab) {
|
|
|
|
this.warmingTabs.delete(tab);
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
warmupTab(tab) {
|
|
|
|
if (!this.canWarmTab(tab)) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.logState("warmupTab " + this.tinfo(tab));
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.warmingTabs.add(tab);
|
|
|
|
this.setTabState(tab, this.STATE_LOADING);
|
|
|
|
this.suppressDisplayPortAndQueueUnload(tab,
|
|
|
|
this.tabbrowser.tabWarmingUnloadDelay);
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// Called when the user asks to switch to a given tab.
|
|
|
|
requestTab(tab) {
|
|
|
|
if (tab === this.requestedTab) {
|
|
|
|
return;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.tabbrowser.tabWarmingEnabled) {
|
|
|
|
let warmingState = "disqualified";
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.warmingTabs.has(tab)) {
|
|
|
|
let tabState = this.getTabState(tab);
|
|
|
|
if (tabState == this.STATE_LOADING) {
|
|
|
|
warmingState = "stillLoading";
|
|
|
|
} else if (tabState == this.STATE_LOADED) {
|
|
|
|
warmingState = "loaded";
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
} else if (this.canWarmTab(tab)) {
|
|
|
|
warmingState = "notWarmed";
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
Services.telemetry
|
|
|
|
.getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE")
|
|
|
|
.add(warmingState);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this._requestingTab = true;
|
|
|
|
this.logState("requestTab " + this.tinfo(tab));
|
|
|
|
this.startTabSwitch();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.requestedTab = tab;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
tab.linkedBrowser.setAttribute("primary", "true");
|
|
|
|
if (this.lastPrimaryTab && this.lastPrimaryTab != tab) {
|
|
|
|
this.lastPrimaryTab.linkedBrowser.removeAttribute("primary");
|
|
|
|
}
|
|
|
|
this.lastPrimaryTab = tab;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.suppressDisplayPortAndQueueUnload(this.requestedTab, this.UNLOAD_DELAY);
|
|
|
|
this._requestingTab = false;
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
suppressDisplayPortAndQueueUnload(tab, unloadTimeout) {
|
|
|
|
let browser = tab.linkedBrowser;
|
|
|
|
let fl = browser.frameLoader;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (fl && fl.tabParent && !this.activeSuppressDisplayport.has(fl.tabParent)) {
|
|
|
|
fl.tabParent.suppressDisplayport(true);
|
|
|
|
this.activeSuppressDisplayport.add(fl.tabParent);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.preActions();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (this.unloadTimer) {
|
|
|
|
this.clearTimer(this.unloadTimer);
|
|
|
|
}
|
|
|
|
this.unloadTimer = this.setTimer(() => this.onUnloadTimeout(), unloadTimeout);
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.postActions();
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
handleEvent(event, delayed = false) {
|
|
|
|
if (this._processing) {
|
|
|
|
this.setTimer(() => this.handleEvent(event, true), 0);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (delayed && this.tabbrowser._switcher != this) {
|
|
|
|
// if we delayed processing this event, we might be out of date, in which
|
|
|
|
// case we drop the delayed events
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this._processing = true;
|
|
|
|
this.preActions();
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
if (event.type == "MozLayerTreeReady") {
|
|
|
|
this.onLayersReady(event.originalTarget);
|
|
|
|
}
|
|
|
|
if (event.type == "MozAfterPaint") {
|
|
|
|
this.onPaint();
|
|
|
|
} else if (event.type == "MozLayerTreeCleared") {
|
|
|
|
this.onLayersCleared(event.originalTarget);
|
|
|
|
} else if (event.type == "TabRemotenessChange") {
|
|
|
|
this.onRemotenessChange(event.target);
|
|
|
|
} else if (event.type == "sizemodechange" ||
|
|
|
|
event.type == "occlusionstatechange") {
|
|
|
|
this.onSizeModeOrOcclusionStateChange();
|
|
|
|
} else if (event.type == "SwapDocShells") {
|
|
|
|
this.onSwapDocShells(event.originalTarget, event.detail);
|
|
|
|
} else if (event.type == "EndSwapDocShells") {
|
|
|
|
this.onEndSwapDocShells(event.originalTarget, event.detail);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this.postActions();
|
|
|
|
this._processing = false;
|
|
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Telemetry and Profiler related helpers for recording tab switch
|
|
|
|
* timing.
|
|
|
|
*/
|
|
|
|
|
|
|
|
startTabSwitch() {
|
|
|
|
TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
|
|
this.addMarker("AsyncTabSwitch:Start");
|
|
|
|
this.switchInProgress = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Something has occurred that might mean that we've completed
|
|
|
|
* the tab switch (layers are ready, paints are done, spinners
|
|
|
|
* are hidden). This checks to make sure all conditions are
|
|
|
|
* satisfied, and then records the tab switch as finished.
|
|
|
|
*/
|
|
|
|
maybeFinishTabSwitch() {
|
|
|
|
if (this.switchInProgress && this.requestedTab &&
|
|
|
|
(this.getTabState(this.requestedTab) == this.STATE_LOADED ||
|
|
|
|
this.requestedTab === this.blankTab)) {
|
|
|
|
// After this point the tab has switched from the content thread's point of view.
|
|
|
|
// The changes will be visible after the next refresh driver tick + composite.
|
|
|
|
let time = TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
|
|
if (time != -1) {
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", window);
|
|
|
|
this.log("DEBUG: tab switch time = " + time);
|
|
|
|
this.addMarker("AsyncTabSwitch:Finish");
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
this.switchInProgress = false;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
spinnerDisplayed() {
|
|
|
|
this.assert(!this.spinnerTab);
|
|
|
|
let browser = this.requestedTab.linkedBrowser;
|
|
|
|
this.assert(browser.isRemoteBrowser);
|
|
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
|
|
|
|
// We have a second, similar probe for capturing recordings of
|
|
|
|
// when the spinner is displayed for very long periods.
|
|
|
|
TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
|
|
|
|
this.addMarker("AsyncTabSwitch:SpinnerShown");
|
|
|
|
},
|
|
|
|
|
|
|
|
spinnerHidden() {
|
|
|
|
this.assert(this.spinnerTab);
|
|
|
|
this.log("DEBUG: spinner time = " +
|
|
|
|
TelemetryStopwatch.timeElapsed("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window));
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
|
|
|
|
TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
|
|
|
|
this.addMarker("AsyncTabSwitch:SpinnerHidden");
|
|
|
|
// we do not get a onPaint after displaying the spinner
|
|
|
|
},
|
|
|
|
|
|
|
|
addMarker(marker) {
|
|
|
|
if (Services.profiler) {
|
|
|
|
Services.profiler.AddMarker(marker);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Debug related logging for switcher.
|
|
|
|
*/
|
|
|
|
|
|
|
|
_useDumpForLogging: false,
|
|
|
|
_logInit: false,
|
|
|
|
|
|
|
|
logging() {
|
|
|
|
if (this._useDumpForLogging)
|
|
|
|
return true;
|
|
|
|
if (this._logInit)
|
|
|
|
return this._shouldLog;
|
|
|
|
let result = Services.prefs.getBoolPref("browser.tabs.remote.logSwitchTiming", false);
|
|
|
|
this._shouldLog = result;
|
|
|
|
this._logInit = true;
|
|
|
|
return this._shouldLog;
|
|
|
|
},
|
|
|
|
|
|
|
|
tinfo(tab) {
|
|
|
|
if (tab) {
|
|
|
|
return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
|
|
|
|
}
|
|
|
|
return "null";
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
log(s) {
|
|
|
|
if (!this.logging())
|
|
|
|
return;
|
|
|
|
if (this._useDumpForLogging) {
|
|
|
|
dump(s + "\n");
|
|
|
|
} else {
|
|
|
|
Services.console.logStringMessage(s);
|
|
|
|
}
|
|
|
|
},
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
logState(prefix) {
|
|
|
|
if (!this.logging())
|
|
|
|
return;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let accum = prefix + " ";
|
|
|
|
for (let i = 0; i < this.tabbrowser.tabs.length; i++) {
|
|
|
|
let tab = this.tabbrowser.tabs[i];
|
|
|
|
let state = this.getTabState(tab);
|
|
|
|
let isWarming = this.warmingTabs.has(tab);
|
|
|
|
|
|
|
|
accum += i + ":";
|
|
|
|
if (tab === this.lastVisibleTab) accum += "V";
|
|
|
|
if (tab === this.loadingTab) accum += "L";
|
|
|
|
if (tab === this.requestedTab) accum += "R";
|
|
|
|
if (tab === this.blankTab) accum += "B";
|
|
|
|
if (isWarming) accum += "(W)";
|
|
|
|
if (state == this.STATE_LOADED) accum += "(+)";
|
|
|
|
if (state == this.STATE_LOADING) accum += "(+?)";
|
|
|
|
if (state == this.STATE_UNLOADED) accum += "(-)";
|
|
|
|
if (state == this.STATE_UNLOADING) accum += "(-?)";
|
|
|
|
accum += " ";
|
|
|
|
}
|
|
|
|
if (this._useDumpForLogging) {
|
|
|
|
dump(accum + "\n");
|
|
|
|
} else {
|
|
|
|
Services.console.logStringMessage(accum);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
};
|
|
|
|
this._switcher = switcher;
|
|
|
|
switcher.init();
|
|
|
|
return switcher;
|
|
|
|
}
|
|
|
|
|
|
|
|
warmupTab(aTab) {
|
|
|
|
if (gMultiProcessBrowser) {
|
|
|
|
this._getSwitcher().warmupTab(aTab);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleKeyDownEvent(aEvent) {
|
|
|
|
if (!aEvent.isTrusted) {
|
|
|
|
// Don't let untrusted events mess with tabs.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (aEvent.altKey)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// Don't check if the event was already consumed because tab
|
|
|
|
// navigation should always work for better user experience.
|
|
|
|
|
|
|
|
if (aEvent.ctrlKey && aEvent.shiftKey && !aEvent.metaKey) {
|
|
|
|
switch (aEvent.keyCode) {
|
|
|
|
case aEvent.DOM_VK_PAGE_UP:
|
|
|
|
this.moveTabBackward();
|
|
|
|
aEvent.preventDefault();
|
|
|
|
return;
|
|
|
|
case aEvent.DOM_VK_PAGE_DOWN:
|
|
|
|
this.moveTabForward();
|
|
|
|
aEvent.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (AppConstants.platform != "macosx") {
|
|
|
|
if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey &&
|
|
|
|
aEvent.keyCode == KeyEvent.DOM_VK_F4 &&
|
|
|
|
!this.mCurrentTab.pinned) {
|
|
|
|
this.removeCurrentTab({ animate: true });
|
|
|
|
aEvent.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_handleKeyPressEventMac(aEvent) {
|
|
|
|
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.container).direction == "ltr")
|
|
|
|
offset *= -1;
|
|
|
|
this.tabContainer.advanceSelectedTab(offset, true);
|
|
|
|
aEvent.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
createTooltip(event) {
|
|
|
|
event.stopPropagation();
|
|
|
|
var tab = document.tooltipNode;
|
|
|
|
if (tab.localName != "tab") {
|
|
|
|
event.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let stringWithShortcut = (stringId, keyElemId) => {
|
|
|
|
let keyElem = document.getElementById(keyElemId);
|
|
|
|
let shortcut = ShortcutUtils.prettifyShortcut(keyElem);
|
|
|
|
return gTabBrowserBundle.formatStringFromName(stringId, [shortcut], 1);
|
|
|
|
};
|
|
|
|
|
|
|
|
var label;
|
|
|
|
if (tab.mOverCloseButton) {
|
|
|
|
label = tab.selected ?
|
|
|
|
stringWithShortcut("tabs.closeSelectedTab.tooltip", "key_close") :
|
|
|
|
gTabBrowserBundle.GetStringFromName("tabs.closeTab.tooltip");
|
|
|
|
} else if (tab._overPlayingIcon) {
|
|
|
|
let stringID;
|
|
|
|
if (tab.selected) {
|
|
|
|
stringID = tab.linkedBrowser.audioMuted ?
|
|
|
|
"tabs.unmuteAudio.tooltip" :
|
|
|
|
"tabs.muteAudio.tooltip";
|
|
|
|
label = stringWithShortcut(stringID, "key_toggleMute");
|
|
|
|
} else {
|
|
|
|
if (tab.hasAttribute("activemedia-blocked")) {
|
|
|
|
stringID = "tabs.unblockAudio.tooltip";
|
|
|
|
} else {
|
|
|
|
stringID = tab.linkedBrowser.audioMuted ?
|
|
|
|
"tabs.unmuteAudio.background.tooltip" :
|
|
|
|
"tabs.muteAudio.background.tooltip";
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
label = gTabBrowserBundle.GetStringFromName(stringID);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
label = tab._fullLabel || tab.getAttribute("label");
|
|
|
|
if (AppConstants.NIGHTLY_BUILD &&
|
|
|
|
tab.linkedBrowser &&
|
|
|
|
tab.linkedBrowser.isRemoteBrowser &&
|
|
|
|
tab.linkedBrowser.frameLoader) {
|
|
|
|
label += " (pid " + tab.linkedBrowser.frameLoader.tabParent.osPid + ")";
|
|
|
|
}
|
|
|
|
if (tab.userContextId) {
|
|
|
|
label = gTabBrowserBundle.formatStringFromName("tabs.containers.tooltip", [label, ContextualIdentityService.getUserContextLabel(tab.userContextId)], 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
event.target.setAttribute("label", label);
|
|
|
|
}
|
|
|
|
|
|
|
|
handleEvent(aEvent) {
|
|
|
|
switch (aEvent.type) {
|
|
|
|
case "keydown":
|
|
|
|
this._handleKeyDownEvent(aEvent);
|
|
|
|
break;
|
|
|
|
case "keypress":
|
|
|
|
this._handleKeyPressEventMac(aEvent);
|
|
|
|
break;
|
|
|
|
case "sizemodechange":
|
|
|
|
case "occlusionstatechange":
|
|
|
|
if (aEvent.target == window && !this._switcher) {
|
|
|
|
this.mCurrentBrowser.preserveLayers(
|
|
|
|
window.windowState == window.STATE_MINIMIZED || window.isFullyOccluded);
|
|
|
|
this.mCurrentBrowser.docShellIsActive = this.shouldActivateDocShell(this.mCurrentBrowser);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
receiveMessage(aMessage) {
|
|
|
|
let data = aMessage.data;
|
|
|
|
let browser = aMessage.target;
|
|
|
|
|
|
|
|
switch (aMessage.name) {
|
|
|
|
case "DOMTitleChanged":
|
|
|
|
{
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (!tab || tab.hasAttribute("pending"))
|
2018-02-27 17:47:52 +00:00
|
|
|
return undefined;
|
2018-02-27 18:04:14 +00:00
|
|
|
let titleChanged = this.setTabTitle(tab);
|
|
|
|
if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
|
|
|
|
tab.setAttribute("titlechanged", "true");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "DOMWindowClose":
|
|
|
|
{
|
|
|
|
if (this.tabs.length == 1) {
|
|
|
|
// We already did PermitUnload in the content process
|
|
|
|
// for this tab (the only one in the window). So we don't
|
|
|
|
// need to do it again for any tabs.
|
|
|
|
window.skipNextCanClose = true;
|
|
|
|
window.close();
|
|
|
|
return undefined;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (tab) {
|
|
|
|
// Skip running PermitUnload since it already happened in
|
|
|
|
// the content process.
|
|
|
|
this.removeTab(tab, { skipPermitUnload: true });
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "contextmenu":
|
|
|
|
{
|
|
|
|
openContextMenu(aMessage);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "DOMWindowFocus":
|
|
|
|
{
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (!tab)
|
|
|
|
return undefined;
|
|
|
|
this.selectedTab = tab;
|
|
|
|
window.focus();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "Browser:Init":
|
|
|
|
{
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (!tab)
|
|
|
|
return undefined;
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this._outerWindowIDBrowserMap.set(browser.outerWindowID, browser);
|
|
|
|
browser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned });
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "Browser:WindowCreated":
|
|
|
|
{
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
if (tab && data.userContextId) {
|
|
|
|
ContextualIdentityService.telemetry(data.userContextId);
|
|
|
|
tab.setUserContextId(data.userContextId);
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// We don't want to update the container icon and identifier if
|
|
|
|
// this is not the selected browser.
|
|
|
|
if (browser == gBrowser.selectedBrowser) {
|
|
|
|
updateUserContextUIIndicator();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "Findbar:Keypress":
|
|
|
|
{
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
// If the find bar for this tab is not yet alive, only initialize
|
|
|
|
// it if there's a possibility FindAsYouType will be used.
|
|
|
|
// There's no point in doing it for most random keypresses.
|
|
|
|
if (!this.isFindBarInitialized(tab) &&
|
|
|
|
data.shouldFastFind) {
|
|
|
|
let shouldFastFind = this._findAsYouType;
|
|
|
|
if (!shouldFastFind) {
|
|
|
|
// Please keep in sync with toolkit/content/widgets/findbar.xml
|
|
|
|
const FAYT_LINKS_KEY = "'";
|
|
|
|
const FAYT_TEXT_KEY = "/";
|
|
|
|
let charCode = data.fakeEvent.charCode;
|
|
|
|
let key = charCode ? String.fromCharCode(charCode) : null;
|
|
|
|
shouldFastFind = key == FAYT_LINKS_KEY || key == FAYT_TEXT_KEY;
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
if (shouldFastFind) {
|
|
|
|
// Make sure we return the result.
|
|
|
|
return this.getFindBar(tab).receiveMessage(aMessage);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "RefreshBlocker:Blocked":
|
|
|
|
{
|
|
|
|
// The data object is expected to contain the following properties:
|
|
|
|
// - URI (string)
|
|
|
|
// The URI that a page is attempting to refresh or redirect to.
|
|
|
|
// - delay (int)
|
|
|
|
// The delay (in milliseconds) before the page was going to
|
|
|
|
// reload or redirect.
|
|
|
|
// - sameURI (bool)
|
|
|
|
// true if we're refreshing the page. false if we're redirecting.
|
|
|
|
// - outerWindowID (int)
|
|
|
|
// The outerWindowID of the frame that requested the refresh or
|
|
|
|
// redirect.
|
|
|
|
|
|
|
|
let brandBundle = document.getElementById("bundle_brand");
|
|
|
|
let brandShortName = brandBundle.getString("brandShortName");
|
|
|
|
let message =
|
|
|
|
gNavigatorBundle.getFormattedString("refreshBlocked." +
|
|
|
|
(data.sameURI ? "refreshLabel"
|
|
|
|
: "redirectLabel"),
|
|
|
|
[brandShortName]);
|
|
|
|
|
|
|
|
let notificationBox = this.getNotificationBox(browser);
|
|
|
|
let notification = notificationBox.getNotificationWithValue("refresh-blocked");
|
|
|
|
|
|
|
|
if (notification) {
|
|
|
|
notification.label = message;
|
|
|
|
} else {
|
|
|
|
let refreshButtonText =
|
|
|
|
gNavigatorBundle.getString("refreshBlocked.goButton");
|
|
|
|
let refreshButtonAccesskey =
|
|
|
|
gNavigatorBundle.getString("refreshBlocked.goButton.accesskey");
|
|
|
|
|
|
|
|
let buttons = [{
|
|
|
|
label: refreshButtonText,
|
|
|
|
accessKey: refreshButtonAccesskey,
|
|
|
|
callback() {
|
|
|
|
if (browser.messageManager) {
|
|
|
|
browser.messageManager.sendAsyncMessage("RefreshBlocker:Refresh", data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
|
|
|
notificationBox.appendNotification(message, "refresh-blocked",
|
|
|
|
"chrome://browser/skin/notification-icons/popup.svg",
|
|
|
|
notificationBox.PRIORITY_INFO_MEDIUM,
|
|
|
|
buttons);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
observe(aSubject, aTopic, aData) {
|
|
|
|
switch (aTopic) {
|
|
|
|
case "contextual-identity-updated":
|
|
|
|
{
|
|
|
|
for (let tab of this.tabs) {
|
|
|
|
if (tab.getAttribute("usercontextid") == aData) {
|
|
|
|
ContextualIdentityService.setTabStyle(tab);
|
2018-02-27 17:47:52 +00:00
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case "nsPref:changed":
|
|
|
|
{
|
|
|
|
// This is the only pref observed.
|
|
|
|
this._findAsYouType = Services.prefs.getBoolPref("accessibility.typeaheadfind");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_updateNewTabVisibility() {
|
|
|
|
// Helper functions to help deal with customize mode wrapping some items
|
|
|
|
let wrap = n => n.parentNode.localName == "toolbarpaletteitem" ? n.parentNode : n;
|
|
|
|
let unwrap = n => n && n.localName == "toolbarpaletteitem" ? n.firstElementChild : n;
|
|
|
|
|
|
|
|
let sib = this.tabContainer;
|
|
|
|
do {
|
|
|
|
sib = unwrap(wrap(sib).nextElementSibling);
|
|
|
|
} while (sib && sib.hidden);
|
|
|
|
|
|
|
|
const kAttr = "hasadjacentnewtabbutton";
|
|
|
|
if (sib && sib.id == "new-tab-button") {
|
|
|
|
this.tabContainer.setAttribute(kAttr, "true");
|
|
|
|
} else {
|
|
|
|
this.tabContainer.removeAttribute(kAttr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onWidgetAfterDOMChange(aNode, aNextNode, aContainer) {
|
|
|
|
if (aContainer.ownerDocument == document &&
|
|
|
|
aContainer.id == "TabsToolbar") {
|
|
|
|
this._updateNewTabVisibility();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onAreaNodeRegistered(aArea, aContainer) {
|
|
|
|
if (aContainer.ownerDocument == document &&
|
|
|
|
aArea == "TabsToolbar") {
|
|
|
|
this._updateNewTabVisibility();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onAreaReset(aArea, aContainer) {
|
|
|
|
this.onAreaNodeRegistered(aArea, aContainer);
|
|
|
|
}
|
|
|
|
|
|
|
|
_generateUniquePanelID() {
|
|
|
|
if (!this._uniquePanelIDCounter) {
|
|
|
|
this._uniquePanelIDCounter = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
let outerID = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
|
|
.getInterface(Ci.nsIDOMWindowUtils)
|
|
|
|
.outerWindowID;
|
|
|
|
|
|
|
|
// We want panel IDs to be globally unique, that's why we include the
|
|
|
|
// window ID. We switched to a monotonic counter as Date.now() lead
|
|
|
|
// to random failures because of colliding IDs.
|
|
|
|
return "panel-" + outerID + "-" + (++this._uniquePanelIDCounter);
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnectedCallback() {
|
|
|
|
Services.obs.removeObserver(this, "contextual-identity-updated");
|
|
|
|
|
|
|
|
CustomizableUI.removeListener(this);
|
|
|
|
|
|
|
|
for (let tab of this.tabs) {
|
|
|
|
let browser = tab.linkedBrowser;
|
|
|
|
if (browser.registeredOpenURI) {
|
|
|
|
this._unifiedComplete.unregisterOpenPage(browser.registeredOpenURI,
|
|
|
|
browser.getAttribute("usercontextid") || 0);
|
|
|
|
delete browser.registeredOpenURI;
|
|
|
|
}
|
|
|
|
|
|
|
|
let filter = this._tabFilters.get(tab);
|
|
|
|
if (filter) {
|
|
|
|
browser.webProgress.removeProgressListener(filter);
|
|
|
|
|
|
|
|
let listener = this._tabListeners.get(tab);
|
|
|
|
if (listener) {
|
|
|
|
filter.removeProgressListener(listener);
|
|
|
|
listener.destroy();
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
this._tabFilters.delete(tab);
|
|
|
|
this._tabListeners.delete(tab);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const nsIEventListenerService =
|
|
|
|
Components.interfaces.nsIEventListenerService;
|
|
|
|
let els = Components.classes["@mozilla.org/eventlistenerservice;1"]
|
|
|
|
.getService(nsIEventListenerService);
|
|
|
|
els.removeSystemEventListener(document, "keydown", this, false);
|
|
|
|
if (AppConstants.platform == "macosx") {
|
|
|
|
els.removeSystemEventListener(document, "keypress", this, false);
|
|
|
|
}
|
|
|
|
window.removeEventListener("sizemodechange", this);
|
|
|
|
window.removeEventListener("occlusionstatechange", this);
|
|
|
|
|
|
|
|
if (gMultiProcessBrowser) {
|
|
|
|
let messageManager = window.getGroupMessageManager("browsers");
|
|
|
|
messageManager.removeMessageListener("DOMTitleChanged", this);
|
|
|
|
window.messageManager.removeMessageListener("contextmenu", this);
|
|
|
|
|
|
|
|
if (this._switcher) {
|
|
|
|
this._switcher.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Services.prefs.removeObserver("accessibility.typeaheadfind", this);
|
|
|
|
}
|
|
|
|
|
|
|
|
_setupEventListeners() {
|
|
|
|
this.addEventListener("DOMWindowClose", (event) => {
|
|
|
|
if (!event.isTrusted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this.tabs.length == 1) {
|
|
|
|
// We already did PermitUnload in nsGlobalWindow::Close
|
|
|
|
// for this tab. There are no other tabs we need to do
|
|
|
|
// PermitUnload for.
|
|
|
|
window.skipNextCanClose = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var tab = this._getTabForContentWindow(event.target);
|
|
|
|
if (tab) {
|
|
|
|
// Skip running PermitUnload since it already happened.
|
|
|
|
this.removeTab(tab, { skipPermitUnload: true });
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
this.addEventListener("DOMWillOpenModalDialog", (event) => {
|
|
|
|
if (!event.isTrusted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let targetIsWindow = event.target instanceof Window;
|
|
|
|
|
|
|
|
// We're about to open a modal dialog, so figure out for which tab:
|
|
|
|
// If this is a same-process modal dialog, then we're given its DOM
|
|
|
|
// window as the event's target. For remote dialogs, we're given the
|
|
|
|
// browser, but that's in the originalTarget and not the target,
|
|
|
|
// because it's across the tabbrowser's XBL boundary.
|
|
|
|
let tabForEvent = targetIsWindow ?
|
|
|
|
this._getTabForContentWindow(event.target.top) :
|
|
|
|
this.getTabForBrowser(event.originalTarget);
|
|
|
|
|
|
|
|
// Focus window for beforeunload dialog so it is seen but don't
|
|
|
|
// steal focus from other applications.
|
|
|
|
if (event.detail &&
|
|
|
|
event.detail.tabPrompt &&
|
|
|
|
event.detail.inPermitUnload &&
|
|
|
|
Services.focus.activeWindow)
|
|
|
|
window.focus();
|
|
|
|
|
|
|
|
// Don't need to act if the tab is already selected or if there isn't
|
|
|
|
// a tab for the event (e.g. for the webextensions options_ui remote
|
|
|
|
// browsers embedded in the "about:addons" page):
|
|
|
|
if (!tabForEvent || tabForEvent.selected)
|
|
|
|
return;
|
|
|
|
|
|
|
|
// We always switch tabs for beforeunload tab-modal prompts.
|
|
|
|
if (event.detail &&
|
|
|
|
event.detail.tabPrompt &&
|
|
|
|
!event.detail.inPermitUnload) {
|
|
|
|
let docPrincipal = targetIsWindow ? event.target.document.nodePrincipal : null;
|
|
|
|
// At least one of these should/will be non-null:
|
|
|
|
let promptPrincipal = event.detail.promptPrincipal || docPrincipal ||
|
|
|
|
tabForEvent.linkedBrowser.contentPrincipal;
|
|
|
|
// For null principals, we bail immediately and don't show the checkbox:
|
|
|
|
if (!promptPrincipal || promptPrincipal.isNullPrincipal) {
|
|
|
|
tabForEvent.setAttribute("attention", "true");
|
|
|
|
return;
|
|
|
|
}
|
2018-02-27 17:47:52 +00:00
|
|
|
|
2018-02-27 18:04:14 +00:00
|
|
|
// For non-system/expanded principals, we bail and show the checkbox
|
|
|
|
if (promptPrincipal.URI &&
|
|
|
|
!Services.scriptSecurityManager.isSystemPrincipal(promptPrincipal)) {
|
|
|
|
let permission = Services.perms.testPermissionFromPrincipal(promptPrincipal,
|
|
|
|
"focus-tab-by-prompt");
|
|
|
|
if (permission != Services.perms.ALLOW_ACTION) {
|
|
|
|
// Tell the prompt box we want to show the user a checkbox:
|
|
|
|
let tabPrompt = this.getTabModalPromptBox(tabForEvent.linkedBrowser);
|
|
|
|
tabPrompt.onNextPromptShowAllowFocusCheckboxFor(promptPrincipal);
|
|
|
|
tabForEvent.setAttribute("attention", "true");
|
2018-02-27 17:47:52 +00:00
|
|
|
return;
|
|
|
|
}
|
2018-02-27 18:04:14 +00:00
|
|
|
}
|
|
|
|
// ... so system and expanded principals, as well as permitted "normal"
|
|
|
|
// URI-based principals, always get to steal focus for the tab when prompting.
|
|
|
|
}
|
|
|
|
|
|
|
|
// If permissions/origins dictate so, bring tab to the front.
|
|
|
|
this.selectedTab = tabForEvent;
|
|
|
|
}, true);
|
|
|
|
|
|
|
|
this.addEventListener("DOMTitleChanged", (event) => {
|
|
|
|
if (!event.isTrusted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var contentWin = event.target.defaultView;
|
|
|
|
if (contentWin != contentWin.top)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var tab = this._getTabForContentWindow(contentWin);
|
|
|
|
if (!tab || tab.hasAttribute("pending"))
|
|
|
|
return;
|
|
|
|
|
|
|
|
var titleChanged = this.setTabTitle(tab);
|
|
|
|
if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
|
|
|
|
tab.setAttribute("titlechanged", "true");
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addEventListener("oop-browser-crashed", (event) => {
|
|
|
|
if (!event.isTrusted)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let browser = event.originalTarget;
|
|
|
|
|
|
|
|
// Preloaded browsers do not actually have any tabs. If one crashes,
|
|
|
|
// it should be released and removed.
|
|
|
|
if (browser === this._preloadedBrowser) {
|
|
|
|
this.removePreloadedBrowser();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let icon = browser.mIconURL;
|
|
|
|
let tab = this.getTabForBrowser(browser);
|
|
|
|
|
|
|
|
if (this.selectedBrowser == browser) {
|
|
|
|
TabCrashHandler.onSelectedBrowserCrash(browser);
|
|
|
|
} else {
|
|
|
|
this.updateBrowserRemoteness(browser, false);
|
|
|
|
SessionStore.reviveCrashedTab(tab);
|
|
|
|
}
|
|
|
|
|
|
|
|
tab.removeAttribute("soundplaying");
|
|
|
|
this.setIcon(tab, icon, browser.contentPrincipal, browser.contentRequestContextID);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addEventListener("DOMAudioPlaybackStarted", (event) => {
|
|
|
|
var tab = this.getTabFromAudioEvent(event);
|
|
|
|
if (!tab) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
clearTimeout(tab._soundPlayingAttrRemovalTimer);
|
|
|
|
tab._soundPlayingAttrRemovalTimer = 0;
|
|
|
|
|
|
|
|
let modifiedAttrs = [];
|
|
|
|
if (tab.hasAttribute("soundplaying-scheduledremoval")) {
|
|
|
|
tab.removeAttribute("soundplaying-scheduledremoval");
|
|
|
|
modifiedAttrs.push("soundplaying-scheduledremoval");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!tab.hasAttribute("soundplaying")) {
|
|
|
|
tab.setAttribute("soundplaying", true);
|
|
|
|
modifiedAttrs.push("soundplaying");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (modifiedAttrs.length) {
|
|
|
|
// Flush style so that the opacity takes effect immediately, in
|
|
|
|
// case the media is stopped before the style flushes naturally.
|
|
|
|
getComputedStyle(tab).opacity;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._tabAttrModified(tab, modifiedAttrs);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addEventListener("DOMAudioPlaybackStopped", (event) => {
|
|
|
|
var tab = this.getTabFromAudioEvent(event);
|
|
|
|
if (!tab) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tab.hasAttribute("soundplaying")) {
|
|
|
|
let removalDelay = Services.prefs.getIntPref("browser.tabs.delayHidingAudioPlayingIconMS");
|
|
|
|
|
|
|
|
tab.style.setProperty("--soundplaying-removal-delay", `${removalDelay - 300}ms`);
|
|
|
|
tab.setAttribute("soundplaying-scheduledremoval", "true");
|
|
|
|
this._tabAttrModified(tab, ["soundplaying-scheduledremoval"]);
|
|
|
|
|
|
|
|
tab._soundPlayingAttrRemovalTimer = setTimeout(() => {
|
|
|
|
tab.removeAttribute("soundplaying-scheduledremoval");
|
2018-02-27 17:47:52 +00:00
|
|
|
tab.removeAttribute("soundplaying");
|
2018-02-27 18:04:14 +00:00
|
|
|
this._tabAttrModified(tab, ["soundplaying", "soundplaying-scheduledremoval"]);
|
|
|
|
}, removalDelay);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addEventListener("DOMAudioPlaybackBlockStarted", (event) => {
|
|
|
|
var tab = this.getTabFromAudioEvent(event);
|
|
|
|
if (!tab) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!tab.hasAttribute("activemedia-blocked")) {
|
|
|
|
tab.setAttribute("activemedia-blocked", true);
|
|
|
|
this._tabAttrModified(tab, ["activemedia-blocked"]);
|
|
|
|
tab.startMediaBlockTimer();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addEventListener("DOMAudioPlaybackBlockStopped", (event) => {
|
|
|
|
var tab = this.getTabFromAudioEvent(event);
|
|
|
|
if (!tab) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (tab.hasAttribute("activemedia-blocked")) {
|
|
|
|
tab.removeAttribute("activemedia-blocked");
|
|
|
|
this._tabAttrModified(tab, ["activemedia-blocked"]);
|
|
|
|
let hist = Services.telemetry.getHistogramById("TAB_AUDIO_INDICATOR_USED");
|
|
|
|
hist.add(2 /* unblockByVisitingTab */ );
|
|
|
|
tab.finishMediaBlockTimer();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|