1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * This module tracks each browser window and informs network module
7 * the current selected tab's content outer window ID.
10 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
17 XPCOMUtils.defineLazyServiceGetters(lazy, {
18 BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
21 ChromeUtils.defineESModuleGetters(lazy, {
22 HomePage: "resource:///modules/HomePage.sys.mjs",
23 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
24 PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs",
28 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
29 const WINDOW_EVENTS = ["activate", "unload"];
33 let _lastCurrentBrowserId = 0;
34 let _trackedWindows = [];
39 dump("-*- UpdateBrowserIDHelper: " + s + "\n");
43 function _updateCurrentBrowserId(browser) {
46 browser.browserId === _lastCurrentBrowserId ||
47 browser.ownerGlobal != _trackedWindows[0]
52 // Guard on DEBUG here because materializing a long data URI into
53 // a JS string for concatenation is not free.
56 `Current window uri=${browser.currentURI?.spec} browser id=${browser.browserId}`
60 _lastCurrentBrowserId = browser.browserId;
61 let idWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
62 Ci.nsISupportsPRUint64
64 idWrapper.data = _lastCurrentBrowserId;
65 Services.obs.notifyObservers(idWrapper, "net:current-browser-id");
68 function _handleEvent(event) {
70 case "TabBrowserInserted":
72 event.target.ownerGlobal.gBrowser.selectedBrowser ===
73 event.target.linkedBrowser
75 _updateCurrentBrowserId(event.target.linkedBrowser);
79 _updateCurrentBrowserId(event.target.linkedBrowser);
82 WindowHelper.onActivate(event.target);
85 WindowHelper.removeWindow(event.currentTarget);
90 function _trackWindowOrder(window) {
91 if (window.windowState == window.STATE_MINIMIZED) {
92 let firstMinimizedWindow = _trackedWindows.findIndex(
93 w => w.windowState == w.STATE_MINIMIZED
95 if (firstMinimizedWindow == -1) {
96 firstMinimizedWindow = _trackedWindows.length;
98 _trackedWindows.splice(firstMinimizedWindow, 0, window);
100 _trackedWindows.unshift(window);
104 function _untrackWindowOrder(window) {
105 let idx = _trackedWindows.indexOf(window);
107 _trackedWindows.splice(idx, 1);
111 function topicObserved(observeTopic, checkFn) {
112 return new Promise((resolve, reject) => {
113 function observer(subject, topic, data) {
115 if (checkFn && !checkFn(subject, data)) {
118 Services.obs.removeObserver(observer, topic);
120 resolve([subject, data]);
122 Services.obs.removeObserver(observer, topic);
127 Services.obs.addObserver(observer, observeTopic);
131 // Methods that impact a window. Put into single object for organization.
134 // Add event listeners
135 TAB_EVENTS.forEach(function (event) {
136 window.gBrowser.tabContainer.addEventListener(event, _handleEvent);
138 WINDOW_EVENTS.forEach(function (event) {
139 window.addEventListener(event, _handleEvent);
142 _trackWindowOrder(window);
144 // Update the selected tab's content outer window ID.
145 _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
148 removeWindow(window) {
149 _untrackWindowOrder(window);
151 // Remove the event listeners
152 TAB_EVENTS.forEach(function (event) {
153 window.gBrowser.tabContainer.removeEventListener(event, _handleEvent);
155 WINDOW_EVENTS.forEach(function (event) {
156 window.removeEventListener(event, _handleEvent);
161 // If this window was the last focused window, we don't need to do anything
162 if (window == _trackedWindows[0]) {
166 _untrackWindowOrder(window);
167 _trackWindowOrder(window);
169 _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
173 export const BrowserWindowTracker = {
174 pendingWindows: new Map(),
177 * Get the most recent browser window.
179 * @param options an object accepting the arguments for the search.
180 * * private: true to restrict the search to private windows
181 * only, false to restrict the search to non-private only.
182 * Omit the property to search in both groups.
183 * * allowPopups: true if popup windows are permissable.
185 getTopWindow(options = {}) {
186 for (let win of _trackedWindows) {
189 (options.allowPopups || win.toolbar.visible) &&
190 (!("private" in options) ||
191 lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
192 lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private)
201 * Get a window that is in the process of loading. Only supports windows
202 * opened via the `openWindow` function in this module or that have been
203 * registered with the `registerOpeningWindow` function.
205 * @param {Object} options
206 * Options for the search.
207 * @param {boolean} [options.private]
208 * true to restrict the search to private windows only, false to restrict
209 * the search to non-private only. Omit the property to search in both
212 * @returns {Promise<Window> | null}
214 getPendingWindow(options = {}) {
215 for (let pending of this.pendingWindows.values()) {
217 !("private" in options) ||
218 lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
219 pending.isPrivate == options.private
221 return pending.deferred.promise;
228 * Registers a browser window that is in the process of opening. Normally it
229 * would be preferable to use the standard method for opening the window from
232 * @param {Window} window
233 * The opening window.
234 * @param {boolean} isPrivate
235 * Whether the opening window is a private browsing window.
237 registerOpeningWindow(window, isPrivate) {
238 let deferred = lazy.PromiseUtils.defer();
240 this.pendingWindows.set(window, {
245 // Prevent leaks in case the window closes before we track it as an open
247 const topic = "browsing-context-discarded";
248 const observer = (aSubject, aTopic, aData) => {
249 if (window.browsingContext == aSubject) {
250 let pending = this.pendingWindows.get(window);
252 this.pendingWindows.delete(window);
253 pending.deferred.resolve(window);
255 Services.obs.removeObserver(observer, topic);
258 Services.obs.addObserver(observer, topic);
262 * A standard function for opening a new browser window.
264 * @param {Object} [options]
265 * Options for the new window.
266 * @param {Window} [options.openerWindow]
267 * An existing browser window to open the new one from.
268 * @param {boolean} [options.private]
269 * True to make the window a private browsing window.
270 * @param {String} [options.features]
271 * Additional window features to give the new window.
272 * @param {nsIArray | nsISupportsString} [options.args]
273 * Arguments to pass to the new window.
274 * @param {boolean} [options.remote]
275 * A boolean indicating if the window should run remote browser tabs or
276 * not. If omitted, the window will choose the profile default state.
277 * @param {boolean} [options.fission]
278 * A boolean indicating if the window should run with fission enabled or
279 * not. If omitted, the window will choose the profile default state.
284 openerWindow = undefined,
285 private: isPrivate = false,
286 features = undefined,
291 let telemetryObj = {};
292 TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);
294 let windowFeatures = "chrome,dialog=no,all";
296 windowFeatures += `,${features}`;
299 if (isPrivate && lazy.PrivateBrowsingUtils.enabled) {
300 windowFeatures += ",private";
301 if (!args && !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
302 // Force the new window to load about:privatebrowsing instead of the
303 // default home page.
304 loadURIString = "about:privatebrowsing";
307 windowFeatures += ",non-private";
310 loadURIString ??= lazy.BrowserHandler.defaultArgs;
311 args = Cc["@mozilla.org/supports-string;1"].createInstance(
314 args.data = loadURIString;
318 windowFeatures += ",remote";
319 } else if (remote === false) {
320 windowFeatures += ",non-remote";
324 windowFeatures += ",fission";
325 } else if (fission === false) {
326 windowFeatures += ",non-fission";
329 // If the opener window is maximized, we want to skip the animation, since
330 // we're going to be taking up most of the screen anyways, and we want to
331 // optimize for showing the user a useful window as soon as possible.
332 if (openerWindow?.windowState == openerWindow?.STATE_MAXIMIZED) {
333 windowFeatures += ",suppressanimation";
336 let win = Services.ww.openWindow(
338 AppConstants.BROWSER_CHROME_URL,
343 this.registerOpeningWindow(win, isPrivate);
345 win.addEventListener(
348 TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
350 Services.prefs.getIntPref("browser.startup.page") == 1 &&
351 loadURIString == lazy.HomePage.get()
353 // A notification for when a user has triggered their homepage. This
354 // is used to display a doorhanger explaining that an extension has
355 // modified the homepage, if necessary.
356 Services.obs.notifyObservers(win, "browser-open-homepage-start");
366 * Async version of `openWindow` waiting for delayed startup of the new
367 * window before returning.
369 * @param {Object} [options]
370 * Options for the new window. See `openWindow` for details.
374 async promiseOpenWindow(options) {
375 let win = this.openWindow(options);
377 "browser-delayed-startup-finished",
378 subject => subject == win
384 * Number of currently open browser windows.
387 return _trackedWindows.length;
391 * Array of browser windows ordered by z-index, in reverse order.
392 * This means that the top-most browser window will be the first item.
394 get orderedWindows() {
395 // Clone the windows array immediately as it may change during iteration,
396 // we'd rather have an outdated order than skip/revisit windows.
397 return [..._trackedWindows];
400 getAllVisibleTabs() {
402 for (let win of BrowserWindowTracker.orderedWindows) {
403 for (let tab of win.gBrowser.visibleTabs) {
404 // Only use tabs which are not discarded / unrestored
405 if (tab.linkedPanel) {
406 let { contentTitle, browserId } = tab.linkedBrowser;
407 tabs.push({ contentTitle, browserId });
415 let pending = this.pendingWindows.get(window);
417 this.pendingWindows.delete(window);
418 // Waiting for delayed startup to complete ensures that this new window
419 // has started loading its initial urls.
420 window.delayedStartupPromise.then(() => pending.deferred.resolve(window));
423 return WindowHelper.addWindow(window);
426 getBrowserById(browserId) {
427 for (let win of BrowserWindowTracker.orderedWindows) {
428 for (let tab of win.gBrowser.visibleTabs) {
429 if (tab.linkedPanel && tab.linkedBrowser.browserId === browserId) {
430 return tab.linkedBrowser;
437 // For tests only, this function will remove this window from the list of
438 // tracked windows. Please don't forget to add it back at the end of your
440 untrackForTestsOnly(window) {
441 return WindowHelper.removeWindow(window);