/* 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_OPEN_PREF =
"devtools.toolbox.splitconsole.open";
const SPLITCONSOLE_ENABLED_PREF =
"devtools.toolbox.splitconsole.enabled";
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 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.importESModule(
"resource://devtools/shared/loader/browser-loader.sys.mjs"
);
const {
MultiLocalizationHelper,
} = require(
"resource://devtools/shared/l10n.js");
const L10N =
new MultiLocalizationHelper(
"devtools/client/locales/toolbox.properties",
"chrome://branding/locale/brand.properties",
"devtools/client/locales/menus.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
);
loader.lazyRequireGetter(
this,
"TRACER_LOG_METHODS",
"resource://devtools/shared/specs/tracer.js",
true
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AppConstants:
"resource://gre/modules/AppConstants.sys.mjs",
TYPES:
"resource://devtools/shared/highlighters.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"
);
});
const BOOLEAN_CONFIGURATION_PREFS = {
"devtools.cache.disabled": {
name:
"cacheDisabled",
},
"devtools.custom-formatters.enabled": {
name:
"customFormatters",
},
"devtools.serviceWorkers.testing.enabled": {
name:
"serviceWorkersTestingEnabled",
},
"devtools.inspector.simple-highlighters-reduced-motion": {
name:
"useSimpleHighlightersForReducedMotion",
},
"devtools.debugger.features.overlay": {
name:
"pauseOverlay",
thread:
true,
},
"devtools.debugger.features.javascript-tracing": {
name:
"isTracerFeatureEnabled",
},
};
/**
* 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<extensionUUID, extensionName>
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 =
new Set();
/**
* 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._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 the enabled split console setting, and if it's not set, set it with updateIsSplitConsoleEnabled
* @returns {boolean} devtools.toolbox.splitconsole.enabled option
*/
isSplitConsoleEnabled() {
if (
typeof this._splitConsoleEnabled !==
"boolean") {
this.updateIsSplitConsoleEnabled();
}
return this._splitConsoleEnabled;
},
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._onTargetPaused(resource.targetFront, resource.why.type);
}
else if (resource.state ==
"resumed") {
this._onTargetResumed(resource.targetFront);
}
},
/**
* This listener is called by TracerCommand, sooner than the JSTRACER_STATE resource.
* This is called when the frontend toggles the tracer, before the server started interpreting the request.
* This allows to open the console before we start receiving traces.
*/
async onTracerToggled() {
const { tracerCommand } =
this.commands;
if (!tracerCommand.isTracingEnabled) {
return;
}
const { logMethod } =
this.commands.tracerCommand.getTracingOptions();
if (
logMethod == TRACER_LOG_METHODS.CONSOLE &&
this.currentToolId !==
"webconsole"
) {
await
this.openSplitConsole({ focusConsoleInput:
false });
}
else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
const panel = await
this.selectTool(
"jsdebugger");
panel.showTracerSidebar();
}
},
/**
* 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({ defaultPanel:
"stack-chart" });
const profileCaptureResult = {
type:
"SUCCESS",
profile,
};
ProfilerBackground.registerProfileCaptureForBrowser(
browser,
profileCaptureResult,
null
);
},
/**
* Called whenever a given target got its execution paused.
*
* Be careful, this method is synchronous, but highlightTool, raise, selectTool
* are all async.
*
* @param {TargetFront} targetFront
* @param {string} reason
* Reason why the execution paused
*/
_onTargetPaused(targetFront, 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.add(targetFront);
this.emit(
"toolbox-paused");
}
},
/**
* Called whenever a given target got its execution resumed.
*
* @param {TargetFront} targetFront
*/
_onTargetResumed(targetFront) {
if (
this.isHighlighted(
"jsdebugger")) {
this._pausedTargets.
delete(targetFront);
if (
this._pausedTargets.size == 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);
this._refreshHostTitle();
},
_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 ||
this._pausedTargets.has(targetFront)) {
this._onTargetResumed(targetFront);
}
if (targetFront.targetForm.ignoreSubFrames) {
this._updateFrames({
frames: [
{
// The Target Front may already be destroyed and `actorID` be null.
id: targetFront.persistedActorID,
destroy:
true,
},
],
});
}
},
_onTargetThreadFrontResumeWrongOrder() {
const box =
this.getNotificationBox();
box.appendNotification(
L10N.getStr(
"toolbox.resumeOrderWarning"),
"wrong-resume-order",
"",
box.PRIORITY_WARNING_HIGH
);
},
/**
* Open the toolbox
*/
async open() {
try {
// 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;
}
// To avoid any possible artifact, wait for the document to be fully loaded
// before creating the Browser Loader based on toolbox window object.
await
new Promise(resolve => {
DOMHelpers.onceDOMReady(
this.win,
() => {
resolve();
},
this._URL
);
});
// Setup the Toolbox Browser Loader, used to load React component modules
// which expect to be loaded with toolbox.xhtml document as global scope.
this.browserRequire = BrowserLoader({
window:
this.win,
useOnlyShared:
true,
}).require;
// Wait for fluent initialization before mounting React component,
// which depends on it.
await fluentInitPromise;
// Mount toolbox React components and update all its state that can be updated synchronously.
// Do that early as it will be used to render any exception happening next.
this._mountReactComponent(fluentL10n.getBundles());
// Bug 1709063: Use commands.resourceCommand instead of toolbox.resourceCommand
this.resourceCommand =
this.commands.resourceCommand;
this.commands.targetCommand.on(
"target-thread-wrong-order-on-resume",
this._onTargetThreadFrontResumeWrongOrder.bind(
this)
);
registerStoreObserver(
this.commands.targetCommand.store,
this._onTargetCommandStateChange.bind(
this)
);
// Optimization: fire up a few other things before waiting on
// the iframe being ready (makes startup faster)
await
this.commands.targetCommand.startListening();
// Transfer settings early, before watching resources as it may impact them.
// (this is the case for custom formatter pref and console messages)
await
this._listenAndApplyConfigurationPref();
// 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();
this.onTracerToggled =
this.onTracerToggled.bind(
this);
this.commands.tracerCommand.on(
"toggle",
this.onTracerToggled);
}
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,
}
);
this.isReady =
true;
const framesPromise =
this._listFrames();
Services.prefs.addObserver(
BROWSERTOOLBOX_SCOPE_PREF,
this._refreshHostTitle
);
this._buildDockOptions();
this._buildInitialPanelDefinitions();
this._setDebugTargetData();
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_OPEN_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();
}
catch (error) {
console.error(
"Exception while opening the toolbox",
String(error),
error
);
// 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(error.stack +
"\n");
if (error.serverStack) {
dump(
"Server stack:" + error.serverStack +
"\n");
}
// If the exception happens *after* the React component were initialized,
// try to display the exception to the user via AppErrorBoundary component
if (
this._appBoundary) {
this._appBoundary.setState({
errorMsg: error.toString(),
errorStack: error.stack,
errorInfo: {
serverStack: error.serverStack,
},
toolbox:
this,
});
}
}
},
/**
* 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, bypassCache]) => {
const key = L10N.getStr(
"toolbox." + id +
".key");
this.shortcuts.on(key, event => {
this.reload(bypassCache);
// 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);
}
},
/**
* Reload the debugged context.
*
* @param {Boolean} bypassCache
* If true, bypass any cache when reloading.
*/
async reload(bypassCache) {
const box =
this.getNotificationBox();
const notification = box.getNotificationWithValue(
"reload-error");
if (notification) {
notification.close();
}
// When reloading a Web Extension, the top level target isn't destroyed.
// Which prevents some panels (like console and netmonitor) from being correctly cleared.
const consolePanel =
this.getPanel(
"webconsole");
if (consolePanel) {
// Navigation to a null URL will be translated into a reload message
// when persist log is enabled.
consolePanel.hud.ui.handleWillNavigate({
timeStamp:
new Date(),
url:
null,
});
}
const netPanel =
this.getPanel(
"netmonitor");
if (netPanel) {
// Fake a navigation, which will clear the netmonitor, if persists is disabled.
netPanel.panelWin.connector.willNavigate();
}
try {
await
this.commands.targetCommand.reloadTopLevelTarget(bypassCache);
}
catch (e) {
let { message } = e;
// Remove Protocol.JS exception header to focus on the likely manifest error
message = message.replace(
"Protocol error (SyntaxError):",
"");
box.appendNotification(
L10N.getFormatStr(
"toolbox.errorOnReload", message),
"reload-error",
"",
box.PRIORITY_CRITICAL_HIGH
);
}
},
_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: <input> and <textarea>.
// Also support for custom input elements using .devtools-input class
// (e.g. CodeMirror instances).
const isInInput =
e.originalTarget.closest(
"input[type=text]") ||
e.originalTarget.closest(
"input[type=search]") ||
e.originalTarget.closest(
"input:not([type])") ||
e.originalTarget.closest(
".devtools-input") ||
e.originalTarget.closest(
"textarea");
const doc = e.originalTarget.ownerDocument;
const isHTMLPanel = doc.documentElement.namespaceURI === HTML_NS;
if (
// Context-menu events on input elements will use a custom context menu.
isInInput ||
// Context-menu events from HTML panels should not trigger the default
// browser context menu for HTML documents.
isHTMLPanel
) {
e.stopPropagation();
e.preventDefault();
}
if (isInInput) {
this.openTextBoxContextMenu(e.screenX, e.screenY);
}
},
_onMouseDown(e) {
const isMiddleClick = e.button === 1;
if (isMiddleClick) {
// Middle clicks will trigger the scroll lock feature to turn on.
// When the DevTools toolbox was running in an <iframe>, this behavior was
// disabled by default. When running in a <browser> element, we now need
// to catch and preventDefault() on those events.
e.preventDefault();
}
},
_getDebugTargetData() {
const url =
new URL(
this.win.location);
const remoteId = url.searchParams.get(
"remoteId");
const runtimeInfo = remoteClientManager.getRuntimeInfoByRemoteId(remoteId);
const connectionType =
remoteClientManager.getConnectionTypeByRemoteId(remoteId);
return {
connectionType,
runtimeInfo,
descriptorType:
this._descriptorFront.descriptorType,
descriptorName:
this._descriptorFront.name,
};
},
isDebugTargetFenix() {
return this._getDebugTargetData()?.runtimeInfo?.isFenix;
},
/**
* loading React modules when needed (to avoid performance penalties
* during Firefox start up time).
*/
get React() {
return this.browserRequire(
"devtools/client/shared/vendor/react");
},
get ReactDOM() {
return this.browserRequire(
"devtools/client/shared/vendor/react-dom");
},
get ReactRedux() {
return this.browserRequire(
"devtools/client/shared/vendor/react-redux");
},
get ToolboxController() {
return this.browserRequire(
"devtools/client/framework/components/ToolboxController"
);
},
get AppErrorBoundary() {
return this.browserRequire(
"resource://devtools/client/shared/components/AppErrorBoundary.js"
);
},
/**
* A common access point for the client-side mapping service for source maps that
* any panel can use. This is a "low-level" API that connects to
* the source map worker.
*/
get sourceMapLoader() {
if (
this._sourceMapLoader) {
return this._sourceMapLoader;
}
this._sourceMapLoader =
new SourceMapLoader(
this.commands.targetCommand);
return this._sourceMapLoader;
},
/**
* Expose the "Parser" debugger worker to both webconsole and debugger.
*
* Note that the Browser Console will also self-instantiate it as it doesn't involve a toolbox.
*/
get parserWorker() {
if (
this._parserWorker) {
return this._parserWorker;
}
const {
ParserDispatcher,
} = require(
"resource://devtools/client/debugger/src/workers/parser/index.js");
this._parserWorker =
new ParserDispatcher();
return this._parserWorker;
},
/**
* Clients wishing to use source maps but that want the toolbox to
* track the source and style sheet actor mapping can use this
* source map service. This is a higher-level service than the one
* returned by |sourceMapLoader|, in that it automatically tracks
* source and style sheet actor IDs.
*/
get sourceMapURLService() {
if (
this._sourceMapURLService) {
return this._sourceMapURLService;
}
this._sourceMapURLService =
new SourceMapURLService(
this.commands,
this.sourceMapLoader
);
return this._sourceMapURLService;
},
// Return HostType id for telemetry
_getTelemetryHostId() {
switch (
this.hostType) {
case Toolbox.HostType.BOTTOM:
return 0;
case Toolbox.HostType.RIGHT:
return 1;
case Toolbox.HostType.WINDOW:
return 2;
case Toolbox.HostType.BROWSERTOOLBOX:
return 3;
case Toolbox.HostType.LEFT:
return 4;
case Toolbox.HostType.PAGE:
return 5;
default:
return 9;
}
},
// Return HostType string for telemetry
_getTelemetryHostString() {
switch (
this.hostType) {
case Toolbox.HostType.BOTTOM:
return "bottom";
case Toolbox.HostType.LEFT:
return "left";
case Toolbox.HostType.RIGHT:
return "right";
case Toolbox.HostType.WINDOW:
return "window";
case Toolbox.HostType.PAGE:
return "page";
case Toolbox.HostType.BROWSERTOOLBOX:
return "other";
default:
return "bottom";
}
},
_pingTelemetry() {
Services.prefs.setBoolPref(
"devtools.everOpened",
true);
this.telemetry.toolOpened(
"toolbox",
this);
this.telemetry
.getHistogramById(HOST_HISTOGRAM)
.add(
this._getTelemetryHostId());
// Log current theme. The question we want to answer is:
// "What proportion of users use which themes?"
const currentTheme = Services.prefs.getCharPref(
"devtools.theme");
Glean.devtools.currentTheme[currentTheme].add(1);
const browserWin =
this.topWindow;
this.telemetry.preparePendingEvent(browserWin,
"open",
"tools",
null, [
"entrypoint",
"first_panel",
"host",
"shortcut",
"splitconsole",
"width",
]);
this.telemetry.addEventProperty(
browserWin,
"open",
"tools",
null,
"host",
this._getTelemetryHostString()
);
},
/**
* Create a simple object to store the state of a toolbox button. The checked state of
* a button can be updated arbitrarily outside of the scope of the toolbar and its
* controllers. In order to simplify this interaction this object emits an
* "updatechecked" event any time the isChecked value is updated, allowing any consuming
* components to listen and respond to updates.
*
* @param {Object} options:
*
* @property {String} id - The id of the button or command.
* @property {String} className - An optional additional className for the button.
* @property {String} description - The value that will display as a tooltip and in
* the options panel for enabling/disabling.
* @property {Boolean} disabled - An optional disabled state for the button.
* @property {Function} onClick - The function to run when the button is activated by
* click or keyboard shortcut. First argument will be the 'click'
* event, and second argument is the toolbox instance.
* @property {Boolean} isInStartContainer - Buttons can either be placed at the start
* of the toolbar, or at the end.
* @property {Function} setup - Function run immediately to listen for events changing
* whenever the button is checked or unchecked. The toolbox object
* is passed as first argument and a callback is passed as second
* argument, to be called whenever the checked state changes.
* @property {Function} teardown - Function run on toolbox close to let a chance to
* unregister listeners set when `setup` was called and avoid
* memory leaks. The same arguments than `setup` function are
* passed to `teardown`.
* @property {Function} isToolSupported - Function to automatically enable/disable
* the button based on the toolbox. If the toolbox don't support
* the button feature, this method should return false.
* @property {Function} isCurrentlyVisible - Function to automatically
* hide/show the button based on current state.
* @property {Function} isChecked - Optional function called to known if the button
* is toggled or not. The function should return true when
* the button should be displayed as toggled on.
*/
_createButtonState(options) {
let isCheckedValue =
false;
const {
id,
className,
description,
disabled,
onClick,
isInStartContainer,
setup,
teardown,
isToolSupported,
isCurrentlyVisible,
isChecked,
isToggle,
onKeyDown,
experimentalURL,
} = options;
const toolbox =
this;
const button = {
id,
className,
description,
disabled,
async onClick(event) {
if (
typeof onClick ==
"function") {
await onClick(event, toolbox);
button.emit(
"updatechecked");
}
},
onKeyDown(event) {
if (
typeof onKeyDown ==
"function") {
onKeyDown(event, toolbox);
}
},
isToolSupported,
isCurrentlyVisible,
get isChecked() {
if (
typeof isChecked ==
"function") {
return isChecked(toolbox);
}
return isCheckedValue;
},
set isChecked(value) {
// Note that if options.isChecked is given, this is ignored
isCheckedValue = value;
this.emit(
"updatechecked");
},
isToggle,
// The preference for having this button visible.
visibilityswitch: `devtools.${id}.enabled`,
// The toolbar has a container at the start and end of the toolbar for
// holding buttons. By default the buttons are placed in the end container.
isInStartContainer: !!isInStartContainer,
experimentalURL,
getContextMenu() {
if (options.getContextMenu) {
return options.getContextMenu(toolbox);
}
return null;
},
};
if (
typeof setup ==
"function") {
// Use async function as tracer's definition requires an async function to be passed
// for "toggle" event listener.
const onChange = async () => {
button.emit(
"updatechecked");
};
setup(
this, onChange);
// Save a reference to the cleanup method that will unregister the onChange
// callback. Immediately bind the function argument so that we don't have to
// also save a reference to them.
button.teardown = teardown.bind(options,
this, onChange);
}
button.isVisible =
this._commandIsVisible(button);
EventEmitter.decorate(button);
return button;
},
_splitConsoleOnKeypress(e) {
if (e.keyCode !== KeyCodes.DOM_VK_ESCAPE || !
this.isSplitConsoleEnabled()) {
return;
}
const currentPanel =
this.getCurrentPanel();
if (
typeof currentPanel.onToolboxChromeEventHandlerEscapeKeyDown ===
"function"
) {
const ac =
new this.win.AbortController();
currentPanel.onToolboxChromeEventHandlerEscapeKeyDown(ac);
if (ac.signal.aborted) {
return;
}
}
this.toggleSplitConsole();
// If the debugger is paused, don't let the ESC key stop any pending navigation.
// If the host is page, don't let the ESC stop the load of the webconsole frame.
if (
this.threadFront.state ==
"paused" ||
this.hostType === Toolbox.HostType.PAGE
) {
e.preventDefault();
}
},
/**
* Add a shortcut key that should work when a split console
* has focus to the toolbox.
*
* @param {String} key
* The electron key shortcut.
* @param {Function} handler
* The callback that should be called when the provided key shortcut is pressed.
* @param {String} whichTool
* The tool the key belongs to. The corresponding handler will only be triggered
* if this tool is active.
*/
useKeyWithSplitConsole(key, handler, whichTool) {
this.shortcuts.on(key, event => {
if (
this.currentToolId === whichTool &&
this.isSplitConsoleFocused()) {
handler();
event.preventDefault();
}
});
},
_addWindowListeners() {
this.win.addEventListener(
"unload",
this.destroy);
this.win.addEventListener(
"message",
this._onBrowserMessage,
true);
},
_removeWindowListeners() {
// The host iframe's contentDocument may already be gone.
if (
this.win) {
this.win.removeEventListener(
"unload",
this.destroy);
this.win.removeEventListener(
"message",
this._onBrowserMessage,
true);
}
},
// Called whenever the chrome send a message
_onBrowserMessage(event) {
if (event.data?.name ===
"switched-host") {
this._onSwitchedHost(event.data);
}
if (event.data?.name ===
"switched-host-to-tab") {
this._onSwitchedHostToTab(event.data.browsingContextID);
}
if (event.data?.name ===
"host-raised") {
this.emit(
"host-raised");
}
},
_saveSplitConsoleHeight() {
const height = parseInt(
this.webconsolePanel.style.height, 10);
if (!isNaN(height)) {
Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF, height);
}
},
/**
* Make sure that the console is showing up properly based on all the
* possible conditions.
* 1) If the console tab is selected, then regardless of split state
* it should take up the full height of the deck, and we should
* hide the deck and splitter.
* 2) If the console tab is not selected and it is split, then we should
* show the splitter, deck, and console.
* 3) If the console tab is not selected and it is *not* split,
* then we should hide the console and splitter, and show the deck
* at full height.
*/
_refreshConsoleDisplay() {
const deck =
this.doc.getElementById(
"toolbox-deck");
const webconsolePanel =
this.webconsolePanel;
const splitter =
this.doc.getElementById(
"toolbox-console-splitter");
const openedConsolePanel =
this.currentToolId ===
"webconsole";
if (openedConsolePanel) {
deck.collapsed =
true;
deck.removeAttribute(
"expanded");
splitter.hidden =
true;
webconsolePanel.collapsed =
false;
webconsolePanel.setAttribute(
"expanded",
"");
}
else {
deck.collapsed =
false;
deck.toggleAttribute(
"expanded", !
this.splitConsole);
splitter.hidden = !
this.splitConsole;
webconsolePanel.collapsed = !
this.splitConsole;
webconsolePanel.removeAttribute(
"expanded");
}
},
/**
* Handle any custom key events. Returns true if there was a custom key
* binding run.
* @param {string} toolId Which tool to run the command on (skip if not
* current)
*/
fireCustomKey(toolId) {
const toolDefinition = gDevTools.getToolDefinition(toolId);
if (
toolDefinition.onkey &&
(
this.currentToolId === toolId ||
(toolId ==
"webconsole" &&
this.splitConsole))
) {
toolDefinition.onkey(
this.getCurrentPanel(),
this);
}
},
/**
* Build the notification box as soon as needed.
*/
get notificationBox() {
if (!
this._notificationBox) {
let { NotificationBox, PriorityLevels } =
this.browserRequire(
"devtools/client/shared/components/NotificationBox"
);
NotificationBox =
this.React.createFactory(NotificationBox);
// Render NotificationBox and assign priority levels to it.
const box =
this.doc.getElementById(
"toolbox-notificationbox");
this._notificationBox = Object.assign(
this.ReactDOM.render(NotificationBox({ wrapping:
true }), box),
PriorityLevels
);
}
return this._notificationBox;
},
/**
* Build the options for changing hosts. Called every time
* the host changes.
*/
_buildDockOptions() {
if (!
this._descriptorFront.isLocalTab) {
this.component.setDockOptionsEnabled(
false);
this.component.setCanCloseToolbox(
false);
return;
}
this.component.setDockOptionsEnabled(
true);
this.component.setCanCloseToolbox(
this.hostType !== Toolbox.HostType.WINDOW
);
const hostTypes = [];
for (
const type in Toolbox.HostType) {
const position = Toolbox.HostType[type];
if (
position == Toolbox.HostType.BROWSERTOOLBOX ||
position == Toolbox.HostType.PAGE
) {
continue;
}
hostTypes.push({
position,
switchHost:
this.switchHost.bind(
this, position),
});
}
this.component.setCurrentHostType(
this.hostType);
this.component.setHostTypes(hostTypes);
},
postMessage(msg) {
// We sometime try to send messages in middle of destroy(), where the
// toolbox iframe may already be detached.
if (!
this._destroyer) {
// Toolbox document is still chrome and disallow identifying message
// origin via event.source as it is null. So use a custom id.
msg.frameId =
this.frameId;
this.topWindow.postMessage(msg,
"*");
}
},
/**
* This will fetch the panel definitions from the constants in definitions module
* and populate the state within the ToolboxController component.
*/
async _buildInitialPanelDefinitions() {
// Get the initial list of tab definitions. This list can be amended at a later time
// by tools registering themselves.
const definitions = gDevTools.getToolDefinitionArray();
definitions.forEach(definition =>
this._buildPanelForTool(definition));
// Get the definitions that will only affect the main tab area.
this.panelDefinitions = definitions.filter(
definition =>
definition.isToolSupported(
this) && definition.id !==
"options"
);
},
async _setInitialMeatballState() {
let disableAutohide, pseudoLocale;
// Popup auto-hide disabling is only available in browser toolbox and webextension toolboxes.
if (
this.isBrowserToolbox ||
this._descriptorFront.isWebExtensionDescriptor
) {
disableAutohide = await
this._isDisableAutohideEnabled();
}
// Pseudo locale items are only displayed in the browser toolbox
if (
this.isBrowserToolbox) {
pseudoLocale = await
this.getPseudoLocale();
}
// Parallelize the asynchronous calls, so that the DOM is only updated once when
// updating the React components.
if (
typeof disableAutohide ==
"boolean") {
this.component.setDisableAutohide(disableAutohide);
}
if (
typeof pseudoLocale ==
"string") {
this.component.setPseudoLocale(pseudoLocale);
}
if (
this._descriptorFront.isWebExtensionDescriptor &&
this.hostType === Toolbox.HostType.WINDOW
) {
const alwaysOnTop = Services.prefs.getBoolPref(
DEVTOOLS_ALWAYS_ON_TOP,
false
);
this.component.setAlwaysOnTop(alwaysOnTop);
}
},
/**
* Initiate toolbox React components and all it's properties. Do the initial render.
*
* @param {Object} fluentBundles
* A FluentBundle instance used to display any localized text in the React component.
*/
_mountReactComponent(fluentBundles) {
// Ensure the toolbar doesn't try to render until the tool is ready.
const element =
this.React.createElement(
this.AppErrorBoundary,
{
componentName:
"General",
panel: L10N.getStr(
"webDeveloperToolsMenu.label"),
},
this.React.createElement(
this.ToolboxController, {
ref: r => {
this.component = r;
},
L10N,
--> --------------------
--> maximum size reached
--> --------------------