no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / remote / shared / TabManager.sys.mjs
blobf38f0683098fea53b3ea59cf0db00fbc0adcfeea
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 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
9   BrowsingContextListener:
10     "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
11   EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
12   generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
13   MobileTabBrowser: "chrome://remote/content/shared/MobileTabBrowser.sys.mjs",
14   UserContextManager:
15     "chrome://remote/content/shared/UserContextManager.sys.mjs",
16 });
18 class TabManagerClass {
19   #browserUniqueIds;
20   #contextListener;
21   #navigableIds;
23   constructor() {
24     // Maps browser's permanentKey to uuid: WeakMap.<Object, string>
25     this.#browserUniqueIds = new WeakMap();
27     // Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>.
28     // It's required as a fallback, since in the case when a context was discarded
29     // embedderElement is gone, and we cannot retrieve
30     // the context id from this.#browserUniqueIds.
31     this.#navigableIds = new WeakMap();
33     this.#contextListener = new lazy.BrowsingContextListener();
34     this.#contextListener.on("attached", this.#onContextAttached);
35     this.#contextListener.startListening();
37     this.browsers.forEach(browser => {
38       if (this.isValidCanonicalBrowsingContext(browser.browsingContext)) {
39         this.#navigableIds.set(
40           browser.browsingContext,
41           this.getIdForBrowsingContext(browser.browsingContext)
42         );
43       }
44     });
45   }
47   /**
48    * Retrieve all the browser elements from tabs as contained in open windows.
49    *
50    * @returns {Array<XULBrowser>}
51    *     All the found <xul:browser>s. Will return an empty array if
52    *     no windows and tabs can be found.
53    */
54   get browsers() {
55     const browsers = [];
57     for (const win of this.windows) {
58       for (const tab of this.getTabsForWindow(win)) {
59         const contentBrowser = this.getBrowserForTab(tab);
60         if (contentBrowser !== null) {
61           browsers.push(contentBrowser);
62         }
63       }
64     }
66     return browsers;
67   }
69   /**
70    * Retrieve all the browser tabs in open windows.
71    *
72    * @returns {Array<Tab>}
73    *     All the open browser tabs. Will return an empty list if tab browser
74    *     is not available or tabs are undefined.
75    */
76   get tabs() {
77     const tabs = [];
79     for (const win of this.windows) {
80       tabs.push(...this.getTabsForWindow(win));
81     }
83     return tabs;
84   }
86   /**
87    * Retrieve all the open windows.
88    *
89    * @returns {Array<Window>}
90    *     All the open windows. Will return an empty list if no window is open.
91    */
92   get windows() {
93     const windows = [];
95     for (const win of Services.wm.getEnumerator(null)) {
96       if (win.closed) {
97         continue;
98       }
99       windows.push(win);
100     }
102     return windows;
103   }
105   /**
106    * Array of unique browser ids (UUIDs) for all content browsers of all
107    * windows.
108    *
109    * TODO: Similarly to getBrowserById, we should improve the performance of
110    * this getter in Bug 1750065.
111    *
112    * @returns {Array<string>}
113    *     Array of UUIDs for all content browsers.
114    */
115   get allBrowserUniqueIds() {
116     const browserIds = [];
118     for (const win of this.windows) {
119       // Only return handles for browser windows
120       for (const tab of this.getTabsForWindow(win)) {
121         const contentBrowser = this.getBrowserForTab(tab);
122         const winId = this.getIdForBrowser(contentBrowser);
123         if (winId !== null) {
124           browserIds.push(winId);
125         }
126       }
127     }
129     return browserIds;
130   }
132   /**
133    * Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
134    *
135    * @param {Tab} tab
136    *     The tab whose browser needs to be returned.
137    *
138    * @returns {XULBrowser}
139    *     The linked browser for the tab or null if no browser can be found.
140    */
141   getBrowserForTab(tab) {
142     if (tab && "linkedBrowser" in tab) {
143       return tab.linkedBrowser;
144     }
146     return null;
147   }
149   /**
150    * Return the tab browser for the specified chrome window.
151    *
152    * @param {ChromeWindow} win
153    *     Window whose <code>tabbrowser</code> needs to be accessed.
154    *
155    * @returns {Tab}
156    *     Tab browser or null if it's not a browser window.
157    */
158   getTabBrowser(win) {
159     if (lazy.AppInfo.isAndroid) {
160       return new lazy.MobileTabBrowser(win);
161     } else if (lazy.AppInfo.isFirefox) {
162       return win.gBrowser;
163     }
165     return null;
166   }
168   /**
169    * Create a new tab.
170    *
171    * @param {object} options
172    * @param {boolean=} options.focus
173    *     Set to true if the new tab should be focused (selected). Defaults to
174    *     false. `false` value is not properly supported on Android, additional
175    *     focus of previously selected tab is required after initial navigation.
176    * @param {Tab=} options.referenceTab
177    *     The reference tab after which the new tab will be added. If no
178    *     reference tab is provided, the new tab will be added after all the
179    *     other tabs.
180    * @param {string=} options.userContextId
181    *     A user context id from UserContextManager.
182    * @param {window=} options.window
183    *     The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
184    *     if no window is provided. Will be ignored if referenceTab is provided.
185    */
186   async addTab(options = {}) {
187     let {
188       focus = false,
189       referenceTab = null,
190       userContextId = null,
191       window = Services.wm.getMostRecentWindow(null),
192     } = options;
194     let index;
195     if (referenceTab != null) {
196       // If a reference tab was specified, the window should be the window
197       // owning the reference tab.
198       window = this.getWindowForTab(referenceTab);
199     }
201     if (referenceTab != null) {
202       index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
203     }
205     const tabBrowser = this.getTabBrowser(window);
207     const tab = await tabBrowser.addTab("about:blank", {
208       index,
209       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
210       userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
211     });
213     if (focus) {
214       await this.selectTab(tab);
215     }
217     return tab;
218   }
220   /**
221    * Retrieve the browser element corresponding to the provided unique id,
222    * previously generated via getIdForBrowser.
223    *
224    * TODO: To avoid creating strong references on browser elements and
225    * potentially leaking those elements, this method loops over all windows and
226    * all tabs. It should be replaced by a faster implementation in Bug 1750065.
227    *
228    * @param {string} id
229    *     A browser unique id created by getIdForBrowser.
230    * @returns {XULBrowser}
231    *     The <xul:browser> corresponding to the provided id. Will return null if
232    *     no matching browser element is found.
233    */
234   getBrowserById(id) {
235     for (const win of this.windows) {
236       for (const tab of this.getTabsForWindow(win)) {
237         const contentBrowser = this.getBrowserForTab(tab);
238         if (this.getIdForBrowser(contentBrowser) == id) {
239           return contentBrowser;
240         }
241       }
242     }
243     return null;
244   }
246   /**
247    * Retrieve the browsing context corresponding to the provided unique id.
248    *
249    * @param {string} id
250    *     A browsing context unique id (created by getIdForBrowsingContext).
251    * @returns {BrowsingContext=}
252    *     The browsing context found for this id, null if none was found.
253    */
254   getBrowsingContextById(id) {
255     const browser = this.getBrowserById(id);
256     if (browser) {
257       return browser.browsingContext;
258     }
260     return BrowsingContext.get(id);
261   }
263   /**
264    * Retrieve the unique id for the given xul browser element. The id is a
265    * dynamically generated uuid associated with the permanentKey property of the
266    * given browser element. This method is preferable over getIdForBrowsingContext
267    * in case of working with browser element of a tab, since we can not guarantee
268    * that browsing context is attached to it.
269    *
270    * @param {XULBrowser} browserElement
271    *     The <xul:browser> for which we want to retrieve the id.
272    * @returns {string} The unique id for this browser.
273    */
274   getIdForBrowser(browserElement) {
275     if (browserElement === null) {
276       return null;
277     }
279     const key = browserElement.permanentKey;
280     if (key === undefined) {
281       return null;
282     }
284     if (!this.#browserUniqueIds.has(key)) {
285       this.#browserUniqueIds.set(key, lazy.generateUUID());
286     }
287     return this.#browserUniqueIds.get(key);
288   }
290   /**
291    * Retrieve the id of a Browsing Context.
292    *
293    * For a top-level browsing context a custom unique id will be returned.
294    *
295    * @param {BrowsingContext=} browsingContext
296    *     The browsing context to get the id from.
297    *
298    * @returns {string}
299    *     The id of the browsing context.
300    */
301   getIdForBrowsingContext(browsingContext) {
302     if (!browsingContext) {
303       return null;
304     }
306     if (!browsingContext.parent) {
307       // Top-level browsing contexts have their own custom unique id.
308       // If a context was discarded, embedderElement is already gone,
309       // so use navigable id instead.
310       return browsingContext.embedderElement
311         ? this.getIdForBrowser(browsingContext.embedderElement)
312         : this.#navigableIds.get(browsingContext);
313     }
315     return browsingContext.id.toString();
316   }
318   /**
319    * Get the navigable for the given browsing context.
320    *
321    * Because Gecko doesn't support the Navigable concept in content
322    * scope the content browser could be used to uniquely identify
323    * top-level browsing contexts.
324    *
325    * @param {BrowsingContext} browsingContext
326    *
327    * @returns {BrowsingContext|XULBrowser} The navigable
328    *
329    * @throws {TypeError}
330    *     If `browsingContext` is not a CanonicalBrowsingContext instance.
331    */
332   getNavigableForBrowsingContext(browsingContext) {
333     if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
334       throw new TypeError(
335         `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
336       );
337     }
339     if (browsingContext.isContent && browsingContext.parent === null) {
340       return browsingContext.embedderElement;
341     }
343     return browsingContext;
344   }
346   getTabCount() {
347     let count = 0;
348     for (const win of this.windows) {
349       // For browser windows count the tabs. Otherwise take the window itself.
350       const tabsLength = this.getTabsForWindow(win).length;
351       count += tabsLength ? tabsLength : 1;
352     }
353     return count;
354   }
356   /**
357    * Retrieve the tab owning a Browsing Context.
358    *
359    * @param {BrowsingContext=} browsingContext
360    *     The browsing context to get the tab from.
361    *
362    * @returns {Tab|null}
363    *     The tab owning the Browsing Context.
364    */
365   getTabForBrowsingContext(browsingContext) {
366     const browser = browsingContext?.top.embedderElement;
367     if (!browser) {
368       return null;
369     }
371     const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
372     return tabBrowser.getTabForBrowser(browser);
373   }
375   /**
376    * Retrieve the list of tabs for a given window.
377    *
378    * @param {ChromeWindow} win
379    *     Window whose <code>tabs</code> need to be returned.
380    *
381    * @returns {Array<Tab>}
382    *     The list of tabs. Will return an empty list if tab browser is not available
383    *     or tabs are undefined.
384    */
385   getTabsForWindow(win) {
386     const tabBrowser = this.getTabBrowser(win);
387     // For web-platform reftests a faked tabbrowser is used,
388     // which does not actually have tabs.
389     if (tabBrowser && tabBrowser.tabs) {
390       return tabBrowser.tabs;
391     }
392     return [];
393   }
395   getWindowForTab(tab) {
396     // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
397     // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
398     // of the platforms.
399     return tab.linkedBrowser.ownerGlobal;
400   }
402   /**
403    * Check if the given argument is a valid canonical browsing context and was not
404    * discarded.
405    *
406    * @param {BrowsingContext} browsingContext
407    *     The browsing context to check.
408    *
409    * @returns {boolean}
410    *     True if the browsing context is valid, false otherwise.
411    */
412   isValidCanonicalBrowsingContext(browsingContext) {
413     return (
414       CanonicalBrowsingContext.isInstance(browsingContext) &&
415       !browsingContext.isDiscarded
416     );
417   }
419   /**
420    * Remove the given tab.
421    *
422    * @param {Tab} tab
423    *     Tab to remove.
424    * @param {object=} options
425    * @param {boolean=} options.skipPermitUnload
426    *     Flag to indicate if a potential beforeunload prompt should be skipped
427    *     when closing the tab. Defaults to false.
428    */
429   async removeTab(tab, options = {}) {
430     const { skipPermitUnload = false } = options;
432     if (!tab) {
433       return;
434     }
436     const ownerWindow = this.getWindowForTab(tab);
437     const tabBrowser = this.getTabBrowser(ownerWindow);
438     await tabBrowser.removeTab(tab, {
439       skipPermitUnload,
440     });
441   }
443   /**
444    * Select the given tab.
445    *
446    * @param {Tab} tab
447    *     Tab to select.
448    *
449    * @returns {Promise}
450    *     Promise that resolves when the given tab has been selected.
451    */
452   async selectTab(tab) {
453     if (!tab) {
454       return Promise.resolve();
455     }
457     const ownerWindow = this.getWindowForTab(tab);
458     const tabBrowser = this.getTabBrowser(ownerWindow);
460     if (tab === tabBrowser.selectedTab) {
461       return Promise.resolve();
462     }
464     const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
465     tabBrowser.selectedTab = tab;
467     await selected;
469     // Sometimes at that point window is not focused.
470     if (Services.focus.activeWindow != ownerWindow) {
471       const activated = new lazy.EventPromise(ownerWindow, "activate");
472       ownerWindow.focus();
473       return activated;
474     }
476     return Promise.resolve();
477   }
479   supportsTabs() {
480     return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
481   }
483   #onContextAttached = (eventName, data = {}) => {
484     const { browsingContext } = data;
485     if (this.isValidCanonicalBrowsingContext(browsingContext)) {
486       this.#navigableIds.set(
487         browsingContext,
488         this.getIdForBrowsingContext(browsingContext)
489       );
490     }
491   };
494 // Expose a shared singleton.
495 export const TabManager = new TabManagerClass();