Bug 1890689 Don't pretend to pre-buffer with DynamicResampler r=pehrsons
[gecko.git] / browser / modules / BrowserWindowTracker.sys.mjs
blobb6e3ad2eea42fe5dd4f69bc15e8931229214e84c
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 });
26 // Constants
27 const TAB_EVENTS = ["TabBrowserInserted", "TabSelect"];
28 const WINDOW_EVENTS = ["activate", "unload"];
29 const DEBUG = false;
31 // Variables
32 let _lastCurrentBrowserId = 0;
33 let _trackedWindows = [];
35 // Global methods
36 function debug(s) {
37   if (DEBUG) {
38     dump("-*- UpdateBrowserIDHelper: " + s + "\n");
39   }
42 function _updateCurrentBrowserId(browser) {
43   if (
44     !browser.browserId ||
45     browser.browserId === _lastCurrentBrowserId ||
46     browser.ownerGlobal != _trackedWindows[0]
47   ) {
48     return;
49   }
51   // Guard on DEBUG here because materializing a long data URI into
52   // a JS string for concatenation is not free.
53   if (DEBUG) {
54     debug(
55       `Current window uri=${browser.currentURI?.spec} browser id=${browser.browserId}`
56     );
57   }
59   _lastCurrentBrowserId = browser.browserId;
60   let idWrapper = Cc["@mozilla.org/supports-PRUint64;1"].createInstance(
61     Ci.nsISupportsPRUint64
62   );
63   idWrapper.data = _lastCurrentBrowserId;
64   Services.obs.notifyObservers(idWrapper, "net:current-browser-id");
67 function _handleEvent(event) {
68   switch (event.type) {
69     case "TabBrowserInserted":
70       if (
71         event.target.ownerGlobal.gBrowser.selectedBrowser ===
72         event.target.linkedBrowser
73       ) {
74         _updateCurrentBrowserId(event.target.linkedBrowser);
75       }
76       break;
77     case "TabSelect":
78       _updateCurrentBrowserId(event.target.linkedBrowser);
79       break;
80     case "activate":
81       WindowHelper.onActivate(event.target);
82       break;
83     case "unload":
84       WindowHelper.removeWindow(event.currentTarget);
85       break;
86   }
89 function _trackWindowOrder(window) {
90   if (window.windowState == window.STATE_MINIMIZED) {
91     let firstMinimizedWindow = _trackedWindows.findIndex(
92       w => w.windowState == w.STATE_MINIMIZED
93     );
94     if (firstMinimizedWindow == -1) {
95       firstMinimizedWindow = _trackedWindows.length;
96     }
97     _trackedWindows.splice(firstMinimizedWindow, 0, window);
98   } else {
99     _trackedWindows.unshift(window);
100   }
103 function _untrackWindowOrder(window) {
104   let idx = _trackedWindows.indexOf(window);
105   if (idx >= 0) {
106     _trackedWindows.splice(idx, 1);
107   }
110 function topicObserved(observeTopic, checkFn) {
111   return new Promise((resolve, reject) => {
112     function observer(subject, topic, data) {
113       try {
114         if (checkFn && !checkFn(subject, data)) {
115           return;
116         }
117         Services.obs.removeObserver(observer, topic);
118         checkFn = null;
119         resolve([subject, data]);
120       } catch (ex) {
121         Services.obs.removeObserver(observer, topic);
122         checkFn = null;
123         reject(ex);
124       }
125     }
126     Services.obs.addObserver(observer, observeTopic);
127   });
130 // Methods that impact a window. Put into single object for organization.
131 var WindowHelper = {
132   addWindow(window) {
133     // Add event listeners
134     TAB_EVENTS.forEach(function (event) {
135       window.gBrowser.tabContainer.addEventListener(event, _handleEvent);
136     });
137     WINDOW_EVENTS.forEach(function (event) {
138       window.addEventListener(event, _handleEvent);
139     });
141     _trackWindowOrder(window);
143     // Update the selected tab's content outer window ID.
144     _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
145   },
147   removeWindow(window) {
148     _untrackWindowOrder(window);
150     // Remove the event listeners
151     TAB_EVENTS.forEach(function (event) {
152       window.gBrowser.tabContainer.removeEventListener(event, _handleEvent);
153     });
154     WINDOW_EVENTS.forEach(function (event) {
155       window.removeEventListener(event, _handleEvent);
156     });
157   },
159   onActivate(window) {
160     // If this window was the last focused window, we don't need to do anything
161     if (window == _trackedWindows[0]) {
162       return;
163     }
165     _untrackWindowOrder(window);
166     _trackWindowOrder(window);
168     _updateCurrentBrowserId(window.gBrowser.selectedBrowser);
169   },
172 export const BrowserWindowTracker = {
173   pendingWindows: new Map(),
175   /**
176    * Get the most recent browser window.
177    *
178    * @param options an object accepting the arguments for the search.
179    *        * private: true to restrict the search to private windows
180    *            only, false to restrict the search to non-private only.
181    *            Omit the property to search in both groups.
182    *        * allowPopups: true if popup windows are permissable.
183    */
184   getTopWindow(options = {}) {
185     for (let win of _trackedWindows) {
186       if (
187         !win.closed &&
188         (options.allowPopups || win.toolbar.visible) &&
189         (!("private" in options) ||
190           lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
191           lazy.PrivateBrowsingUtils.isWindowPrivate(win) == options.private)
192       ) {
193         return win;
194       }
195     }
196     return null;
197   },
199   /**
200    * Get a window that is in the process of loading. Only supports windows
201    * opened via the `openWindow` function in this module or that have been
202    * registered with the `registerOpeningWindow` function.
203    *
204    * @param {Object} options
205    *   Options for the search.
206    * @param {boolean} [options.private]
207    *   true to restrict the search to private windows only, false to restrict
208    *   the search to non-private only. Omit the property to search in both
209    *   groups.
210    *
211    * @returns {Promise<Window> | null}
212    */
213   getPendingWindow(options = {}) {
214     for (let pending of this.pendingWindows.values()) {
215       if (
216         !("private" in options) ||
217         lazy.PrivateBrowsingUtils.permanentPrivateBrowsing ||
218         pending.isPrivate == options.private
219       ) {
220         return pending.deferred.promise;
221       }
222     }
223     return null;
224   },
226   /**
227    * Registers a browser window that is in the process of opening. Normally it
228    * would be preferable to use the standard method for opening the window from
229    * this module.
230    *
231    * @param {Window} window
232    *   The opening window.
233    * @param {boolean} isPrivate
234    *   Whether the opening window is a private browsing window.
235    */
236   registerOpeningWindow(window, isPrivate) {
237     let deferred = Promise.withResolvers();
239     this.pendingWindows.set(window, {
240       isPrivate,
241       deferred,
242     });
244     // Prevent leaks in case the window closes before we track it as an open
245     // window.
246     const topic = "browsing-context-discarded";
247     const observer = aSubject => {
248       if (window.browsingContext == aSubject) {
249         let pending = this.pendingWindows.get(window);
250         if (pending) {
251           this.pendingWindows.delete(window);
252           pending.deferred.resolve(window);
253         }
254         Services.obs.removeObserver(observer, topic);
255       }
256     };
257     Services.obs.addObserver(observer, topic);
258   },
260   /**
261    * A standard function for opening a new browser window.
262    *
263    * @param {Object} [options]
264    *   Options for the new window.
265    * @param {Window} [options.openerWindow]
266    *   An existing browser window to open the new one from.
267    * @param {boolean} [options.private]
268    *   True to make the window a private browsing window.
269    * @param {String} [options.features]
270    *   Additional window features to give the new window.
271    * @param {nsIArray | nsISupportsString} [options.args]
272    *   Arguments to pass to the new window.
273    * @param {boolean} [options.remote]
274    *   A boolean indicating if the window should run remote browser tabs or
275    *   not. If omitted, the window  will choose the profile default state.
276    * @param {boolean} [options.fission]
277    *   A boolean indicating if the window should run with fission enabled or
278    *   not. If omitted, the window will choose the profile default state.
279    *
280    * @returns {Window}
281    */
282   openWindow({
283     openerWindow = undefined,
284     private: isPrivate = false,
285     features = undefined,
286     args = null,
287     remote = undefined,
288     fission = undefined,
289   } = {}) {
290     let windowFeatures = "chrome,dialog=no,all";
291     if (features) {
292       windowFeatures += `,${features}`;
293     }
294     let loadURIString;
295     if (isPrivate && lazy.PrivateBrowsingUtils.enabled) {
296       windowFeatures += ",private";
297       if (!args && !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing) {
298         // Force the new window to load about:privatebrowsing instead of the
299         // default home page.
300         loadURIString = "about:privatebrowsing";
301       }
302     } else {
303       windowFeatures += ",non-private";
304     }
305     if (!args) {
306       loadURIString ??= lazy.BrowserHandler.defaultArgs;
307       args = Cc["@mozilla.org/supports-string;1"].createInstance(
308         Ci.nsISupportsString
309       );
310       args.data = loadURIString;
311     }
313     if (remote) {
314       windowFeatures += ",remote";
315     } else if (remote === false) {
316       windowFeatures += ",non-remote";
317     }
319     if (fission) {
320       windowFeatures += ",fission";
321     } else if (fission === false) {
322       windowFeatures += ",non-fission";
323     }
325     // If the opener window is maximized, we want to skip the animation, since
326     // we're going to be taking up most of the screen anyways, and we want to
327     // optimize for showing the user a useful window as soon as possible.
328     if (openerWindow?.windowState == openerWindow?.STATE_MAXIMIZED) {
329       windowFeatures += ",suppressanimation";
330     }
332     let win = Services.ww.openWindow(
333       openerWindow,
334       AppConstants.BROWSER_CHROME_URL,
335       "_blank",
336       windowFeatures,
337       args
338     );
339     this.registerOpeningWindow(win, isPrivate);
341     win.addEventListener(
342       "MozAfterPaint",
343       () => {
344         if (
345           Services.prefs.getIntPref("browser.startup.page") == 1 &&
346           loadURIString == lazy.HomePage.get()
347         ) {
348           // A notification for when a user has triggered their homepage. This
349           // is used to display a doorhanger explaining that an extension has
350           // modified the homepage, if necessary.
351           Services.obs.notifyObservers(win, "browser-open-homepage-start");
352         }
353       },
354       { once: true }
355     );
357     return win;
358   },
360   /**
361    * Async version of `openWindow` waiting for delayed startup of the new
362    * window before returning.
363    *
364    * @param {Object} [options]
365    *   Options for the new window. See `openWindow` for details.
366    *
367    * @returns {Window}
368    */
369   async promiseOpenWindow(options) {
370     let win = this.openWindow(options);
371     await topicObserved(
372       "browser-delayed-startup-finished",
373       subject => subject == win
374     );
375     return win;
376   },
378   /**
379    * Number of currently open browser windows.
380    */
381   get windowCount() {
382     return _trackedWindows.length;
383   },
385   /**
386    * Array of browser windows ordered by z-index, in reverse order.
387    * This means that the top-most browser window will be the first item.
388    */
389   get orderedWindows() {
390     // Clone the windows array immediately as it may change during iteration,
391     // we'd rather have an outdated order than skip/revisit windows.
392     return [..._trackedWindows];
393   },
395   getAllVisibleTabs() {
396     let tabs = [];
397     for (let win of BrowserWindowTracker.orderedWindows) {
398       for (let tab of win.gBrowser.visibleTabs) {
399         // Only use tabs which are not discarded / unrestored
400         if (tab.linkedPanel) {
401           let { contentTitle, browserId } = tab.linkedBrowser;
402           tabs.push({ contentTitle, browserId });
403         }
404       }
405     }
406     return tabs;
407   },
409   track(window) {
410     let pending = this.pendingWindows.get(window);
411     if (pending) {
412       this.pendingWindows.delete(window);
413       // Waiting for delayed startup to complete ensures that this new window
414       // has started loading its initial urls.
415       window.delayedStartupPromise.then(() => pending.deferred.resolve(window));
416     }
418     return WindowHelper.addWindow(window);
419   },
421   getBrowserById(browserId) {
422     for (let win of BrowserWindowTracker.orderedWindows) {
423       for (let tab of win.gBrowser.visibleTabs) {
424         if (tab.linkedPanel && tab.linkedBrowser.browserId === browserId) {
425           return tab.linkedBrowser;
426         }
427       }
428     }
429     return null;
430   },
432   // For tests only, this function will remove this window from the list of
433   // tracked windows. Please don't forget to add it back at the end of your
434   // tests!
435   untrackForTestsOnly(window) {
436     return WindowHelper.removeWindow(window);
437   },