Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / browser / modules / BrowserWindowTracker.sys.mjs
blobf6718dd01b95e7f2b4182542ee44b440da3ea299
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/. */
5 /*
6  * This module tracks each browser window and informs network module
7  * the current selected tab's content outer window ID.
8  */
10 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
11 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
13 const lazy = {};
15 // Lazy getters
17 XPCOMUtils.defineLazyServiceGetters(lazy, {
18   BrowserHandler: ["@mozilla.org/browser/clh;1", "nsIBrowserHandler"],
19 });
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",
25 });
27 // Constants
28 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
29 const WINDOW_EVENTS = ["activate", "unload"];
30 const DEBUG = false;
32 // Variables
33 let _lastCurrentBrowserId = 0;
34 let _trackedWindows = [];
36 // Global methods
37 function debug(s) {
38   if (DEBUG) {
39     dump("-*- UpdateBrowserIDHelper: " + s + "\n");
40   }
43 function _updateCurrentBrowserId(browser) {
44   if (
45     !browser.browserId ||
46     browser.browserId === _lastCurrentBrowserId ||
47     browser.ownerGlobal != _trackedWindows[0]
48   ) {
49     return;
50   }
52   // Guard on DEBUG here because materializing a long data URI into
53   // a JS string for concatenation is not free.
54   if (DEBUG) {
55     debug(
56       `Current window uri=${browser.currentURI?.spec} browser id=${browser.browserId}`
57     );
58   }
60   _lastCurrentBrowserId = browser.browserId;
61   let idWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
62     Ci.nsISupportsPRUint64
63   );
64   idWrapper.data = _lastCurrentBrowserId;
65   Services.obs.notifyObservers(idWrapper, "net:current-browser-id");
68 function _handleEvent(event) {
69   switch (event.type) {
70     case "TabBrowserInserted":
71       if (
72         event.target.ownerGlobal.gBrowser.selectedBrowser ===
73         event.target.linkedBrowser
74       ) {
75         _updateCurrentBrowserId(event.target.linkedBrowser);
76       }
77       break;
78     case "TabSelect":
79       _updateCurrentBrowserId(event.target.linkedBrowser);
80       break;
81     case "activate":
82       WindowHelper.onActivate(event.target);
83       break;
84     case "unload":
85       WindowHelper.removeWindow(event.currentTarget);
86       break;
87   }
90 function _trackWindowOrder(window) {
91   if (window.windowState == window.STATE_MINIMIZED) {
92     let firstMinimizedWindow = _trackedWindows.findIndex(
93       w => w.windowState == w.STATE_MINIMIZED
94     );
95     if (firstMinimizedWindow == -1) {
96       firstMinimizedWindow = _trackedWindows.length;
97     }
98     _trackedWindows.splice(firstMinimizedWindow, 0, window);
99   } else {
100     _trackedWindows.unshift(window);
101   }
104 function _untrackWindowOrder(window) {
105   let idx = _trackedWindows.indexOf(window);
106   if (idx >= 0) {
107     _trackedWindows.splice(idx, 1);
108   }
111 function topicObserved(observeTopic, checkFn) {
112   return new Promise((resolve, reject) => {
113     function observer(subject, topic, data) {
114       try {
115         if (checkFn && !checkFn(subject, data)) {
116           return;
117         }
118         Services.obs.removeObserver(observer, topic);
119         checkFn = null;
120         resolve([subject, data]);
121       } catch (ex) {
122         Services.obs.removeObserver(observer, topic);
123         checkFn = null;
124         reject(ex);
125       }
126     }
127     Services.obs.addObserver(observer, observeTopic);
128   });
131 // Methods that impact a window. Put into single object for organization.
132 var WindowHelper = {
133   addWindow(window) {
134     // Add event listeners
135     TAB_EVENTS.forEach(function (event) {
136       window.gBrowser.tabContainer.addEventListener(event, _handleEvent);
137     });
138     WINDOW_EVENTS.forEach(function (event) {
139       window.addEventListener(event, _handleEvent);
140     });
142     _trackWindowOrder(window);
144     // Update the selected tab's content outer window ID.
145     _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
146   },
148   removeWindow(window) {
149     _untrackWindowOrder(window);
151     // Remove the event listeners
152     TAB_EVENTS.forEach(function (event) {
153       window.gBrowser.tabContainer.removeEventListener(event, _handleEvent);
154     });
155     WINDOW_EVENTS.forEach(function (event) {
156       window.removeEventListener(event, _handleEvent);
157     });
158   },
160   onActivate(window) {
161     // If this window was the last focused window, we don't need to do anything
162     if (window == _trackedWindows[0]) {
163       return;
164     }
166     _untrackWindowOrder(window);
167     _trackWindowOrder(window);
169     _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
170   },
173 export const BrowserWindowTracker = {
174   pendingWindows: new Map(),
176   /**
177    * Get the most recent browser window.
178    *
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.
184    */
185   getTopWindow(options = {}) {
186     for (let win of _trackedWindows) {
187       if (
188         !win.closed &&
189         (options.allowPopups || win.toolbar.visible) &&
190         (!("private" in options) ||
191           lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
192           lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private)
193       ) {
194         return win;
195       }
196     }
197     return null;
198   },
200   /**
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.
204    *
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
210    *   groups.
211    *
212    * @returns {Promise<Window> | null}
213    */
214   getPendingWindow(options = {}) {
215     for (let pending of this.pendingWindows.values()) {
216       if (
217         !("private" in options) ||
218         lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
219         pending.isPrivate == options.private
220       ) {
221         return pending.deferred.promise;
222       }
223     }
224     return null;
225   },
227   /**
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
230    * this module.
231    *
232    * @param {Window} window
233    *   The opening window.
234    * @param {boolean} isPrivate
235    *   Whether the opening window is a private browsing window.
236    */
237   registerOpeningWindow(window, isPrivate) {
238     let deferred = lazy.PromiseUtils.defer();
240     this.pendingWindows.set(window, {
241       isPrivate,
242       deferred,
243     });
245     // Prevent leaks in case the window closes before we track it as an open
246     // window.
247     const topic = "browsing-context-discarded";
248     const observer = (aSubject, aTopic, aData) => {
249       if (window.browsingContext == aSubject) {
250         let pending = this.pendingWindows.get(window);
251         if (pending) {
252           this.pendingWindows.delete(window);
253           pending.deferred.resolve(window);
254         }
255         Services.obs.removeObserver(observer, topic);
256       }
257     };
258     Services.obs.addObserver(observer, topic);
259   },
261   /**
262    * A standard function for opening a new browser window.
263    *
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.
280    *
281    * @returns {Window}
282    */
283   openWindow({
284     openerWindow = undefined,
285     private: isPrivate = false,
286     features = undefined,
287     args = null,
288     remote = undefined,
289     fission = undefined,
290   } = {}) {
291     let telemetryObj = {};
292     TelemetryStopwatch.start("FX_NEW_WINDOW_MS", telemetryObj);
294     let windowFeatures = "chrome,dialog=no,all";
295     if (features) {
296       windowFeatures += `,${features}`;
297     }
298     let loadURIString;
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";
305       }
306     } else {
307       windowFeatures += ",non-private";
308     }
309     if (!args) {
310       loadURIString ??= lazy.BrowserHandler.defaultArgs;
311       args = Cc["@mozilla.org/supports-string;1"].createInstance(
312         Ci.nsISupportsString
313       );
314       args.data = loadURIString;
315     }
317     if (remote) {
318       windowFeatures += ",remote";
319     } else if (remote === false) {
320       windowFeatures += ",non-remote";
321     }
323     if (fission) {
324       windowFeatures += ",fission";
325     } else if (fission === false) {
326       windowFeatures += ",non-fission";
327     }
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";
334     }
336     let win = Services.ww.openWindow(
337       openerWindow,
338       AppConstants.BROWSER_CHROME_URL,
339       "_blank",
340       windowFeatures,
341       args
342     );
343     this.registerOpeningWindow(win, isPrivate);
345     win.addEventListener(
346       "MozAfterPaint",
347       () => {
348         TelemetryStopwatch.finish("FX_NEW_WINDOW_MS", telemetryObj);
349         if (
350           Services.prefs.getIntPref("browser.startup.page") == 1 &&
351           loadURIString == lazy.HomePage.get()
352         ) {
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");
357         }
358       },
359       { once: true }
360     );
362     return win;
363   },
365   /**
366    * Async version of `openWindow` waiting for delayed startup of the new
367    * window before returning.
368    *
369    * @param {Object} [options]
370    *   Options for the new window. See `openWindow` for details.
371    *
372    * @returns {Window}
373    */
374   async promiseOpenWindow(options) {
375     let win = this.openWindow(options);
376     await topicObserved(
377       "browser-delayed-startup-finished",
378       subject => subject == win
379     );
380     return win;
381   },
383   /**
384    * Number of currently open browser windows.
385    */
386   get windowCount() {
387     return _trackedWindows.length;
388   },
390   /**
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.
393    */
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];
398   },
400   getAllVisibleTabs() {
401     let tabs = [];
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 });
408         }
409       }
410     }
411     return tabs;
412   },
414   track(window) {
415     let pending = this.pendingWindows.get(window);
416     if (pending) {
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));
421     }
423     return WindowHelper.addWindow(window);
424   },
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;
431         }
432       }
433     }
434     return null;
435   },
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
439   // tests!
440   untrackForTestsOnly(window) {
441     return WindowHelper.removeWindow(window);
442   },