TaskbarTabsWindowManager.sys.mjs (10541B)
1 /* vim: se cin sw=2 ts=2 et filetype=javascript : 2 * This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const kTaskbarTabsWindowFeatures = 9 "titlebar,close,toolbar,location,personalbar=no,status,menubar=no,resizable,minimizable,scrollbars"; 10 11 let lazy = {}; 12 13 ChromeUtils.defineESModuleGetters(lazy, { 14 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.sys.mjs", 15 TaskbarTabsUtils: "resource:///modules/taskbartabs/TaskbarTabsUtils.sys.mjs", 16 }); 17 18 XPCOMUtils.defineLazyServiceGetters(lazy, { 19 WindowsUIUtils: ["@mozilla.org/windows-ui-utils;1", Ci.nsIWindowsUIUtils], 20 WinTaskbar: ["@mozilla.org/windows-taskbar;1", Ci.nsIWinTaskbar], 21 }); 22 23 ChromeUtils.defineLazyGetter(lazy, "logConsole", () => { 24 return console.createInstance({ 25 prefix: "TaskbarTabs", 26 maxLogLevel: "Warn", 27 }); 28 }); 29 30 /** 31 * Manager for the lifetimes of Taskbar Tab windows. 32 */ 33 export class TaskbarTabsWindowManager { 34 // Map from the taskbar tab ID to a Set of window IDs. Use #trackWindow 35 // and #untrackWindow. 36 #openWindows = new Map(); 37 // Map from the tab browser permanent key to originating window ID. 38 #tabOriginMap = new WeakMap(); 39 40 /** 41 * Moves an existing browser tab into a Taskbar Tab. 42 * 43 * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab to replace the window with. 44 * @param {MozTabbrowserTab} aTab - The tab to adopt as a Taskbar Tab. 45 * @returns {Promise<DOMWindow>} The newly created Taskbar Tab window. 46 */ 47 async replaceTabWithWindow(aTaskbarTab, aTab) { 48 let originWindow = aTab.ownerGlobal; 49 50 Glean.webApp.moveToTaskbar.record({}); 51 52 // Save the parent window of this tab, so we can revert back if needed. 53 let tabId = getTabId(aTab); 54 let windowId = getWindowId(originWindow); 55 56 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 57 Ci.nsIWritablePropertyBag2 58 ); 59 extraOptions.setPropertyAsAString("taskbartab", aTaskbarTab.id); 60 61 let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 62 args.appendElement(aTab); 63 args.appendElement(extraOptions); 64 65 this.#tabOriginMap.set(tabId, windowId); 66 return await this.#openWindow(aTaskbarTab, args); 67 } 68 69 /** 70 * Opens a new Taskbar Tab Window. 71 * 72 * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab to open. 73 * @returns {Promise<DOMWindow>} The newly-created Taskbar Tab window. 74 */ 75 async openWindow(aTaskbarTab) { 76 let url = Cc["@mozilla.org/supports-string;1"].createInstance( 77 Ci.nsISupportsString 78 ); 79 url.data = aTaskbarTab.startUrl; 80 81 let extraOptions = Cc["@mozilla.org/hash-property-bag;1"].createInstance( 82 Ci.nsIWritablePropertyBag2 83 ); 84 extraOptions.setPropertyAsAString("taskbartab", aTaskbarTab.id); 85 86 let userContextId = Cc["@mozilla.org/supports-PRUint32;1"].createInstance( 87 Ci.nsISupportsPRUint32 88 ); 89 userContextId.data = aTaskbarTab.userContextId; 90 91 let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); 92 args.appendElement(url); 93 args.appendElement(extraOptions); 94 args.appendElement(null); 95 args.appendElement(null); 96 args.appendElement(undefined); 97 args.appendElement(userContextId); 98 args.appendElement(null); 99 args.appendElement(null); 100 args.appendElement(Services.scriptSecurityManager.getSystemPrincipal()); 101 102 return await this.#openWindow(aTaskbarTab, args); 103 } 104 105 /** 106 * Handles common window opening behavior for Taskbar Tabs. 107 * 108 * @param {TaskbarTab} aTaskbarTab - The Taskbar Tab associated to the window. 109 * @param {nsIMutableArray} aArgs - `args` to pass to the opening window. 110 * @returns {Promise<DOMWindow>} Resolves once window has opened and tab count 111 * has been incremented. 112 */ 113 async #openWindow(aTaskbarTab, aArgs) { 114 let url = Services.io.newURI(aTaskbarTab.startUrl); 115 let imgPromise = lazy.TaskbarTabsUtils.getFavicon(url); 116 117 let win = await lazy.BrowserWindowTracker.promiseOpenWindow({ 118 args: aArgs, 119 features: kTaskbarTabsWindowFeatures, 120 all: false, 121 }); 122 123 imgPromise.then(imgContainer => 124 lazy.WindowsUIUtils.setWindowIcon(win, imgContainer, imgContainer) 125 ); 126 127 this.#trackWindow(aTaskbarTab.id, win); 128 129 lazy.WinTaskbar.setGroupIdForWindow(win, aTaskbarTab.id); 130 131 this.#attachWindowFocusTelemetry(win); 132 Glean.webApp.activate.record({}); 133 134 win.focus(); 135 136 win.gBrowser.tabs.forEach(tab => { 137 const browser = win.gBrowser.getBrowserForTab(tab); 138 browser.browsingContext.displayMode = "minimal-ui"; 139 }); 140 141 return win; 142 } 143 144 /** 145 * Adds the window to the set of windows open within the taskbar tab. 146 * The window will automatically be removed when the window closes if 147 * it hasn't been untracked already. 148 * 149 * @param {string} aId Taskbar Tab ID that the window should be assigned to. 150 * @param {DOMWindow} aWindow Window to track. 151 */ 152 #trackWindow(aId, aWindow) { 153 let openWindows = this.#openWindows.get(aId); 154 if (typeof openWindows === "undefined") { 155 openWindows = new Set(); 156 this.#openWindows.set(aId, openWindows); 157 } 158 159 openWindows.add(getWindowId(aWindow)); 160 aWindow.addEventListener("unload", _e => this.#untrackWindow(aId, aWindow)); 161 } 162 163 /** 164 * Remove the window from the set of windows open within the taskbar tab. 165 * This function is idempotent. 166 * 167 * @param {string} aId Taskbar Tab ID that the window should be assigned to. 168 * @param {DOMWindow} aWindow Window to track. 169 */ 170 #untrackWindow(aId, aWindow) { 171 let openWindows = this.#openWindows.get(aId); 172 if (typeof openWindows === "undefined") { 173 // If it is undefined, the window wasn't being tracked anyways. 174 return; 175 } 176 177 openWindows.delete(getWindowId(aWindow)); 178 if (openWindows.size === 0) { 179 // Avoid leaking entries in the map. 180 this.#openWindows.delete(aId); 181 } 182 } 183 184 #attachWindowFocusTelemetry(aWindow) { 185 let timerId = null; 186 187 function focused() { 188 if (timerId == null) { 189 timerId = Glean.webApp.usageTime.start(); 190 } 191 } 192 193 function blur() { 194 if (timerId != null) { 195 Glean.webApp.usageTime.stopAndAccumulate(timerId); 196 } 197 198 timerId = null; 199 } 200 201 aWindow.addEventListener("focus", focused); 202 aWindow.addEventListener("blur", blur); 203 aWindow.addEventListener("unload", blur); 204 205 // The window might already have been focused, and at any rate it'll get 206 // focused. Forcefully trigger focused now to account for that. 207 focused(); 208 } 209 210 /** 211 * Reverts a web app to a tab in a regular Firefox window. We will try to use 212 * the window the taskbar tab originated from, if that's not avaliable, we 213 * will use the most recently active window. If no window is avalaible, a new 214 * one will be opened. 215 * 216 * @param {DOMWindow} aWindow - A Taskbar Tab window. 217 */ 218 async ejectWindow(aWindow) { 219 lazy.logConsole.info("Ejecting window from Taskbar Tabs."); 220 221 let taskbarTabId = lazy.TaskbarTabsUtils.getTaskbarTabIdFromWindow(aWindow); 222 if (!taskbarTabId) { 223 throw new Error("No Taskbar Tab ID found on window."); 224 } else { 225 lazy.logConsole.debug(`Taskbar Tab ID is ${taskbarTabId}`); 226 } 227 228 let windowList = lazy.BrowserWindowTracker.getOrderedWindows({ 229 private: false, 230 }); 231 232 Glean.webApp.eject.record({}); 233 234 // A Taskbar Tab should only contain one tab, but iterate over the browser's 235 // tabs just in case one snuck in. 236 for (const tab of aWindow.gBrowser.tabs) { 237 let tabId = getTabId(tab); 238 let originWindowId = this.#tabOriginMap.get(tabId); 239 240 let win = 241 // Find the originating window for the Taskbar Tab if it still exists. 242 windowList.find(window => { 243 let windowId = getWindowId(window); 244 let matching = windowId === originWindowId; 245 if (matching) { 246 lazy.logConsole.debug( 247 `Ejecting into originating window: ${windowId}` 248 ); 249 } 250 return matching; 251 }); 252 253 if (!win) { 254 // Otherwise the most recent non-Taskbar Tabs window interacted with. 255 win = lazy.BrowserWindowTracker.getTopWindow({ 256 private: false, 257 }); 258 259 if (win) { 260 lazy.logConsole.debug(`Ejecting into top window.`); 261 } 262 } 263 264 let newTab; 265 if (win) { 266 // Set this tab to the last tab position of the window. 267 newTab = win.gBrowser.adoptTab(tab, { 268 tabIndex: win.gBrowser.openTabs.length, 269 selectTab: true, 270 }); 271 } else { 272 lazy.logConsole.debug( 273 "No originating or existing browser window found, ejecting into newly created window." 274 ); 275 win = await lazy.BrowserWindowTracker.promiseOpenWindow({ args: tab }); 276 newTab = win.gBrowser.tabs[0]; 277 } 278 279 win.focus(); 280 281 let browser = win.gBrowser.getBrowserForTab(newTab); 282 browser.browsingContext.displayMode = "browser"; 283 284 this.#tabOriginMap.delete(tabId); 285 } 286 287 this.#untrackWindow(taskbarTabId, aWindow); 288 } 289 290 /** 291 * Returns a count of the current windows associated to a Taskbar Tab. 292 * 293 * @param {string} aId - The Taskbar Tab ID. 294 * @returns {integer} Count of windows associated to the Taskbar Tab ID. 295 */ 296 getCountForId(aId) { 297 return this.#openWindows.get(aId)?.size ?? 0; 298 } 299 300 /** 301 * Utility function to mock `nsIWindowsUIUtils`. 302 * 303 * @param {nsIWindowsUIUtils} mock - A mock of nsIWindowsUIUtils. 304 */ 305 testOnlyMockUIUtils(mock) { 306 if (!Cu.isInAutomation) { 307 throw new Error("Can only mock utils in automation."); 308 } 309 // eslint-disable-next-line mozilla/valid-lazy 310 Object.defineProperty(lazy, "WindowsUIUtils", { 311 get() { 312 if (mock) { 313 return mock; 314 } 315 return Cc["@mozilla.org/windows-ui-utils;1"].getService( 316 Ci.nsIWindowsUIUtils 317 ); 318 }, 319 }); 320 } 321 } 322 323 /** 324 * Retrieves the browser tab's ID. 325 * 326 * @param {MozTabbrowserTab} aTab - Tab to retrieve the ID from. 327 * @returns {object} The permanent key identifying the tab. 328 */ 329 function getTabId(aTab) { 330 return aTab.permanentKey; 331 } 332 333 /** 334 * Retrieves the window ID. 335 * 336 * @param {DOMWindow} aWindow 337 * @returns {string} A unique string identifying the window. 338 */ 339 function getWindowId(aWindow) { 340 return aWindow.docShell.outerWindowID; 341 }