diff options
Diffstat (limited to 'browser/modules/BrowserWindowTracker.jsm')
-rw-r--r-- | browser/modules/BrowserWindowTracker.jsm | 321 |
1 files changed, 321 insertions, 0 deletions
diff --git a/browser/modules/BrowserWindowTracker.jsm b/browser/modules/BrowserWindowTracker.jsm new file mode 100644 index 0000000000..6872c4d55d --- /dev/null +++ b/browser/modules/BrowserWindowTracker.jsm @@ -0,0 +1,321 @@ +/* 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/. */ + +/* + * This module tracks each browser window and informs network module + * the current selected tab's content outer window ID. + */ + +var EXPORTED_SYMBOLS = ["BrowserWindowTracker"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +// Lazy getters +ChromeUtils.defineESModuleGetters(lazy, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +// Constants +const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"]; +const WINDOW_EVENTS = ["activate", "unload"]; +const DEBUG = false; + +// Variables +let _lastCurrentBrowserId = 0; +let _trackedWindows = []; + +// Global methods +function debug(s) { + if (DEBUG) { + dump("-*- UpdateBrowserIDHelper: " + s + "\n"); + } +} + +function _updateCurrentBrowserId(browser) { + if ( + !browser.browserId || + browser.browserId === _lastCurrentBrowserId || + browser.ownerGlobal != _trackedWindows[0] + ) { + return; + } + + // Guard on DEBUG here because materializing a long data URI into + // a JS string for concatenation is not free. + if (DEBUG) { + debug( + `Current window uri=${browser.currentURI?.spec} browser id=${browser.browserId}` + ); + } + + _lastCurrentBrowserId = browser.browserId; + let idWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance( + Ci.nsISupportsPRUint64 + ); + idWrapper.data = _lastCurrentBrowserId; + Services.obs.notifyObservers(idWrapper, "net:current-browser-id"); +} + +function _handleEvent(event) { + switch (event.type) { + case "TabBrowserInserted": + if ( + event.target.ownerGlobal.gBrowser.selectedBrowser === + event.target.linkedBrowser + ) { + _updateCurrentBrowserId(event.target.linkedBrowser); + } + break; + case "TabSelect": + _updateCurrentBrowserId(event.target.linkedBrowser); + break; + case "activate": + WindowHelper.onActivate(event.target); + break; + case "unload": + WindowHelper.removeWindow(event.currentTarget); + break; + } +} + +function _trackWindowOrder(window) { + if (window.windowState == window.STATE_MINIMIZED) { + let firstMinimizedWindow = _trackedWindows.findIndex( + w => w.windowState == w.STATE_MINIMIZED + ); + if (firstMinimizedWindow == -1) { + firstMinimizedWindow = _trackedWindows.length; + } + _trackedWindows.splice(firstMinimizedWindow, 0, window); + } else { + _trackedWindows.unshift(window); + } +} + +function _untrackWindowOrder(window) { + let idx = _trackedWindows.indexOf(window); + if (idx >= 0) { + _trackedWindows.splice(idx, 1); + } +} + +// Methods that impact a window. Put into single object for organization. +var WindowHelper = { + addWindow(window) { + // Add event listeners + TAB_EVENTS.forEach(function (event) { + window.gBrowser.tabContainer.addEventListener(event, _handleEvent); + }); + WINDOW_EVENTS.forEach(function (event) { + window.addEventListener(event, _handleEvent); + }); + + _trackWindowOrder(window); + + // Update the selected tab's content outer window ID. + _updateCurrentBrowserId(window.gBrowser.selectedBrowser); + }, + + removeWindow(window) { + _untrackWindowOrder(window); + + // Remove the event listeners + TAB_EVENTS.forEach(function (event) { + window.gBrowser.tabContainer.removeEventListener(event, _handleEvent); + }); + WINDOW_EVENTS.forEach(function (event) { + window.removeEventListener(event, _handleEvent); + }); + }, + + onActivate(window) { + // If this window was the last focused window, we don't need to do anything + if (window == _trackedWindows[0]) { + return; + } + + _untrackWindowOrder(window); + _trackWindowOrder(window); + + _updateCurrentBrowserId(window.gBrowser.selectedBrowser); + }, +}; + +const BrowserWindowTracker = { + pendingWindows: new Map(), + + /** + * Get the most recent browser window. + * + * @param options an object accepting the arguments for the search. + * * private: true to restrict the search to private windows + * only, false to restrict the search to non-private only. + * Omit the property to search in both groups. + * * allowPopups: true if popup windows are permissable. + */ + getTopWindow(options = {}) { + for (let win of _trackedWindows) { + if ( + !win.closed && + (options.allowPopups || win.toolbar.visible) && + (!("private" in options) || + lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || + lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private) + ) { + return win; + } + } + return null; + }, + + /** + * Get a window that is in the process of loading. Only supports windows + * opened via the `openWindow` function in this module or that have been + * registered with the `registerOpeningWindow` function. + * + * @param {Object} options + * Options for the search. + * @param {boolean} [options.private] + * true to restrict the search to private windows only, false to restrict + * the search to non-private only. Omit the property to search in both + * groups. + * + * @returns {Promise<Window> | null} + */ + getPendingWindow(options = {}) { + for (let pending of this.pendingWindows.values()) { + if ( + !("private" in options) || + lazy.PrivateBrowsingUtils.permanentPrivateBrowsing || + pending.isPrivate == options.private + ) { + return pending.deferred.promise; + } + } + return null; + }, + + /** + * Registers a browser window that is in the process of opening. Normally it + * would be preferable to use the standard method for opening the window from + * this module. + * + * @param {Window} window + * The opening window. + * @param {boolean} isPrivate + * Whether the opening window is a private browsing window. + */ + registerOpeningWindow(window, isPrivate) { + let deferred = lazy.PromiseUtils.defer(); + + this.pendingWindows.set(window, { + isPrivate, + deferred, + }); + }, + + /** + * A standard function for opening a new browser window. + * + * @param {Object} [options] + * Options for the new window. + * @param {boolean} [options.private] + * True to make the window a private browsing window. + * @param {String} [options.features] + * Additional window features to give the new window. + * @param {nsIArray | nsISupportsString} [options.args] + * Arguments to pass to the new window. + * + * @returns {Window} + */ + openWindow({ + private: isPrivate = false, + features = undefined, + args = null, + } = {}) { + let windowFeatures = "chrome,dialog=no,all"; + if (features) { + windowFeatures += `,${features}`; + } + if (isPrivate) { + windowFeatures += ",private"; + } + + let win = Services.ww.openWindow( + null, + AppConstants.BROWSER_CHROME_URL, + "_blank", + windowFeatures, + args + ); + this.registerOpeningWindow(win, isPrivate); + return win; + }, + + /** + * Number of currently open browser windows. + */ + get windowCount() { + return _trackedWindows.length; + }, + + /** + * Array of browser windows ordered by z-index, in reverse order. + * This means that the top-most browser window will be the first item. + */ + get orderedWindows() { + // Clone the windows array immediately as it may change during iteration, + // we'd rather have an outdated order than skip/revisit windows. + return [..._trackedWindows]; + }, + + getAllVisibleTabs() { + let tabs = []; + for (let win of BrowserWindowTracker.orderedWindows) { + for (let tab of win.gBrowser.visibleTabs) { + // Only use tabs which are not discarded / unrestored + if (tab.linkedPanel) { + let { contentTitle, browserId } = tab.linkedBrowser; + tabs.push({ contentTitle, browserId }); + } + } + } + return tabs; + }, + + track(window) { + let pending = this.pendingWindows.get(window); + if (pending) { + this.pendingWindows.delete(window); + // Waiting for delayed startup to complete ensures that this new window + // has started loading its initial urls. + window.delayedStartupPromise.then(() => pending.deferred.resolve(window)); + } + + return WindowHelper.addWindow(window); + }, + + getBrowserById(browserId) { + for (let win of BrowserWindowTracker.orderedWindows) { + for (let tab of win.gBrowser.visibleTabs) { + if (tab.linkedPanel && tab.linkedBrowser.browserId === browserId) { + return tab.linkedBrowser; + } + } + } + return null; + }, + + // For tests only, this function will remove this window from the list of + // tracked windows. Please don't forget to add it back at the end of your + // tests! + untrackForTestsOnly(window) { + return WindowHelper.removeWindow(window); + }, +}; |