/* 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/. */
/** * 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;
// `component` might be null if the toolbox was destroying during the throttling this._throttledSetToolboxButtons = throttle(
() => this.component?.setToolboxButtons(this.toolbarButtons),
500, this
);
/** * 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",
};
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);
}
returnthis._nodePicker;
},
get store() { if (!this._store) { this._store = createToolboxStore();
} returnthis._store;
},
get currentToolId() { returnthis._currentToolId;
},
set currentToolId(id) { this._currentToolId = id; this.component.setCurrentToolId(id);
},
get defaultToolId() { returnthis._defaultToolId;
},
get panelDefinitions() { returnthis._panelDefinitions;
},
set panelDefinitions(definitions) { this._panelDefinitions = definitions; this._combineAndSortPanelDefinitions();
},
get visibleAdditionalTools() { if (!this._visibleAdditionalTools) { this._visibleAdditionalTools = [];
}
returnthis._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() { returnnew Map(this._toolPanels);
},
/** * Access the panel for a given tool
*/
getPanel(id) { returnthis._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); returnnew 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() { returnthis._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() { returnthis.commands.targetCommand.targetFront;
},
get threadFront() { returnthis.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() { returnthis._hostType;
},
/** * Shortcut to the window containing the toolbox UI
*/
get win() { returnthis._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() { returnthis.topWindow.document;
},
/** * Shortcut to the document containing the toolbox UI
*/
get doc() { returnthis.win.document;
},
/** * Get the toggled state of the split console
*/
get splitConsole() { returnthis._splitConsole;
},
/** * Get the focused state of the split console
*/
isSplitConsoleFocused() { if (!this._splitConsole) { returnfalse;
} 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 (typeofthis._splitConsoleEnabled !== "boolean") { this.updateIsSplitConsoleEnabled();
}
returnthis._splitConsoleEnabled;
},
get isBrowserToolbox() { returnthis.hostType === Toolbox.HostType.BROWSERTOOLBOX;
},
get isMultiProcessBrowserToolbox() { returnthis.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) { returnnull;
}
/** * 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);
} elseif (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 });
} elseif (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" });
/** * 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 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);
}
},
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);
} elseif (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,
},
],
});
}
},
/** * 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;
// 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);
}
// 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);
// 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 }
);
// 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
);
}
// 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) { returnnull;
} returnthis.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();
_removeChromeEventHandlerEvents() { if (!this._chromeEventHandler) { return;
}
// Remove shortcuts and window-host-shortcuts that use the ChromeEventHandler as // target. this._removeShortcuts(); this._removeWindowHostShortcuts();
_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();
}
});
// 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();
}
/** * 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();
});
} elseif (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);
}
},
_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");
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();
}
},
/** * loading React modules when needed (to avoid performance penalties * during Firefox start up time).
*/
get React() { returnthis.browserRequire("devtools/client/shared/vendor/react");
},
get ReactDOM() { returnthis.browserRequire("devtools/client/shared/vendor/react-dom");
},
get ReactRedux() { returnthis.browserRequire("devtools/client/shared/vendor/react-redux");
},
get ToolboxController() { returnthis.browserRequire( "devtools/client/framework/components/ToolboxController"
);
},
get AppErrorBoundary() { returnthis.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) { returnthis._sourceMapLoader;
} this._sourceMapLoader = new SourceMapLoader(this.commands.targetCommand); returnthis._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) { returnthis._parserWorker;
}
this._parserWorker = new ParserDispatcher(); returnthis._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) { returnthis._sourceMapURLService;
} this._sourceMapURLService = new SourceMapURLService( this.commands, this.sourceMapLoader
); returnthis._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";
}
},
// 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);
/** * 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);
} returnnull;
},
}; 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);
const currentPanel = this.getCurrentPanel(); if ( typeof currentPanel.onToolboxChromeEventHandlerEscapeKeyDown === "function"
) { const ac = newthis.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();
}
});
},
/** * 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";
/** * 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);
/** * Build the notification box as soon as needed.
*/
get notificationBox() { if (!this._notificationBox) {
let { NotificationBox, PriorityLevels } = this.browserRequire( "devtools/client/shared/components/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
);
} returnthis._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;
}
const hostTypes = []; for (const type in Toolbox.HostType) { const position = Toolbox.HostType[type]; if (
position == Toolbox.HostType.BROWSERTOOLBOX ||
position == Toolbox.HostType.PAGE
) { continue;
}
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
--> --------------------
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.