From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- devtools/client/framework/toolbox.js | 4787 ++++++++++++++++++++++++++++++++++ 1 file changed, 4787 insertions(+) create mode 100644 devtools/client/framework/toolbox.js (limited to 'devtools/client/framework/toolbox.js') diff --git a/devtools/client/framework/toolbox.js b/devtools/client/framework/toolbox.js new file mode 100644 index 0000000000..2294ff879d --- /dev/null +++ b/devtools/client/framework/toolbox.js @@ -0,0 +1,4787 @@ +/* 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", "removeTarget"], + "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 +); +loader.lazyRequireGetter( + this, + "openProfilerTab", + "resource://devtools/client/performance-new/shared/browser.js", + true +); +loader.lazyGetter(this, "ProfilerBackground", () => { + return ChromeUtils.importESModule( + "resource://devtools/client/performance-new/shared/background.sys.mjs" + ); +}); + +/** + * 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(); + } + }, + + /** + * Called on each new JSTRACER_STATE resource + * + * @param {Object} resource The JSTRACER_STATE resource + */ + async _onTracingStateChanged(resource) { + const { profile } = resource; + if (!profile) { + return; + } + const browser = await openProfilerTab(); + + const profileCaptureResult = { + type: "SUCCESS", + profile, + }; + ProfilerBackground.registerProfileCaptureForBrowser( + browser, + profileCaptureResult, + null + ); + }, + + /** + * 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" || + reason === "breakpointConditionThrown" + ) { + 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 }) { + removeTarget(this.store, 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, + ]; + + let tracerInitialization; + if ( + Services.prefs.getBoolPref( + "devtools.debugger.features.javascript-tracing", + false + ) + ) { + watchedResources.push(this.resourceCommand.TYPES.JSTRACER_STATE); + tracerInitialization = this.commands.tracerCommand.initialize(); + } + + 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, + tracerInitialization, + ]); + + // 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