/* 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 SOURCE_MAP_WORKER = "resource://devtools/client/shared/source-map/worker.js"; const SOURCE_MAP_WORKER_ASSETS = "resource://devtools/client/shared/source-map/assets/"; const MAX_ORDINAL = 99; const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled"; const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight"; const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide"; const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST"; const CURRENT_THEME_SCALAR = "devtools.current_theme"; const HTML_NS = "http://www.w3.org/1999/xhtml"; var { Ci, Cc } = require("chrome"); var promise = require("promise"); const { debounce } = require("devtools/shared/debounce"); var Services = require("Services"); var ChromeUtils = require("ChromeUtils"); var { gDevTools } = require("devtools/client/framework/devtools"); var EventEmitter = require("devtools/shared/event-emitter"); const Selection = require("devtools/client/framework/selection"); var Telemetry = require("devtools/client/shared/telemetry"); const { getUnicodeUrl } = require("devtools/client/shared/unicode-url"); var { DOMHelpers } = require("devtools/shared/dom-helpers"); const { KeyCodes } = require("devtools/client/shared/keycodes"); var Startup = Cc["@mozilla.org/devtools/startup-clh;1"].getService( Ci.nsISupports ).wrappedJSObject; const { TargetList } = require("devtools/shared/resources/target-list"); const { BrowserLoader } = ChromeUtils.import( "resource://devtools/client/shared/browser-loader.js" ); const { LocalizationHelper } = require("devtools/shared/l10n"); const L10N = new LocalizationHelper( "devtools/client/locales/toolbox.properties" ); loader.lazyRequireGetter( this, "createToolboxStore", "devtools/client/framework/store", true ); loader.lazyRequireGetter( this, "registerWalkerListeners", "devtools/client/framework/actions/index", true ); loader.lazyRequireGetter( this, "AppConstants", "resource://gre/modules/AppConstants.jsm", true ); loader.lazyRequireGetter(this, "flags", "devtools/shared/flags"); loader.lazyRequireGetter( this, "KeyShortcuts", "devtools/client/shared/key-shortcuts" ); loader.lazyRequireGetter(this, "ZoomKeys", "devtools/client/shared/zoom-keys"); loader.lazyRequireGetter( this, "settleAll", "devtools/shared/ThreadSafeDevToolsUtils", true ); loader.lazyRequireGetter( this, "ToolboxButtons", "devtools/client/definitions", true ); loader.lazyRequireGetter( this, "SourceMapURLService", "devtools/client/framework/source-map-url-service", true ); loader.lazyRequireGetter( this, "BrowserConsoleManager", "devtools/client/webconsole/browser-console-manager", true ); loader.lazyRequireGetter( this, "viewSource", "devtools/client/shared/view-source" ); loader.lazyRequireGetter( this, "buildHarLog", "devtools/client/netmonitor/src/har/har-builder-utils", true ); loader.lazyRequireGetter( this, "NetMonitorAPI", "devtools/client/netmonitor/src/api", true ); loader.lazyRequireGetter( this, "sortPanelDefinitions", "devtools/client/framework/toolbox-tabs-order-manager", true ); loader.lazyRequireGetter( this, "createEditContextMenu", "devtools/client/framework/toolbox-context-menu", true ); loader.lazyRequireGetter( this, "remoteClientManager", "devtools/client/shared/remote-debugging/remote-client-manager.js", true ); loader.lazyRequireGetter( this, "ResponsiveUIManager", "devtools/client/responsive/manager" ); loader.lazyRequireGetter( this, "DevToolsUtils", "devtools/shared/DevToolsUtils" ); loader.lazyRequireGetter( this, "NodePicker", "devtools/client/inspector/node-picker" ); loader.lazyGetter(this, "domNodeConstants", () => { return require("devtools/shared/dom-node-constants"); }); loader.lazyGetter(this, "DEBUG_TARGET_TYPES", () => { return require("devtools/client/shared/remote-debugging/constants") .DEBUG_TARGET_TYPES; }); loader.lazyGetter(this, "registerHarOverlay", () => { return require("devtools/client/netmonitor/src/har/toolbox-overlay").register; }); loader.lazyGetter( this, "reloadAndRecordTab", () => require("devtools/client/webreplay/menu.js").reloadAndRecordTab ); loader.lazyGetter( this, "reloadAndStopRecordingTab", () => require("devtools/client/webreplay/menu.js").reloadAndStopRecordingTab ); loader.lazyRequireGetter( this, "defaultThreadOptions", "devtools/client/shared/thread-utils", true ); loader.lazyRequireGetter( this, "NodeFront", "devtools/shared/fronts/node", true ); /** * A "Toolbox" is the component that holds all the tools for one specific * target. Visually, it's a document that includes the tools tabs and all * the iframes where the tool panels will be living in. * * @param {object} target * The object the toolbox is debugging. * @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 * @param {Number} msSinceProcessStart * the number of milliseconds since process start using monotonic * timestamps (unaffected by system clock changes). */ function Toolbox( target, selectedTool, hostType, contentWindow, frameId, msSinceProcessStart ) { this._win = contentWindow; this.frameId = frameId; this.selection = new Selection(); this.telemetry = new Telemetry(); this.targetList = new TargetList(target.client.mainRoot, target); // The session ID is used to determine which telemetry events belong to which // toolbox session. Because we use Amplitude to analyse the telemetry data we // must use the time since the system wide epoch as the session ID. this.sessionId = msSinceProcessStart; // Map of the available DevTools WebExtensions: // Map this._webExtensions = new Map(); this._toolPanels = new Map(); // Map of tool startup components for given tool id. this._toolStartups = 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; // Set of paused threads to determine whether the toolbox is paused this._pausedThreads = 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._onWillNavigate = this._onWillNavigate.bind(this); this._refreshHostTitle = this._refreshHostTitle.bind(this); this.toggleNoAutohide = this.toggleNoAutohide.bind(this); this._updateFrames = this._updateFrames.bind(this); this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this); this.closeToolbox = this.closeToolbox.bind(this); this.destroy = this.destroy.bind(this); this._applyCacheSettings = this._applyCacheSettings.bind(this); this._applyServiceWorkersTestingSettings = this._applyServiceWorkersTestingSettings.bind( this ); this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this); this._onFocus = this._onFocus.bind(this); this._onBrowserMessage = this._onBrowserMessage.bind(this); this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this); this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.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.togglePaintFlashing = this.togglePaintFlashing.bind(this); this.toggleDragging = this.toggleDragging.bind(this); this._onPausedState = this._onPausedState.bind(this); this._onResumedState = this._onResumedState.bind(this); this._onTargetAvailable = this._onTargetAvailable.bind(this); this._onTargetDestroyed = this._onTargetDestroyed.bind(this); this.isPaintFlashing = false; this._isBrowserToolbox = false; 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", CUSTOM: "custom", // 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", SIDE_ENABLED: "devtools.toolbox.sideEnabled", }, get nodePicker() { if (!this._nodePicker) { this._nodePicker = new NodePicker(this.targetList, 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: function() { return new Map(this._toolPanels); }, /** * Access the panel for a given tool */ getPanel: function(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: function(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: function() { return this._toolPanels.get(this.currentToolId); }, toggleDragging: function() { this.doc.querySelector("window").classList.toggle("dragging"); }, /** * Instruct the toolbox to switch to a new top-level target. * It means that the currently debugged target is destroyed in favor of a new one. * This typically happens when navigating to a new URL which has to be loaded * in a distinct process. */ async switchToTarget(newTarget) { // Notify gDevTools that the toolbox will be hooked to another target. this.emit("switch-target", newTarget); // TargetList.switchToTarget won't wait for all target listeners, like // Toolbox._onTargetAvailable to be finished before resolving. // But, we do expect the target to be attached before calling listFrames // and initPerformance. So wait for this via an internal event. const onAttached = this.once("top-target-attached"); await this.targetList.switchToTarget(newTarget); await onAttached; // Attach the toolbox to this new target await this._listFrames(); await this.initPerformance(); // Notify all the tools that the target has changed await Promise.all( [...this._toolPanels.values()].map(panel => { if (panel.switchToTarget) { return panel.switchToTarget(newTarget); } return Promise.resolve(); }) ); this.emit("switched-target", newTarget); }, /** * Get the current top level target the toolbox is debugging. */ get target() { return this.targetList.targetFront; }, get threadFront() { return this._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: function() { if (!this._splitConsole) { return false; } const focusedWin = Services.focus.focusedWindow; return ( focusedWin && focusedWin === this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow ); }, setBrowserToolbox: function(isBrowserToolbox) { this._isBrowserToolbox = isBrowserToolbox; }, isBrowserToolbox: function() { return this._isBrowserToolbox; }, _onPausedState: function(packet, threadFront) { // Suppress interrupted events by default because the thread is // paused/resumed a lot for various actions. if (packet.why.type === "interrupted") { return; } this.highlightTool("jsdebugger"); if ( packet.why.type === "debuggerStatement" || packet.why.type === "mutationBreakpoint" || packet.why.type === "eventBreakpoint" || packet.why.type === "breakpoint" || packet.why.type === "exception" ) { this.raise(); this.selectTool("jsdebugger", packet.why.type); this._pausedThreads.add(threadFront); this.emit("toolbox-paused"); } }, _onResumedState: function(threadFront) { this._pausedThreads.delete(threadFront); if (this._pausedThreads.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({ type, targetFront, isTopLevel }) { if (isTopLevel) { // Attach to a new top-level target. // For now, register these event listeners only on the top level target targetFront.on("will-navigate", this._onWillNavigate); targetFront.on("navigate", this._refreshHostTitle); targetFront.on("frame-update", this._updateFrames); targetFront.on("inspect-object", this._onInspectObject); targetFront.watchFronts("inspector", async inspectorFront => { registerWalkerListeners(this.store, inspectorFront.walker); }); } await this._attachTarget({ type, targetFront, isTopLevel }); if (isTopLevel) { this.emit("top-target-attached"); } }, _onTargetDestroyed({ type, targetFront, isTopLevel }) { if (isTopLevel) { this.detachTarget(); } }, /** * This method focuses on attaching to one particular target. * It ensure that the target actor is fully initialized and is watching for * resources. We do that by calling its `attach` method. * And we listen for thread actor events in order to update toolbox UI when * we hit a breakpoint. */ async _attachTarget({ type, targetFront, isTopLevel }) { await targetFront.attach(); // Start tracking network activity on toolbox open for targets such as tabs. const webConsoleFront = await targetFront.getFront("console"); await webConsoleFront.startListeners(["NetworkActivity"]); // Do not attach to the thread of additional Frame targets, as they are // already tracked by the content process targets. At least in the context // of the Browser Toolbox. // We would have to revisit that for the content toolboxes. if (isTopLevel || type != TargetList.TYPES.FRAME) { const threadFront = await this._attachAndResumeThread(targetFront); this._startThreadFrontListeners(threadFront); if (isTopLevel) { this._threadFront = threadFront; } } }, _startThreadFrontListeners: function(threadFront) { // threadFront listeners are removed when the thread is destroyed threadFront.on("paused", packet => this._onPausedState(packet, threadFront) ); threadFront.on("resumed", () => this._onResumedState(threadFront)); }, _attachAndResumeThread: async function(target) { const options = defaultThreadOptions(); const [, threadFront] = await target.attachThread(options); try { await threadFront.resume(); } catch (ex) { // Interpret a possible error thrown by ThreadActor.resume if (ex.error === "wrongOrder") { const box = this.getNotificationBox(); box.appendNotification( L10N.getStr("toolbox.resumeOrderWarning"), "wrong-resume-order", "", box.PRIORITY_WARNING_HIGH ); } else { throw ex; } } return threadFront; }, /** * Open the toolbox */ open: function() { return async function() { 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; } if (this.hostType === Toolbox.HostType.PAGE) { // Displays DebugTargetInfo which shows the basic information of debug target, // if `about:devtools-toolbox` URL opens directly. // DebugTargetInfo requires this._debugTargetData to be populated this._debugTargetData = this._getDebugTargetData(); } const domReady = new Promise(resolve => { DOMHelpers.onceDOMReady( this.win, () => { resolve(); }, this._URL ); }); await this.targetList.startListening(TargetList.ALL_TYPES); // Optimization: fire up a few other things before waiting on // the iframe being ready (makes startup faster) await this.targetList.watchTargets( TargetList.ALL_TYPES, this._onTargetAvailable, this._onTargetDestroyed ); await domReady; this.browserRequire = BrowserLoader({ window: this.win, useOnlyShared: true, }).require; // The web console is immediately loaded when replaying, so that the // timeline will always be populated with generated messages. if (this.target.isReplayEnabled()) { await this.loadTool("webconsole"); } this.isReady = true; const framesPromise = this._listFrames(); Services.prefs.addObserver( "devtools.cache.disabled", this._applyCacheSettings ); Services.prefs.addObserver( "devtools.serviceWorkers.testing.enabled", this._applyServiceWorkersTestingSettings ); // Get the DOM element to mount the ToolboxController to. this._componentMount = this.doc.getElementById("toolbox-toolbar-mount"); this._mountReactComponent(); this._buildDockOptions(); this._buildTabs(); this._applyCacheSettings(); this._applyServiceWorkersTestingSettings(); this._addWindowListeners(); this._addChromeEventHandlerEvents(); this._registerOverlays(); this._componentMount.addEventListener( "keypress", this._onToolbarArrowKeypress ); this._componentMount.setAttribute( "aria-label", L10N.getStr("toolbox.label") ); this.webconsolePanel = this.doc.querySelector( "#toolbox-panel-webconsole" ); this.webconsolePanel.height = Services.prefs.getIntPref( SPLITCONSOLE_HEIGHT_PREF ); this.webconsolePanel.addEventListener( "resize", this._saveSplitConsoleHeight ); this._buildButtons(); this._pingTelemetry(); // The isTargetSupported 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.isTargetSupported(this.target)) { this._defaultToolId = "webconsole"; } // Start rendering the toolbox toolbar before selecting the tool, as the tools // can take a few hundred milliseconds seconds to start up. // // Delay React rendering as Toolbox.open is synchronous. // Even if this involve promises, it is synchronous. Toolbox.open already loads // react modules and freeze the event loop for a significant time. // requestIdleCallback allows releasing it to allow user events to be processed. // Use 16ms maximum delay to allow one frame to be rendered at 60FPS // (1000ms/60FPS=16ms) this.win.requestIdleCallback( () => { this.component.setCanRender(); }, { timeout: 16 } ); await this.selectTool(this._defaultToolId, "initial_panel"); // Wait until the original tool is selected so that the split // console input will receive focus. let splitConsolePromise = promise.resolve(); if (Services.prefs.getBoolPref(SPLITCONSOLE_ENABLED_PREF)) { splitConsolePromise = this.openSplitConsole(); this.telemetry.addEventProperty( this.topWindow, "open", "tools", null, "splitconsole", true ); } else { this.telemetry.addEventProperty( this.topWindow, "open", "tools", null, "splitconsole", false ); } await promise.all([splitConsolePromise, framesPromise]); // 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.target.reconfigure({ options: { restoreFocus: true, }, }); } // Lazily connect to the profiler here and don't wait for it to complete, // used to intercept console.profile calls before the performance tools are open. const performanceFrontConnection = this.initPerformance(); // If in testing environment, wait for performance connection to finish, // so we don't have to explicitly wait for this in tests; ideally, all tests // will handle this on their own, but each have their own tear down function. if (flags.testing) { await performanceFrontConnection; } this.emit("ready"); this._resolveIsOpen(); } .bind(this)() .catch(e => { console.error("Exception while opening the toolbox", String(e), e); // While the exception stack is correctly printed in the Browser console when // passing `e` to console.error, it is not on the stdout, so print it via dump. dump(e.stack + "\n"); }); }, detachTarget() { this.target.off("inspect-object", this._onInspectObject); this.target.off("will-navigate", this._onWillNavigate); this.target.off("navigate", this._refreshHostTitle); this.target.off("frame-update", this._updateFrames); // Detach the thread this._threadFront = null; }, /** * 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: function() { // 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( "contextmenu", this._onContextMenu ); this._chromeEventHandler.addEventListener("mousedown", this._onMouseDown); }, _removeChromeEventHandlerEvents: function() { 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( "contextmenu", this._onContextMenu ); this._chromeEventHandler.removeEventListener( "mousedown", this._onMouseDown ); this._chromeEventHandler = null; }, _addShortcuts: function() { // 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); // Listen for Reload shortcuts [ ["reload", false], ["reload2", false], ["forceReload", true], ["forceReload2", true], ].forEach(([id, force]) => { const key = L10N.getStr("toolbox." + id + ".key"); this.shortcuts.on(key, event => { this.reloadTarget(force); // Prevent Firefox shortcuts from reloading the page event.preventDefault(); }); }); // Add zoom-related shortcuts. if (!this._hostOptions || this._hostOptions.zoom === true) { ZoomKeys.register(this.win, this.shortcuts); } // Monitor shortcuts that are not supported by DevTools, but might be used // by users because they are widely implemented in other developer tools // (example: the command palette triggered via ctrl+P) const wrongShortcuts = ["CmdOrCtrl+P", "CmdOrCtrl+Shift+P"]; for (const shortcut of wrongShortcuts) { this.shortcuts.on(shortcut, event => { this.telemetry.recordEvent("wrong_shortcut", "tools", null, { shortcut, tool_id: this.currentToolId, session_id: this.sessionId, }); }); } }, _removeShortcuts: function() { if (this.shortcuts) { this.shortcuts.destroy(); this.shortcuts = null; } }, /** * Adds the keys and commands to the Toolbox Window in window mode. */ _addWindowHostShortcuts: function() { 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 (AppConstants.platform == "macosx") { shortcuts.on( L10N.getStr("toolbox.toggleToolboxOSX.key"), this.closeToolbox ); } else { shortcuts.on(L10N.getStr("toolbox.toggleToolbox.key"), this.closeToolbox); } }, _removeWindowHostShortcuts: function() { if (this._windowHostShortcuts) { this._windowHostShortcuts.destroy(); this._windowHostShortcuts = null; } }, _onContextMenu: function(e) { // Handle context menu events in standard input elements: and