manager.js (8030B)
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/. */ 4 5 "use strict"; 6 7 const EventEmitter = require("resource://devtools/shared/event-emitter.js"); 8 9 loader.lazyRequireGetter( 10 this, 11 "ResponsiveUI", 12 "resource://devtools/client/responsive/ui.js" 13 ); 14 loader.lazyRequireGetter( 15 this, 16 "startup", 17 "resource://devtools/client/responsive/utils/window.js", 18 true 19 ); 20 loader.lazyRequireGetter( 21 this, 22 "showNotification", 23 "resource://devtools/client/responsive/utils/notification.js", 24 true 25 ); 26 loader.lazyRequireGetter( 27 this, 28 "l10n", 29 "resource://devtools/client/responsive/utils/l10n.js" 30 ); 31 loader.lazyRequireGetter( 32 this, 33 "PriorityLevels", 34 "resource://devtools/client/shared/components/NotificationBox.js", 35 true 36 ); 37 loader.lazyRequireGetter( 38 this, 39 "gDevTools", 40 "resource://devtools/client/framework/devtools.js", 41 true 42 ); 43 loader.lazyRequireGetter( 44 this, 45 "gDevToolsBrowser", 46 "resource://devtools/client/framework/devtools-browser.js", 47 true 48 ); 49 loader.lazyRequireGetter( 50 this, 51 "Telemetry", 52 "resource://devtools/client/shared/telemetry.js" 53 ); 54 55 /** 56 * ResponsiveUIManager is the external API for the browser UI, etc. to use when 57 * opening and closing the responsive UI. 58 */ 59 class ResponsiveUIManager { 60 constructor() { 61 this.activeTabs = new Map(); 62 63 this.handleMenuCheck = this.handleMenuCheck.bind(this); 64 65 EventEmitter.decorate(this); 66 } 67 68 get telemetry() { 69 if (!this._telemetry) { 70 this._telemetry = new Telemetry(); 71 } 72 73 return this._telemetry; 74 } 75 76 /** 77 * Toggle the responsive UI for a tab. 78 * 79 * @param window 80 * The main browser chrome window. 81 * @param tab 82 * The browser tab. 83 * @param options 84 * Other options associated with toggling. Currently includes: 85 * - `trigger`: String denoting the UI entry point, such as: 86 * - `toolbox`: Toolbox Button 87 * - `menu`: Browser Tools menu item 88 * - `shortcut`: Keyboard shortcut 89 * @return Promise 90 * Resolved when the toggling has completed. If the UI has opened, 91 * it is resolved to the ResponsiveUI instance for this tab. If the 92 * the UI has closed, there is no resolution value. 93 */ 94 toggle(window, tab, options = {}) { 95 const completed = this._toggleForTab(window, tab, options); 96 completed.catch(console.error); 97 return completed; 98 } 99 100 _toggleForTab(window, tab, options) { 101 if (this.isActiveForTab(tab)) { 102 return this.closeIfNeeded(window, tab, options); 103 } 104 105 return this.openIfNeeded(window, tab, options); 106 } 107 108 /** 109 * Opens the responsive UI, if not already open. 110 * 111 * @param window 112 * The main browser chrome window. 113 * @param tab 114 * The browser tab. 115 * @param options 116 * Other options associated with opening. Currently includes: 117 * - `trigger`: String denoting the UI entry point, such as: 118 * - `toolbox`: Toolbox Button 119 * - `menu`: Browser Tools menu item 120 * - `shortcut`: Keyboard shortcut 121 * @return Promise 122 * Resolved to the ResponsiveUI instance for this tab when opening is 123 * complete. 124 */ 125 async openIfNeeded(window, tab, options = {}) { 126 if (!this.isActiveForTab(tab)) { 127 this.initMenuCheckListenerFor(window); 128 129 const ui = new ResponsiveUI(this, window, tab); 130 this.activeTabs.set(tab, ui); 131 132 // Explicitly not await on telemetry to avoid delaying RDM opening 133 this.recordTelemetryOpen(window, tab, options); 134 135 await gDevToolsBrowser.loadBrowserStyleSheet(window); 136 await this.setMenuCheckFor(tab, window); 137 await ui.initialize(); 138 this.emit("on", { tab }); 139 } 140 141 return this.getResponsiveUIForTab(tab); 142 } 143 144 /** 145 * Record all telemetry probes related to RDM opening. 146 */ 147 recordTelemetryOpen(window, tab, options) { 148 // Track whether a toolbox was opened before RDM was opened. 149 const toolbox = gDevTools.getToolboxForTab(tab); 150 const hostType = toolbox ? toolbox.hostType : "none"; 151 const hasToolbox = !!toolbox; 152 153 if (hasToolbox) { 154 Glean.devtoolsResponsive.toolboxOpenedFirst.add(1); 155 } 156 157 this.telemetry.recordEvent("activate", "responsive_design", null, { 158 host: hostType, 159 width: Math.ceil(window.outerWidth / 50) * 50, 160 }); 161 162 // Track opens keyed by the UI entry point used. 163 let { trigger } = options; 164 if (!trigger) { 165 trigger = "unknown"; 166 } 167 Glean.devtoolsResponsive.openTrigger[trigger].add(1); 168 } 169 170 /** 171 * Closes the responsive UI, if not already closed. 172 * 173 * @param window 174 * The main browser chrome window. 175 * @param tab 176 * The browser tab. 177 * @param options 178 * Other options associated with closing. Currently includes: 179 * - `trigger`: String denoting the UI entry point, such as: 180 * - `toolbox`: Toolbox Button 181 * - `menu`: Browser Tools menu item 182 * - `shortcut`: Keyboard shortcut 183 * - `reason`: String detailing the specific cause for closing 184 * @return Promise 185 * Resolved (with no value) when closing is complete. 186 */ 187 async closeIfNeeded(window, tab, options = {}) { 188 if (this.isActiveForTab(tab)) { 189 const ui = this.activeTabs.get(tab); 190 const destroyed = await ui.destroy(options); 191 if (!destroyed) { 192 // Already in the process of destroying, abort. 193 return; 194 } 195 196 this.activeTabs.delete(tab); 197 198 if (!this.isActiveForWindow(window)) { 199 this.removeMenuCheckListenerFor(window); 200 } 201 this.emit("off", { tab }); 202 await this.setMenuCheckFor(tab, window); 203 204 // Explicitly not await on telemetry to avoid delaying RDM closing 205 this.recordTelemetryClose(window, tab); 206 } 207 } 208 209 recordTelemetryClose(window, tab) { 210 const toolbox = gDevTools.getToolboxForTab(tab); 211 212 const hostType = toolbox ? toolbox.hostType : "none"; 213 214 this.telemetry.recordEvent("deactivate", "responsive_design", null, { 215 host: hostType, 216 width: Math.ceil(window.outerWidth / 50) * 50, 217 }); 218 } 219 220 /** 221 * Returns true if responsive UI is active for a given tab. 222 * 223 * @param tab 224 * The browser tab. 225 * @return boolean 226 */ 227 isActiveForTab(tab) { 228 return this.activeTabs.has(tab); 229 } 230 231 /** 232 * Returns true if responsive UI is active in any tab in the given window. 233 * 234 * @param window 235 * The main browser chrome window. 236 * @return boolean 237 */ 238 isActiveForWindow(window) { 239 return [...this.activeTabs.keys()].some(t => t.ownerGlobal === window); 240 } 241 242 /** 243 * Return the responsive UI controller for a tab. 244 * 245 * @param tab 246 * The browser tab. 247 * @return ResponsiveUI 248 * The UI instance for this tab. 249 */ 250 getResponsiveUIForTab(tab) { 251 return this.activeTabs.get(tab); 252 } 253 254 handleMenuCheck({ target }) { 255 this.setMenuCheckFor(target); 256 } 257 258 initMenuCheckListenerFor(window) { 259 const { tabContainer } = window.gBrowser; 260 tabContainer.addEventListener("TabSelect", this.handleMenuCheck); 261 } 262 263 removeMenuCheckListenerFor(window) { 264 if (window?.gBrowser?.tabContainer) { 265 const { tabContainer } = window.gBrowser; 266 tabContainer.removeEventListener("TabSelect", this.handleMenuCheck); 267 } 268 } 269 270 async setMenuCheckFor(tab, window = tab.ownerGlobal) { 271 await startup(window); 272 273 const menu = window.document.getElementById("menu_responsiveUI"); 274 if (menu) { 275 menu.setAttribute("checked", this.isActiveForTab(tab)); 276 } 277 } 278 279 showRemoteOnlyNotification(window, tab, { trigger } = {}) { 280 return showNotification(window, tab, { 281 toolboxButton: trigger == "toolbox", 282 msg: l10n.getStr("responsive.remoteOnly"), 283 priority: PriorityLevels.PRIORITY_CRITICAL_MEDIUM, 284 }); 285 } 286 } 287 288 module.exports = new ResponsiveUIManager();