/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const MAX_ORDINAL = 99; const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled"; const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; const DEVTOOLS_ALWAYS_ON_TOP = "devtools.toolbox.alwaysOnTop"; const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide"; const PSEUDO_LOCALE_PREF = "intl.l10n.pseudo"; const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST"; const CURRENT_THEME_SCALAR = "devtools.current_theme"; const HTML_NS = "http://www.w3.org/1999/xhtml"; const REGEX_4XX_5XX = /^[4,5]\d\d$/; const BROWSERTOOLBOX_SCOPE_PREF = "devtools.browsertoolbox.scope"; const BROWSERTOOLBOX_SCOPE_EVERYTHING = "everything"; const BROWSERTOOLBOX_SCOPE_PARENTPROCESS = "parent-process"; const { debounce } = require("resource://devtools/shared/debounce.js"); const { throttle } = require("resource://devtools/shared/throttle.js"); const { safeAsyncMethod, } = require("resource://devtools/shared/async-utils.js"); var { gDevTools } = require("resource://devtools/client/framework/devtools.js"); var EventEmitter = require("resource://devtools/shared/event-emitter.js"); const Selection = require("resource://devtools/client/framework/selection.js"); var Telemetry = require("resource://devtools/client/shared/telemetry.js"); const { getUnicodeUrl, } = require("resource://devtools/client/shared/unicode-url.js"); var { DOMHelpers } = require("resource://devtools/shared/dom-helpers.js"); const { KeyCodes } = require("resource://devtools/client/shared/keycodes.js"); const { FluentL10n, } = require("resource://devtools/client/shared/fluent-l10n/fluent-l10n.js"); var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService( Ci.nsISupports ).wrappedJSObject; const { BrowserLoader } = ChromeUtils.import( "resource://devtools/shared/loader/browser-loader.js" ); const { MultiLocalizationHelper, } = require("resource://devtools/shared/l10n.js"); const L10N = new MultiLocalizationHelper( "devtools/client/locales/toolbox.properties", "chrome://branding/locale/brand.properties" ); loader.lazyRequireGetter( this, "registerStoreObserver", "resource://devtools/client/shared/redux/subscriber.js", true ); loader.lazyRequireGetter( this, "createToolboxStore", "resource://devtools/client/framework/store.js", true ); loader.lazyRequireGetter( this, ["registerWalkerListeners"], "resource://devtools/client/framework/actions/index.js", true ); loader.lazyRequireGetter( this, ["selectTarget"], "resource://devtools/shared/commands/target/actions/targets.js", true ); const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { AppConstants: "resource://gre/modules/AppConstants.sys.mjs", }); loader.lazyRequireGetter(this, "flags", "resource://devtools/shared/flags.js"); loader.lazyRequireGetter( this, "KeyShortcuts", "resource://devtools/client/shared/key-shortcuts.js" ); loader.lazyRequireGetter( this, "ZoomKeys", "resource://devtools/client/shared/zoom-keys.js" ); loader.lazyRequireGetter( this, "ToolboxButtons", "resource://devtools/client/definitions.js", true ); loader.lazyRequireGetter( this, "SourceMapURLService", "resource://devtools/client/framework/source-map-url-service.js", true ); loader.lazyRequireGetter( this, "BrowserConsoleManager", "resource://devtools/client/webconsole/browser-console-manager.js", true ); loader.lazyRequireGetter( this, "viewSource", "resource://devtools/client/shared/view-source.js" ); loader.lazyRequireGetter( this, "buildHarLog", "resource://devtools/client/netmonitor/src/har/har-builder-utils.js", true ); loader.lazyRequireGetter( this, "NetMonitorAPI", "resource://devtools/client/netmonitor/src/api.js", true ); loader.lazyRequireGetter( this, "sortPanelDefinitions", "resource://devtools/client/framework/toolbox-tabs-order-manager.js", true ); loader.lazyRequireGetter( this, "createEditContextMenu", "resource://devtools/client/framework/toolbox-context-menu.js", true ); loader.lazyRequireGetter( this, "getSelectedTarget", "resource://devtools/shared/commands/target/selectors/targets.js", true ); loader.lazyRequireGetter( this, "remoteClientManager", "resource://devtools/client/shared/remote-debugging/remote-client-manager.js", true ); loader.lazyRequireGetter( this, "ResponsiveUIManager", "resource://devtools/client/responsive/manager.js" ); loader.lazyRequireGetter( this, "DevToolsUtils", "resource://devtools/shared/DevToolsUtils.js" ); loader.lazyRequireGetter( this, "NodePicker", "resource://devtools/client/inspector/node-picker.js" ); loader.lazyGetter(this, "domNodeConstants", () => { return require("resource://devtools/shared/dom-node-constants.js"); }); loader.lazyRequireGetter( this, "NodeFront", "resource://devtools/client/fronts/node.js", true ); loader.lazyRequireGetter( this, "PICKER_TYPES", "resource://devtools/shared/picker-constants.js" ); loader.lazyRequireGetter( this, "HarAutomation", "resource://devtools/client/netmonitor/src/har/har-automation.js", true ); loader.lazyRequireGetter( this, "getThreadOptions", "resource://devtools/client/shared/thread-utils.js", true ); loader.lazyRequireGetter( this, "SourceMapLoader", "resource://devtools/client/shared/source-map-loader/index.js", true ); /** * A "Toolbox" is the component that holds all the tools for one specific * target. Visually, it's a document that includes the tools tabs and all * the iframes where the tool panels will be living in. * * @param {object} commands * The context to inspect identified by this commands. * @param {string} selectedTool * Tool to select initially * @param {Toolbox.HostType} hostType * Type of host that will host the toolbox (e.g. sidebar, window) * @param {DOMWindow} contentWindow * The window object of the toolbox document * @param {string} frameId * A unique identifier to differentiate toolbox documents from the * chrome codebase when passing DOM messages */ function Toolbox(commands, selectedTool, hostType, contentWindow, frameId) { this._win = contentWindow; this.frameId = frameId; this.selection = new Selection(); this.telemetry = new Telemetry({ useSessionId: true }); // This attribute helps identify one particular toolbox instance. this.sessionId = this.telemetry.sessionId; // This attribute is meant to be a public attribute on the Toolbox object // It exposes commands modules listed in devtools/shared/commands/index.js // which are an abstraction on top of RDP methods. // See devtools/shared/commands/README.md this.commands = commands; this._descriptorFront = commands.descriptorFront; // Map of the available DevTools WebExtensions: // Map this._webExtensions = new Map(); this._toolPanels = new Map(); this._inspectorExtensionSidebars = new Map(); this._netMonitorAPI = null; // Map of frames (id => frame-info) and currently selected frame id. this.frameMap = new Map(); this.selectedFrameId = null; // Number of targets currently paused this._pausedTargets = 0; /** * KeyShortcuts instance specific to WINDOW host type. * This is the key shortcuts that are only register when the toolbox * is loaded in its own window. Otherwise, these shortcuts are typically * registered by devtools-startup.js module. */ this._windowHostShortcuts = null; this._toolRegistered = this._toolRegistered.bind(this); this._toolUnregistered = this._toolUnregistered.bind(this); this._refreshHostTitle = this._refreshHostTitle.bind(this); this.toggleNoAutohide = this.toggleNoAutohide.bind(this); this.toggleAlwaysOnTop = this.toggleAlwaysOnTop.bind(this); this.disablePseudoLocale = () => this.changePseudoLocale("none"); this.enableAccentedPseudoLocale = () => this.changePseudoLocale("accented"); this.enableBidiPseudoLocale = () => this.changePseudoLocale("bidi"); this._updateFrames = this._updateFrames.bind(this); this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this); this.closeToolbox = this.closeToolbox.bind(this); this.destroy = this.destroy.bind(this); this._applyCacheSettings = this._applyCacheSettings.bind(this); this._applyCustomFormatterSetting = this._applyCustomFormatterSetting.bind(this); this._applyServiceWorkersTestingSettings = this._applyServiceWorkersTestingSettings.bind(this); this._applySimpleHighlightersSettings = this._applySimpleHighlightersSettings.bind(this); this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this); this._onFocus = this._onFocus.bind(this); this._onBlur = this._onBlur.bind(this); this._onBrowserMessage = this._onBrowserMessage.bind(this); this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this); this._onToolbarFocus = this._onToolbarFocus.bind(this); this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this); this._onPickerClick = this._onPickerClick.bind(this); this._onPickerKeypress = this._onPickerKeypress.bind(this); this._onPickerStarting = this._onPickerStarting.bind(this); this._onPickerStarted = this._onPickerStarted.bind(this); this._onPickerStopped = this._onPickerStopped.bind(this); this._onPickerCanceled = this._onPickerCanceled.bind(this); this._onPickerPicked = this._onPickerPicked.bind(this); this._onPickerPreviewed = this._onPickerPreviewed.bind(this); this._onInspectObject = this._onInspectObject.bind(this); this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this); this._onToolSelected = this._onToolSelected.bind(this); this._onContextMenu = this._onContextMenu.bind(this); this._onMouseDown = this._onMouseDown.bind(this); this.updateToolboxButtonsVisibility = this.updateToolboxButtonsVisibility.bind(this); this.updateToolboxButtons = this.updateToolboxButtons.bind(this); this.selectTool = this.selectTool.bind(this); this._pingTelemetrySelectTool = this._pingTelemetrySelectTool.bind(this); this.toggleSplitConsole = this.toggleSplitConsole.bind(this); this.toggleOptions = this.toggleOptions.bind(this); this._onTargetAvailable = this._onTargetAvailable.bind(this); this._onTargetDestroyed = this._onTargetDestroyed.bind(this); this._onTargetSelected = this._onTargetSelected.bind(this); this._onResourceAvailable = this._onResourceAvailable.bind(this); this._onResourceUpdated = this._onResourceUpdated.bind(this); this._onToolSelectedStopPicker = this._onToolSelectedStopPicker.bind(this); // `component` might be null if the toolbox was destroying during the throttling this._throttledSetToolboxButtons = throttle( () => this.component?.setToolboxButtons(this.toolbarButtons), 500, this ); this._debounceUpdateFocusedState = debounce( () => { this.component?.setFocusedState(this._isToolboxFocused); }, 500, this ); if (!selectedTool) { selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL); } this._defaultToolId = selectedTool; this._hostType = hostType; this.isOpen = new Promise( function (resolve) { this._resolveIsOpen = resolve; }.bind(this) ); EventEmitter.decorate(this); this.on("host-changed", this._refreshHostTitle); this.on("select", this._onToolSelected); this.selection.on("new-node-front", this._onNewSelectedNodeFront); gDevTools.on("tool-registered", this._toolRegistered); gDevTools.on("tool-unregistered", this._toolUnregistered); /** * Get text direction for the current locale direction. * * `getComputedStyle` forces a synchronous reflow, so use a lazy getter in order to * call it only once. */ loader.lazyGetter(this, "direction", () => { const { documentElement } = this.doc; const isRtl = this.win.getComputedStyle(documentElement).direction === "rtl"; return isRtl ? "rtl" : "ltr"; }); } exports.Toolbox = Toolbox; /** * The toolbox can be 'hosted' either embedded in a browser window * or in a separate window. */ Toolbox.HostType = { BOTTOM: "bottom", RIGHT: "right", LEFT: "left", WINDOW: "window", BROWSERTOOLBOX: "browsertoolbox", // This is typically used by `about:debugging`, when opening toolbox in a new tab, // via `about:devtools-toolbox` URLs. PAGE: "page", }; Toolbox.prototype = { _URL: "about:devtools-toolbox", _prefs: { LAST_TOOL: "devtools.toolbox.selectedTool", }, get nodePicker() { if (!this._nodePicker) { this._nodePicker = new NodePicker(this.commands, this.selection); this._nodePicker.on("picker-starting", this._onPickerStarting); this._nodePicker.on("picker-started", this._onPickerStarted); this._nodePicker.on("picker-stopped", this._onPickerStopped); this._nodePicker.on("picker-node-canceled", this._onPickerCanceled); this._nodePicker.on("picker-node-picked", this._onPickerPicked); this._nodePicker.on("picker-node-previewed", this._onPickerPreviewed); } return this._nodePicker; }, get store() { if (!this._store) { this._store = createToolboxStore(); } return this._store; }, get currentToolId() { return this._currentToolId; }, set currentToolId(id) { this._currentToolId = id; this.component.setCurrentToolId(id); }, get defaultToolId() { return this._defaultToolId; }, get panelDefinitions() { return this._panelDefinitions; }, set panelDefinitions(definitions) { this._panelDefinitions = definitions; this._combineAndSortPanelDefinitions(); }, get visibleAdditionalTools() { if (!this._visibleAdditionalTools) { this._visibleAdditionalTools = []; } return this._visibleAdditionalTools; }, set visibleAdditionalTools(tools) { this._visibleAdditionalTools = tools; if (this.isReady) { this._combineAndSortPanelDefinitions(); } }, /** * Combines the built-in panel definitions and the additional tool definitions that * can be set by add-ons. */ _combineAndSortPanelDefinitions() { let definitions = [ ...this._panelDefinitions, ...this.getVisibleAdditionalTools(), ]; definitions = sortPanelDefinitions(definitions); this.component.setPanelDefinitions(definitions); }, lastUsedToolId: null, /** * Returns a *copy* of the _toolPanels collection. * * @return {Map} panels * All the running panels in the toolbox */ getToolPanels() { return new Map(this._toolPanels); }, /** * Access the panel for a given tool */ getPanel(id) { return this._toolPanels.get(id); }, /** * Get the panel instance for a given tool once it is ready. * If the tool is already opened, the promise will resolve immediately, * otherwise it will wait until the tool has been opened before resolving. * * Note that this does not open the tool, use selectTool if you'd * like to select the tool right away. * * @param {String} id * The id of the panel, for example "jsdebugger". * @returns Promise * A promise that resolves once the panel is ready. */ getPanelWhenReady(id) { const panel = this.getPanel(id); return new Promise(resolve => { if (panel) { resolve(panel); } else { this.on(id + "-ready", initializedPanel => { resolve(initializedPanel); }); } }); }, /** * This is a shortcut for getPanel(currentToolId) because it is much more * likely that we're going to want to get the panel that we've just made * visible */ getCurrentPanel() { return this._toolPanels.get(this.currentToolId); }, /** * Get the current top level target the toolbox is debugging. * * This will only be defined *after* calling Toolbox.open(), * after it has called `targetCommands.startListening`. */ get target() { return this.commands.targetCommand.targetFront; }, get threadFront() { return this.commands.targetCommand.targetFront.threadFront; }, /** * Get/alter the host of a Toolbox, i.e. is it in browser or in a separate * tab. See HostType for more details. */ get hostType() { return this._hostType; }, /** * Shortcut to the window containing the toolbox UI */ get win() { return this._win; }, /** * When the toolbox is loaded in a frame with type="content", win.parent will not return * the parent Chrome window. This getter should return the parent Chrome window * regardless of the frame type. See Bug 1539979. */ get topWindow() { return DevToolsUtils.getTopWindow(this.win); }, get topDoc() { return this.topWindow.document; }, /** * Shortcut to the document containing the toolbox UI */ get doc() { return this.win.document; }, /** * Get the toggled state of the split console */ get splitConsole() { return this._splitConsole; }, /** * Get the focused state of the split console */ isSplitConsoleFocused() { if (!this._splitConsole) { return false; } const focusedWin = Services.focus.focusedWindow; return ( focusedWin && focusedWin === this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow ); }, get isBrowserToolbox() { return this.hostType === Toolbox.HostType.BROWSERTOOLBOX; }, get isMultiProcessBrowserToolbox() { return this.isBrowserToolbox; }, /** * Set a given target as selected (which may impact the console evaluation context selector). * * @param {String} targetActorID: The actorID of the target we want to select. */ selectTarget(targetActorID) { if (this.getSelectedTargetFront()?.actorID !== targetActorID) { // The selected target is managed by the TargetCommand's store. // So dispatch this action against that other store. this.commands.targetCommand.store.dispatch(selectTarget(targetActorID)); } }, /** * @returns {ThreadFront|null} The selected thread front, or null if there is none. */ getSelectedTargetFront() { // The selected target is managed by the TargetCommand's store. // So pull the state from that other store. const selectedTarget = getSelectedTarget( this.commands.targetCommand.store.getState() ); if (!selectedTarget) { return null; } return this.commands.client.getFrontByID(selectedTarget.actorID); }, /** * For now, the debugger isn't hooked to TargetCommand's store * to display its thread list. So manually forward target selection change * to the debugger via a dedicated action */ _onTargetCommandStateChange(state, oldState) { if (getSelectedTarget(state) !== getSelectedTarget(oldState)) { const dbg = this.getPanel("jsdebugger"); if (!dbg) { return; } const threadActorID = getSelectedTarget(state)?.threadFront?.actorID; if (!threadActorID) { return; } dbg.selectThread(threadActorID); } }, /** * Called on each new THREAD_STATE resource * * @param {Object} resource The THREAD_STATE resource */ _onThreadStateChanged(resource) { if (resource.state == "paused") { this._pauseToolbox(resource.why.type); } else if (resource.state == "resumed") { this._resumeToolbox(); } }, /** * Be careful, this method is synchronous, but highlightTool, raise, selectTool * are all async. */ _pauseToolbox(reason) { // Suppress interrupted events by default because the thread is // paused/resumed a lot for various actions. if (reason === "interrupted") { return; } this.highlightTool("jsdebugger"); if ( reason === "debuggerStatement" || reason === "mutationBreakpoint" || reason === "eventBreakpoint" || reason === "breakpoint" || reason === "exception" || reason === "resumeLimit" || reason === "XHR" ) { this.raise(); this.selectTool("jsdebugger", reason); // Each Target/Thread can be paused only once at a time, // so, for each pause, we should have a related resumed event. // But we may have multiple targets paused at the same time this._pausedTargets++; this.emit("toolbox-paused"); } }, _resumeToolbox() { if (this.isHighlighted("jsdebugger")) { this._pausedTargets--; if (this._pausedTargets == 0) { this.emit("toolbox-resumed"); this.unhighlightTool("jsdebugger"); } } }, /** * This method will be called for the top-level target, as well as any potential * additional targets we may care about. */ async _onTargetAvailable({ targetFront, isTargetSwitching }) { if (targetFront.isTopLevel) { // Attach to a new top-level target. // For now, register these event listeners only on the top level target if (!targetFront.targetForm.ignoreSubFrames) { targetFront.on("frame-update", this._updateFrames); } const consoleFront = await targetFront.getFront("console"); consoleFront.on("inspectObject", this._onInspectObject); } // Walker listeners allow to monitor DOM Mutation breakpoint updates. // All targets should be monitored. targetFront.watchFronts("inspector", async inspectorFront => { registerWalkerListeners(this.store, inspectorFront.walker); }); if (targetFront.isTopLevel && isTargetSwitching) { // These methods expect the target to be attached, which is guaranteed by the time // _onTargetAvailable is called by the targetCommand. await this._listFrames(); // The target may have been destroyed while calling _listFrames if we navigate quickly if (targetFront.isDestroyed()) { return; } } if (targetFront.targetForm.ignoreSubFrames) { this._updateFrames({ frames: [ { id: targetFront.actorID, targetFront, url: targetFront.url, title: targetFront.title, isTopLevel: targetFront.isTopLevel, }, ], }); } // If a new popup is debugged, automagically switch the toolbox to become // an independant window so that we can easily keep debugging the new tab. // Only do that if that's not the current top level, otherwise it means // we opened a toolbox dedicated to the popup. if ( targetFront.targetForm.isPopup && !targetFront.isTopLevel && this._descriptorFront.isLocalTab ) { await this.switchHostToTab(targetFront.targetForm.browsingContextID); } }, async _onTargetSelected({ targetFront }) { this._updateFrames({ selected: targetFront.actorID }); this.selectTarget(targetFront.actorID); }, _onTargetDestroyed({ targetFront }) { if (targetFront.isTopLevel) { const consoleFront = targetFront.getCachedFront("console"); // If the target has already been destroyed, its console front will // also already be destroyed and so we won't be able to retrieve it. // Nor is it important to clear its listener as fronts automatically clears // all their listeners on destroy. if (consoleFront) { consoleFront.off("inspectObject", this._onInspectObject); } targetFront.off("frame-update", this._updateFrames); } else if (this.selection) { this.selection.onTargetDestroyed(targetFront); } // When navigating the old (top level) target can get destroyed before the thread state changed // event for the target is received, so it gets lost. This currently happens with bf-cache // navigations when paused, so lets make sure we resumed if not. // // We should also resume if a paused non-top-level target is destroyed if (targetFront.isTopLevel || targetFront.threadFront?.paused) { this._resumeToolbox(); } if (targetFront.targetForm.ignoreSubFrames) { this._updateFrames({ frames: [ { id: targetFront.actorID, destroy: true, }, ], }); } }, _onTargetThreadFrontResumeWrongOrder() { const box = this.getNotificationBox(); box.appendNotification( L10N.getStr("toolbox.resumeOrderWarning"), "wrong-resume-order", "", box.PRIORITY_WARNING_HIGH ); }, /** * Open the toolbox */ open() { return async function () { // Kick off async loading the Fluent bundles. const fluentL10n = new FluentL10n(); const fluentInitPromise = fluentL10n.init([ "devtools/client/toolbox.ftl", ]); const isToolboxURL = this.win.location.href.startsWith(this._URL); if (isToolboxURL) { // Update the URL so that onceDOMReady watch for the right url. this._URL = this.win.location.href; } const domReady = new Promise(resolve => { DOMHelpers.onceDOMReady( this.win, () => { resolve(); }, this._URL ); }); this.commands.targetCommand.on( "target-thread-wrong-order-on-resume", this._onTargetThreadFrontResumeWrongOrder.bind(this) ); registerStoreObserver( this.commands.targetCommand.store, this._onTargetCommandStateChange.bind(this) ); // Bug 1709063: Use commands.resourceCommand instead of toolbox.resourceCommand this.resourceCommand = this.commands.resourceCommand; // Optimization: fire up a few other things before waiting on // the iframe being ready (makes startup faster) await this.commands.targetCommand.startListening(); // Lets get the current thread settings from the prefs and // update the threadConfigurationActor which should manage // updating the current threads. const options = await getThreadOptions(); await this.commands.threadConfigurationCommand.updateConfiguration( options ); // This needs to be done before watching for resources so console messages can be // custom formatted right away. await this._applyCustomFormatterSetting(); // The targetCommand is created right before this code. // It means that this call to watchTargets is the first, // and we are registering the first target listener, which means // Toolbox._onTargetAvailable will be called first, before any other // onTargetAvailable listener that might be registered on targetCommand. await this.commands.targetCommand.watchTargets({ types: this.commands.targetCommand.ALL_TYPES, onAvailable: this._onTargetAvailable, onSelected: this._onTargetSelected, onDestroyed: this._onTargetDestroyed, }); const watchedResources = [ // Watch for console API messages, errors and network events in order to populate // the error count icon in the toolbox. this.resourceCommand.TYPES.CONSOLE_MESSAGE, this.resourceCommand.TYPES.ERROR_MESSAGE, this.resourceCommand.TYPES.DOCUMENT_EVENT, this.resourceCommand.TYPES.THREAD_STATE, ]; if (!this.isBrowserToolbox) { // Independently of watching network event resources for the error count icon, // we need to start tracking network activity on toolbox open for targets such // as tabs, in order to ensure there is always at least one listener existing // for network events across the lifetime of the various panels, so stopping // the resource command from clearing out its cache of network event resources. watchedResources.push(this.resourceCommand.TYPES.NETWORK_EVENT); } const onResourcesWatched = this.resourceCommand.watchResources( watchedResources, { onAvailable: this._onResourceAvailable, onUpdated: this._onResourceUpdated, } ); await domReady; this.browserRequire = BrowserLoader({ window: this.win, useOnlyShared: true, }).require; this.isReady = true; const framesPromise = this._listFrames(); Services.prefs.addObserver( "devtools.cache.disabled", this._applyCacheSettings ); Services.prefs.addObserver( "devtools.custom-formatters.enabled", this._applyCustomFormatterSetting ); Services.prefs.addObserver( "devtools.serviceWorkers.testing.enabled", this._applyServiceWorkersTestingSettings ); Services.prefs.addObserver( "devtools.inspector.simple-highlighters-reduced-motion", this._applySimpleHighlightersSettings ); Services.prefs.addObserver( BROWSERTOOLBOX_SCOPE_PREF, this._refreshHostTitle ); // Get the DOM element to mount the ToolboxController to. this._componentMount = this.doc.getElementById("toolbox-toolbar-mount"); await fluentInitPromise; // Mount the ToolboxController component and update all its state // that can be updated synchronousl this._mountReactComponent(fluentL10n.getBundles()); this._buildDockOptions(); this._buildInitialPanelDefinitions(); this._setDebugTargetData(); // Forward configuration flags to the DevTools server. this._applyCacheSettings(); this._applyServiceWorkersTestingSettings(); this._applySimpleHighlightersSettings(); this._addWindowListeners(); this._addChromeEventHandlerEvents(); // Get the tab bar of the ToolboxController to attach the "keypress" event listener to. this._tabBar = this.doc.querySelector(".devtools-tabbar"); this._tabBar.addEventListener("keypress", this._onToolbarArrowKeypress); this._componentMount.setAttribute( "aria-label", L10N.getStr("toolbox.label") ); this.webconsolePanel = this.doc.querySelector( "#toolbox-panel-webconsole" ); this.webconsolePanel.style.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF) + "px"; this.webconsolePanel.addEventListener( "resize", this._saveSplitConsoleHeight ); this._buildButtons(); this._pingTelemetry(); // The isToolSupported check needs to happen after the target is // remoted, otherwise we could have done it in the toolbox constructor // (bug 1072764). const toolDef = gDevTools.getToolDefinition(this._defaultToolId); if (!toolDef || !toolDef.isToolSupported(this)) { this._defaultToolId = "webconsole"; } // Update all ToolboxController state that can only be done asynchronously await this._setInitialMeatballState(); // Start rendering the toolbox toolbar before selecting the tool, as the tools // can take a few hundred milliseconds seconds to start up. // // Delay React rendering as Toolbox.open is synchronous. // Even if this involve promises, it is synchronous. Toolbox.open already loads // react modules and freeze the event loop for a significant time. // requestIdleCallback allows releasing it to allow user events to be processed. // Use 16ms maximum delay to allow one frame to be rendered at 60FPS // (1000ms/60FPS=16ms) this.win.requestIdleCallback( () => { this.component.setCanRender(); }, { timeout: 16 } ); await this.selectTool(this._defaultToolId, "initial_panel"); // Wait until the original tool is selected so that the split // console input will receive focus. let splitConsolePromise = Promise.resolve(); if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) { splitConsolePromise = this.openSplitConsole(); this.telemetry.addEventProperty( this.topWindow, "open", "tools", null, "splitconsole", true ); } else { this.telemetry.addEventProperty( this.topWindow, "open", "tools", null, "splitconsole", false ); } await Promise.all([ splitConsolePromise, framesPromise, onResourcesWatched, ]); // We do not expect the focus to be restored when using about:debugging toolboxes // Otherwise, when reloading the toolbox, the debugged tab will be focused. if (this.hostType !== Toolbox.HostType.PAGE) { // Request the actor to restore the focus to the content page once the // target is detached. This typically happens when the console closes. // We restore the focus as it may have been stolen by the console input. await this.commands.targetConfigurationCommand.updateConfiguration({ restoreFocus: true, }); } await this.initHarAutomation(); this.emit("ready"); this._resolveIsOpen(); } .bind(this)() .catch(e => { console.error("Exception while opening the toolbox", String(e), e); // While the exception stack is correctly printed in the Browser console when // passing `e` to console.error, it is not on the stdout, so print it via dump. dump(e.stack + "\n"); }); }, /** * Retrieve the ChromeEventHandler associated to the toolbox frame. * When DevTools are loaded in a content frame, this will return the containing chrome * frame. Events from nested frames will bubble up to this chrome frame, which allows to * listen to events from nested frames. */ getChromeEventHandler() { if (!this.win || !this.win.docShell) { return null; } return this.win.docShell.chromeEventHandler; }, /** * Attach events on the chromeEventHandler for the current window. When loaded in a * frame with type set to "content", events will not bubble across frames. The * chromeEventHandler does not have this limitation and will catch all events triggered * on any of the frames under the devtools document. * * Events relying on the chromeEventHandler need to be added and removed at specific * moments in the lifecycle of the toolbox, so all the events relying on it should be * grouped here. */ _addChromeEventHandlerEvents() { // win.docShell.chromeEventHandler might not be accessible anymore when removing the // events, so we can't rely on a dynamic getter here. // Keep a reference on the chromeEventHandler used to addEventListener to be sure we // can remove the listeners afterwards. this._chromeEventHandler = this.getChromeEventHandler(); if (!this._chromeEventHandler) { return; } // Add shortcuts and window-host-shortcuts that use the ChromeEventHandler as target. this._addShortcuts(); this._addWindowHostShortcuts(); this._chromeEventHandler.addEventListener( "keypress", this._splitConsoleOnKeypress ); this._chromeEventHandler.addEventListener("focus", this._onFocus, true); this._chromeEventHandler.addEventListener("blur", this._onBlur, true); this._chromeEventHandler.addEventListener( "contextmenu", this._onContextMenu ); this._chromeEventHandler.addEventListener("mousedown", this._onMouseDown); }, _removeChromeEventHandlerEvents() { if (!this._chromeEventHandler) { return; } // Remove shortcuts and window-host-shortcuts that use the ChromeEventHandler as // target. this._removeShortcuts(); this._removeWindowHostShortcuts(); this._chromeEventHandler.removeEventListener( "keypress", this._splitConsoleOnKeypress ); this._chromeEventHandler.removeEventListener("focus", this._onFocus, true); this._chromeEventHandler.removeEventListener("focus", this._onBlur, true); this._chromeEventHandler.removeEventListener( "contextmenu", this._onContextMenu ); this._chromeEventHandler.removeEventListener( "mousedown", this._onMouseDown ); this._chromeEventHandler = null; }, _addShortcuts() { // Create shortcuts instance for the toolbox if (!this.shortcuts) { this.shortcuts = new KeyShortcuts({ window: this.doc.defaultView, // The toolbox key shortcuts should be triggered from any frame in DevTools. // Use the chromeEventHandler as the target to catch events from all frames. target: this.getChromeEventHandler(), }); } // Listen for the shortcut key to show the frame list this.shortcuts.on(L10N.getStr("toolbox.showFrames.key"), event => { if (event.target.id === "command-button-frames") { event.target.click(); } }); // Listen for tool navigation shortcuts. this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"), event => { this.selectNextTool(); event.preventDefault(); }); this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"), event => { this.selectPreviousTool(); event.preventDefault(); }); this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"), event => { this.switchToPreviousHost(); event.preventDefault(); }); // List for Help/Settings key. this.shortcuts.on(L10N.getStr("toolbox.help.key"), this.toggleOptions); if (!this.isBrowserToolbox) { // Listen for Reload shortcuts [ ["reload", false], ["reload2", false], ["forceReload", true], ["forceReload2", true], ].forEach(([id, force]) => { const key = L10N.getStr("toolbox." + id + ".key"); this.shortcuts.on(key, event => { this.commands.targetCommand.reloadTopLevelTarget(force); // Prevent Firefox shortcuts from reloading the page event.preventDefault(); }); }); } // Add zoom-related shortcuts. if (this.hostType != Toolbox.HostType.PAGE) { // When the toolbox is rendered in a tab (ie host type is PAGE), the // zoom should be handled by the default browser shortcuts. ZoomKeys.register(this.win, this.shortcuts); } }, _removeShortcuts() { if (this.shortcuts) { this.shortcuts.destroy(); this.shortcuts = null; } }, /** * Adds the keys and commands to the Toolbox Window in window mode. */ _addWindowHostShortcuts() { if (this.hostType != Toolbox.HostType.WINDOW) { // Those shortcuts are only valid for host type WINDOW. return; } if (!this._windowHostShortcuts) { this._windowHostShortcuts = new KeyShortcuts({ window: this.win, // The window host key shortcuts should be triggered from any frame in DevTools. // Use the chromeEventHandler as the target to catch events from all frames. target: this.getChromeEventHandler(), }); } const shortcuts = this._windowHostShortcuts; for (const item of Startup.KeyShortcuts) { const { id, toolId, shortcut, modifiers } = item; const electronKey = KeyShortcuts.parseXulKey(modifiers, shortcut); if (id == "browserConsole") { // Add key for toggling the browser console from the detached window shortcuts.on(electronKey, () => { BrowserConsoleManager.toggleBrowserConsole(); }); } else if (toolId) { // KeyShortcuts contain tool-specific and global key shortcuts, // here we only need to copy shortcut specific to each tool. shortcuts.on(electronKey, () => { this.selectTool(toolId, "key_shortcut").then(() => this.fireCustomKey(toolId) ); }); } } // CmdOrCtrl+W is registered only when the toolbox is running in // detached window. In the other case the entire browser tab // is closed when the user uses this shortcut. shortcuts.on(L10N.getStr("toolbox.closeToolbox.key"), this.closeToolbox); // The others are only registered in window host type as for other hosts, // these keys are already registered by devtools-startup.js shortcuts.on( L10N.getStr("toolbox.toggleToolboxF12.key"), this.closeToolbox ); if (lazy.AppConstants.platform == "macosx") { shortcuts.on( L10N.getStr("toolbox.toggleToolboxOSX.key"), this.closeToolbox ); } else { shortcuts.on(L10N.getStr("toolbox.toggleToolbox.key"), this.closeToolbox); } }, _removeWindowHostShortcuts() { if (this._windowHostShortcuts) { this._windowHostShortcuts.destroy(); this._windowHostShortcuts = null; } }, _onContextMenu(e) { // Handle context menu events in standard input elements: and