toolbox-hosts.js (13731B)
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 "gDevToolsBrowser", 12 "resource://devtools/client/framework/devtools-browser.js", 13 true 14 ); 15 16 const lazy = {}; 17 ChromeUtils.defineESModuleGetters(lazy, { 18 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 19 }); 20 21 /* A host should always allow this much space for the page to be displayed. 22 * There is also a min-height on the browser, but we still don't want to set 23 * frame.style.height to be larger than that, since it can cause problems with 24 * resizing the toolbox and panel layout. */ 25 const MIN_PAGE_SIZE = 25; 26 27 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 28 29 /** 30 * A toolbox host represents an object that contains a toolbox (e.g. the 31 * sidebar or a separate window). Any host object should implement the 32 * following functions: 33 * 34 * create() - create the UI 35 * raise() - bring UI in foreground 36 * setTitle - update UI's visible title (if any) 37 * destroy() - destroy the host's UI 38 */ 39 40 /** 41 * Base class for any in-browser host: left, bottom, right. 42 */ 43 class BaseInBrowserHost { 44 /** 45 * @param {Tab} hostTab 46 * The web page's tab where DevTools are displayed. 47 * @param {string} type 48 * The host type: left, bottom, right. 49 */ 50 constructor(hostTab, type) { 51 this.hostTab = hostTab; 52 this.type = type; 53 54 this._gBrowser = this.hostTab.ownerGlobal.gBrowser; 55 this._browserContainer = this._gBrowser.getBrowserContainer( 56 this.hostTab.linkedBrowser 57 ); 58 59 // Reference to the <browser> element used to load DevTools. 60 // This is created by each subclass from create() method 61 this.frame = null; 62 63 Services.obs.addObserver(this, "browsing-context-active-change"); 64 } 65 66 _createFrame() { 67 this.frame = createDevToolsFrame( 68 this.hostTab.ownerDocument, 69 this.type == "bottom" 70 ? "devtools-toolbox-bottom-iframe" 71 : "devtools-toolbox-side-iframe" 72 ); 73 } 74 75 observe(subject, topic) { 76 if (topic != "browsing-context-active-change") { 77 return; 78 } 79 // Ignore any BrowsingContext which isn't the debugged tab's BrowsingContext 80 // (toolbox may be half destroyed and the linkedBrowser be null when moving a tab 81 // with DevTools to another window) 82 if (this.hostTab.linkedBrowser?.browsingContext != subject) { 83 return; 84 } 85 86 // In case this is called before create() is called 87 if (!this.frame) { 88 return; 89 } 90 91 // Update DevTools <browser> element's isActive according to the debugged <browser> element status. 92 // This helps activate/deactivate DevTools when changing tabs. 93 // It notably triggers visibilitychange events on DevTools documents. 94 this.frame.docShellIsActive = subject.isActive; 95 } 96 97 /** 98 * Raise the host. 99 */ 100 raise() { 101 focusTab(this.hostTab); 102 } 103 104 /** 105 * Set the toolbox title. 106 * Nothing to do for this host type. 107 */ 108 setTitle() {} 109 110 destroy() { 111 Services.obs.removeObserver(this, "browsing-context-active-change"); 112 this._gBrowser = null; 113 this._browserContainer = null; 114 } 115 } 116 117 /** 118 * Host object for the dock on the bottom of the browser 119 */ 120 class BottomHost extends BaseInBrowserHost { 121 constructor(hostTab) { 122 super(hostTab, "bottom"); 123 124 this.heightPref = "devtools.toolbox.footer.height"; 125 } 126 127 #splitter; 128 129 #destroyed; 130 131 /** 132 * Create a box at the bottom of the host tab. 133 */ 134 async create() { 135 await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal); 136 137 const { ownerDocument } = this.hostTab; 138 this.#splitter = ownerDocument.createXULElement("splitter"); 139 this.#splitter.setAttribute("class", "devtools-horizontal-splitter"); 140 this.#splitter.setAttribute("resizebefore", "none"); 141 this.#splitter.setAttribute("resizeafter", "sibling"); 142 143 this._createFrame(); 144 145 this.frame.style.height = 146 Math.min( 147 Services.prefs.getIntPref(this.heightPref), 148 this._browserContainer.clientHeight - MIN_PAGE_SIZE 149 ) + "px"; 150 151 this._browserContainer.appendChild(this.#splitter); 152 this._browserContainer.appendChild(this.frame); 153 this.frame.docShellIsActive = true; 154 155 focusTab(this.hostTab); 156 return this.frame; 157 } 158 159 /** 160 * Destroy the bottom dock. 161 */ 162 destroy() { 163 if (!this.#destroyed) { 164 this.#destroyed = true; 165 166 const height = parseInt(this.frame.style.height, 10); 167 if (!isNaN(height)) { 168 Services.prefs.setIntPref(this.heightPref, height); 169 } 170 171 this._browserContainer.removeChild(this.#splitter); 172 this._browserContainer.removeChild(this.frame); 173 this.frame = null; 174 this.#splitter = null; 175 176 super.destroy(); 177 } 178 179 return Promise.resolve(null); 180 } 181 } 182 183 /** 184 * Base Host object for the in-browser left or right sidebars 185 */ 186 class SidebarHost extends BaseInBrowserHost { 187 constructor(hostTab, type) { 188 super(hostTab, type); 189 190 this.widthPref = "devtools.toolbox.sidebar.width"; 191 } 192 193 #splitter; 194 #browserPanel; 195 #destroyed; 196 197 /** 198 * Create a box in the sidebar of the host tab. 199 */ 200 async create() { 201 await gDevToolsBrowser.loadBrowserStyleSheet(this.hostTab.ownerGlobal); 202 203 this.#browserPanel = this._gBrowser.getPanel(this.hostTab.linkedBrowser); 204 const { ownerDocument } = this.hostTab; 205 206 this.#splitter = ownerDocument.createXULElement("splitter"); 207 this.#splitter.setAttribute("class", "devtools-side-splitter"); 208 209 this._createFrame(); 210 211 this.frame.style.width = 212 Math.min( 213 Services.prefs.getIntPref(this.widthPref), 214 this.#browserPanel.clientWidth - MIN_PAGE_SIZE 215 ) + "px"; 216 217 // We should consider the direction when changing the dock position. 218 const topWindow = this.hostTab.ownerGlobal; 219 const topDoc = topWindow.document.documentElement; 220 const isLTR = topWindow.getComputedStyle(topDoc).direction === "ltr"; 221 222 this.#splitter.setAttribute("resizebefore", "none"); 223 this.#splitter.setAttribute("resizeafter", "none"); 224 225 if ((isLTR && this.type == "right") || (!isLTR && this.type == "left")) { 226 this.#splitter.setAttribute("resizeafter", "sibling"); 227 this.#browserPanel.appendChild(this.#splitter); 228 this.#browserPanel.appendChild(this.frame); 229 } else { 230 this.#splitter.setAttribute("resizebefore", "sibling"); 231 this.#browserPanel.insertBefore(this.frame, this._browserContainer); 232 this.#browserPanel.insertBefore(this.#splitter, this._browserContainer); 233 } 234 this.frame.docShellIsActive = true; 235 236 focusTab(this.hostTab); 237 return this.frame; 238 } 239 240 /** 241 * Destroy the sidebar. 242 */ 243 destroy() { 244 if (!this.#destroyed) { 245 this.#destroyed = true; 246 247 const width = parseInt(this.frame.style.width, 10); 248 if (!isNaN(width)) { 249 Services.prefs.setIntPref(this.widthPref, width); 250 } 251 252 this.#browserPanel.removeChild(this.#splitter); 253 this.#browserPanel.removeChild(this.frame); 254 this.#browserPanel = null; 255 this.#splitter = null; 256 this.frame = null; 257 258 super.destroy(); 259 } 260 261 return Promise.resolve(null); 262 } 263 } 264 265 /** 266 * Host object for the in-browser left sidebar 267 */ 268 class LeftHost extends SidebarHost { 269 constructor(hostTab) { 270 super(hostTab, "left"); 271 } 272 } 273 274 /** 275 * Host object for the in-browser right sidebar 276 */ 277 class RightHost extends SidebarHost { 278 constructor(hostTab) { 279 super(hostTab, "right"); 280 } 281 } 282 283 /** 284 * Host object for the toolbox in a separate window 285 */ 286 class WindowHost extends EventEmitter { 287 constructor(hostTab, options) { 288 super(); 289 290 this._boundUnload = this._boundUnload.bind(this); 291 this.hostTab = hostTab; 292 this.options = options; 293 } 294 295 type = "window"; 296 297 WINDOW_URL = "chrome://devtools/content/framework/toolbox-window.xhtml"; 298 299 /** 300 * Create a new xul window to contain the toolbox. 301 */ 302 create() { 303 return new Promise(resolve => { 304 let flags = "chrome,centerscreen,resizable,dialog=no"; 305 306 // If we are debugging a tab which is in a Private window, we must also 307 // set the private flag on the DevTools host window. Otherwise switching 308 // hosts between docked and window modes can fail due to incompatible 309 // docshell origin attributes. See 1581093. 310 const owner = this.hostTab?.ownerGlobal; 311 if (owner && lazy.PrivateBrowsingUtils.isWindowPrivate(owner)) { 312 flags += ",private"; 313 } 314 315 // If the current window is a non-fission window, force the non-fission 316 // flag. Otherwise switching to window host from a non-fission window in 317 // a fission Firefox (!) will attempt to swapFrameLoaders between fission 318 // and non-fission frames. See Bug 1650963. 319 if (this.hostTab && !this.hostTab.ownerGlobal.gFissionBrowser) { 320 flags += ",non-fission"; 321 } 322 323 // When debugging local Web Extension, the toolbox is opened in an 324 // always foremost top level window in order to be kept visible 325 // when interacting with the Firefox Window. 326 if (this.options?.alwaysOnTop) { 327 flags += ",alwaysontop"; 328 } 329 330 const win = Services.ww.openWindow( 331 null, 332 this.WINDOW_URL, 333 "_blank", 334 flags, 335 null 336 ); 337 338 const frameLoad = () => { 339 win.removeEventListener("load", frameLoad, true); 340 win.focus(); 341 342 this.frame = createDevToolsFrame( 343 win.document, 344 "devtools-toolbox-window-iframe" 345 ); 346 win.document 347 .getElementById("devtools-toolbox-window") 348 .appendChild(this.frame); 349 this.frame.docShellIsActive = true; 350 351 // The forceOwnRefreshDriver attribute is set to avoid Windows only issues with 352 // CSS transitions when switching from docked to window hosts. 353 // Added in Bug 832920, should be reviewed in Bug 1542468. 354 this.frame.setAttribute("forceOwnRefreshDriver", ""); 355 resolve(this.frame); 356 }; 357 358 win.addEventListener("load", frameLoad, true); 359 win.addEventListener("unload", this._boundUnload); 360 361 this._window = win; 362 }); 363 } 364 365 /** 366 * Catch the user closing the window. 367 */ 368 _boundUnload(event) { 369 if (event.target.location != this.WINDOW_URL) { 370 return; 371 } 372 this._window.removeEventListener("unload", this._boundUnload); 373 374 this.emit("window-closed"); 375 } 376 377 /** 378 * Raise the host. 379 */ 380 raise() { 381 this._window.focus(); 382 } 383 384 /** 385 * Set the toolbox title. 386 */ 387 setTitle(title) { 388 this._window.document.title = title; 389 } 390 391 /** 392 * Destroy the window. 393 */ 394 destroy() { 395 if (!this._destroyed) { 396 this._destroyed = true; 397 398 this._window.removeEventListener("unload", this._boundUnload); 399 this._window.close(); 400 } 401 402 return Promise.resolve(null); 403 } 404 } 405 406 /** 407 * Host object for the Browser Toolbox 408 */ 409 class BrowserToolboxHost extends EventEmitter { 410 constructor(hostTab, options) { 411 super(); 412 413 this.doc = options.doc; 414 } 415 416 type = "browsertoolbox"; 417 418 async create() { 419 this.frame = createDevToolsFrame( 420 this.doc, 421 "devtools-toolbox-browsertoolbox-iframe" 422 ); 423 424 this.doc.body.appendChild(this.frame); 425 this.frame.docShellIsActive = true; 426 427 return this.frame; 428 } 429 430 /** 431 * Raise the host. 432 */ 433 raise() { 434 this.doc.defaultView.focus(); 435 } 436 437 /** 438 * Set the toolbox title. 439 */ 440 setTitle(title) { 441 this.doc.title = title; 442 } 443 444 // Do nothing. The BrowserToolbox is destroyed by quitting the application. 445 destroy() { 446 return Promise.resolve(null); 447 } 448 } 449 450 /** 451 * Host object for the toolbox as a page. 452 * This is typically used by `about:debugging`, when opening toolbox in a new tab, 453 * via `about:devtools-toolbox` URLs. 454 * The `iframe` ends up being the tab's browser element. 455 */ 456 class PageHost { 457 constructor(hostTab, options) { 458 this.frame = options.customIframe; 459 } 460 461 type = "page"; 462 463 create() { 464 return Promise.resolve(this.frame); 465 } 466 467 // Focus the tab owning the browser element. 468 raise() { 469 // See @constructor, for the page host, the frame is also the browser 470 // element. 471 focusTab(this.frame.ownerGlobal.gBrowser.getTabForBrowser(this.frame)); 472 } 473 474 // Do nothing. 475 setTitle() {} 476 477 // Do nothing. 478 destroy() { 479 return Promise.resolve(null); 480 } 481 } 482 483 /** 484 * Switch to the given tab in a browser and focus the browser window 485 */ 486 function focusTab(tab) { 487 const browserWindow = tab.ownerGlobal; 488 browserWindow.focus(); 489 browserWindow.gBrowser.selectedTab = tab; 490 } 491 492 /** 493 * Create an iframe that can be used to load DevTools via about:devtools-toolbox. 494 */ 495 function createDevToolsFrame(doc, className) { 496 const frame = doc.createXULElement("browser"); 497 frame.setAttribute("type", "content"); 498 frame.style.flex = "1 auto"; // Required to be able to shrink when the window shrinks 499 frame.className = className; 500 501 const inXULDocument = doc.documentElement.namespaceURI === XUL_NS; 502 if (inXULDocument) { 503 // When the toolbox frame is loaded in a XUL document, tooltips rely on a 504 // special XUL <tooltip id="aHTMLTooltip"> element. 505 // This attribute should not be set when the frame is loaded in a HTML 506 // document (for instance: Browser Toolbox). 507 frame.tooltip = "aHTMLTooltip"; 508 } 509 510 // Allows toggling the `docShellIsActive` attribute 511 frame.setAttribute("manualactiveness", "true"); 512 return frame; 513 } 514 515 exports.Hosts = { 516 bottom: BottomHost, 517 left: LeftHost, 518 right: RightHost, 519 window: WindowHost, 520 browsertoolbox: BrowserToolboxHost, 521 page: PageHost, 522 };