зеркало из https://github.com/mozilla/gecko-dev.git
2584 строки
83 KiB
JavaScript
2584 строки
83 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const MAX_ORDINAL = 99;
|
|
const SPLITCONSOLE_ENABLED_PREF = "devtools.toolbox.splitconsoleEnabled";
|
|
const SPLITCONSOLE_HEIGHT_PREF = "devtools.toolbox.splitconsoleHeight";
|
|
const DISABLE_AUTOHIDE_PREF = "ui.popup.disable_autohide";
|
|
const OS_HISTOGRAM = "DEVTOOLS_OS_ENUMERATED_PER_USER";
|
|
const OS_IS_64_BITS = "DEVTOOLS_OS_IS_64_BITS_PER_USER";
|
|
const HOST_HISTOGRAM = "DEVTOOLS_TOOLBOX_HOST";
|
|
const SCREENSIZE_HISTOGRAM = "DEVTOOLS_SCREEN_RESOLUTION_ENUMERATED_PER_USER";
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
var {Ci, Cu} = require("chrome");
|
|
var promise = require("promise");
|
|
var defer = require("devtools/shared/defer");
|
|
var Services = require("Services");
|
|
var {Task} = require("devtools/shared/task");
|
|
var {gDevTools} = require("devtools/client/framework/devtools");
|
|
var EventEmitter = require("devtools/shared/event-emitter");
|
|
var Telemetry = require("devtools/client/shared/telemetry");
|
|
var { attachThread, detachThread } = require("./attach-thread");
|
|
var Menu = require("devtools/client/framework/menu");
|
|
var MenuItem = require("devtools/client/framework/menu-item");
|
|
var { DOMHelpers } = require("resource://devtools/client/shared/DOMHelpers.jsm");
|
|
const { KeyCodes } = require("devtools/client/shared/keycodes");
|
|
|
|
const { BrowserLoader } =
|
|
Cu.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, "getHighlighterUtils",
|
|
"devtools/client/framework/toolbox-highlighter-utils", true);
|
|
loader.lazyRequireGetter(this, "Selection",
|
|
"devtools/client/framework/selection", true);
|
|
loader.lazyRequireGetter(this, "InspectorFront",
|
|
"devtools/shared/fronts/inspector", true);
|
|
loader.lazyRequireGetter(this, "flags",
|
|
"devtools/shared/flags");
|
|
loader.lazyRequireGetter(this, "showDoorhanger",
|
|
"devtools/client/shared/doorhanger", true);
|
|
loader.lazyRequireGetter(this, "createPerformanceFront",
|
|
"devtools/shared/fronts/performance", true);
|
|
loader.lazyRequireGetter(this, "system",
|
|
"devtools/shared/system");
|
|
loader.lazyRequireGetter(this, "getPreferenceFront",
|
|
"devtools/shared/fronts/preference", true);
|
|
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, "SourceMapService",
|
|
"devtools/client/framework/source-map-service", true);
|
|
loader.lazyRequireGetter(this, "HUDService",
|
|
"devtools/client/webconsole/hudservice");
|
|
loader.lazyRequireGetter(this, "viewSource",
|
|
"devtools/client/shared/view-source");
|
|
|
|
loader.lazyGetter(this, "registerHarOverlay", () => {
|
|
return require("devtools/client/netmonitor/har/toolbox-overlay").register;
|
|
});
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function Toolbox(target, selectedTool, hostType, contentWindow, frameId) {
|
|
this._target = target;
|
|
this._win = contentWindow;
|
|
this.frameId = frameId;
|
|
|
|
this._toolPanels = new Map();
|
|
this._telemetry = new Telemetry();
|
|
if (Services.prefs.getBoolPref("devtools.source-map.locations.enabled")) {
|
|
this._sourceMapService = new SourceMapService(this._target);
|
|
}
|
|
|
|
this._initInspector = null;
|
|
this._inspector = null;
|
|
|
|
// Map of frames (id => frame-info) and currently selected frame id.
|
|
this.frameMap = new Map();
|
|
this.selectedFrameId = 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.showFramesMenu = this.showFramesMenu.bind(this);
|
|
this._updateFrames = this._updateFrames.bind(this);
|
|
this._splitConsoleOnKeypress = this._splitConsoleOnKeypress.bind(this);
|
|
this.destroy = this.destroy.bind(this);
|
|
this.highlighterUtils = getHighlighterUtils(this);
|
|
this._highlighterReady = this._highlighterReady.bind(this);
|
|
this._highlighterHidden = this._highlighterHidden.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._showDevEditionPromo = this._showDevEditionPromo.bind(this);
|
|
this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
|
|
this._onBottomHostMinimized = this._onBottomHostMinimized.bind(this);
|
|
this._onBottomHostMaximized = this._onBottomHostMaximized.bind(this);
|
|
this._onToolSelectWhileMinimized = this._onToolSelectWhileMinimized.bind(this);
|
|
this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
|
|
this._onBottomHostWillChange = this._onBottomHostWillChange.bind(this);
|
|
this._toggleMinimizeMode = this._toggleMinimizeMode.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._onPickerStarted = this._onPickerStarted.bind(this);
|
|
this._onPickerStopped = this._onPickerStopped.bind(this);
|
|
this.selectTool = this.selectTool.bind(this);
|
|
|
|
this._target.on("close", this.destroy);
|
|
|
|
if (!selectedTool) {
|
|
selectedTool = Services.prefs.getCharPref(this._prefs.LAST_TOOL);
|
|
}
|
|
this._defaultToolId = selectedTool;
|
|
|
|
this._hostType = hostType;
|
|
|
|
this._isOpenDeferred = defer();
|
|
this.isOpen = this._isOpenDeferred.promise;
|
|
|
|
EventEmitter.decorate(this);
|
|
|
|
this._target.on("navigate", this._refreshHostTitle);
|
|
this._target.on("frame-update", this._updateFrames);
|
|
|
|
this.on("host-changed", this._refreshHostTitle);
|
|
this.on("select", this._refreshHostTitle);
|
|
|
|
this.on("ready", this._showDevEditionPromo);
|
|
|
|
gDevTools.on("tool-registered", this._toolRegistered);
|
|
gDevTools.on("tool-unregistered", this._toolUnregistered);
|
|
|
|
this.on("picker-started", this._onPickerStarted);
|
|
this.on("picker-stopped", this._onPickerStopped);
|
|
}
|
|
exports.Toolbox = Toolbox;
|
|
|
|
/**
|
|
* The toolbox can be 'hosted' either embedded in a browser window
|
|
* or in a separate window.
|
|
*/
|
|
Toolbox.HostType = {
|
|
BOTTOM: "bottom",
|
|
SIDE: "side",
|
|
WINDOW: "window",
|
|
CUSTOM: "custom"
|
|
};
|
|
|
|
Toolbox.prototype = {
|
|
_URL: "about:devtools-toolbox",
|
|
|
|
_prefs: {
|
|
LAST_TOOL: "devtools.toolbox.selectedTool",
|
|
SIDE_ENABLED: "devtools.toolbox.sideEnabled",
|
|
},
|
|
|
|
get currentToolId() {
|
|
return this._currentToolId;
|
|
},
|
|
|
|
set currentToolId(id) {
|
|
this._currentToolId = id;
|
|
this.component.setCurrentToolId(id);
|
|
},
|
|
|
|
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;
|
|
this._combineAndSortPanelDefinitions();
|
|
},
|
|
|
|
/**
|
|
* Combines the built-in panel definitions and the additional tool definitions that
|
|
* can be set by add-ons.
|
|
*/
|
|
_combineAndSortPanelDefinitions() {
|
|
const definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()];
|
|
definitions.sort(definition => {
|
|
return -1 * (definition.ordinal == undefined || definition.ordinal < 0
|
|
? MAX_ORDINAL
|
|
: definition.ordinal
|
|
);
|
|
});
|
|
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) {
|
|
let deferred = defer();
|
|
let panel = this.getPanel(id);
|
|
if (panel) {
|
|
deferred.resolve(panel);
|
|
} else {
|
|
this.on(id + "-ready", (e, initializedPanel) => {
|
|
deferred.resolve(initializedPanel);
|
|
});
|
|
}
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* 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);
|
|
},
|
|
|
|
/**
|
|
* Get/alter the target of a Toolbox so we're debugging something different.
|
|
* See Target.jsm for more details.
|
|
* TODO: Do we allow |toolbox.target = null;| ?
|
|
*/
|
|
get target() {
|
|
return this._target;
|
|
},
|
|
|
|
get threadClient() {
|
|
return this._threadClient;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
},
|
|
|
|
/**
|
|
* Shortcut to the document containing the toolbox UI
|
|
*/
|
|
get doc() {
|
|
return this.win.document;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox highlighter front. Note that it may not always have been
|
|
* initialized first. Use `initInspector()` if needed.
|
|
* Consider using highlighterUtils instead, it exposes the highlighter API in
|
|
* a useful way for the toolbox panels
|
|
*/
|
|
get highlighter() {
|
|
return this._highlighter;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox's performance front. Note that it may not always have been
|
|
* initialized first. Use `initPerformance()` if needed.
|
|
*/
|
|
get performance() {
|
|
return this._performance;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox's inspector front. Note that it may not always have been
|
|
* initialized first. Use `initInspector()` if needed.
|
|
*/
|
|
get inspector() {
|
|
return this._inspector;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox's walker front. Note that it may not always have been
|
|
* initialized first. Use `initInspector()` if needed.
|
|
*/
|
|
get walker() {
|
|
return this._walker;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox's node selection. Note that it may not always have been
|
|
* initialized first. Use `initInspector()` if needed.
|
|
*/
|
|
get selection() {
|
|
return this._selection;
|
|
},
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
let focusedWin = Services.focus.focusedWindow;
|
|
return focusedWin && focusedWin ===
|
|
this.doc.querySelector("#toolbox-panel-iframe-webconsole").contentWindow;
|
|
},
|
|
|
|
/**
|
|
* Open the toolbox
|
|
*/
|
|
open: function () {
|
|
return Task.spawn(function* () {
|
|
this.browserRequire = BrowserLoader({
|
|
window: this.doc.defaultView,
|
|
useOnlyShared: true
|
|
}).require;
|
|
|
|
if (this.win.location.href.startsWith(this._URL)) {
|
|
// Update the URL so that onceDOMReady watch for the right url.
|
|
this._URL = this.win.location.href;
|
|
}
|
|
|
|
let domReady = defer();
|
|
let domHelper = new DOMHelpers(this.win);
|
|
domHelper.onceDOMReady(() => {
|
|
domReady.resolve();
|
|
}, this._URL);
|
|
|
|
// Optimization: fire up a few other things before waiting on
|
|
// the iframe being ready (makes startup faster)
|
|
|
|
// Load the toolbox-level actor fronts and utilities now
|
|
yield this._target.makeRemote();
|
|
|
|
// Attach the thread
|
|
this._threadClient = yield attachThread(this);
|
|
yield domReady.promise;
|
|
|
|
this.isReady = true;
|
|
let framesPromise = this._listFrames();
|
|
|
|
Services.prefs.addObserver("devtools.cache.disabled", this._applyCacheSettings,
|
|
false);
|
|
Services.prefs.addObserver("devtools.serviceWorkers.testing.enabled",
|
|
this._applyServiceWorkersTestingSettings, false);
|
|
|
|
this.textBoxContextMenuPopup =
|
|
this.doc.getElementById("toolbox-textbox-context-popup");
|
|
this.textBoxContextMenuPopup.addEventListener("popupshowing",
|
|
this._updateTextBoxMenuItems, true);
|
|
|
|
this.shortcuts = new KeyShortcuts({
|
|
window: this.doc.defaultView
|
|
});
|
|
// Get the DOM element to mount the ToolboxController to.
|
|
this._componentMount = this.doc.getElementById("toolbox-toolbar-mount");
|
|
|
|
this._mountReactComponent();
|
|
this._buildDockButtons();
|
|
this._buildOptions();
|
|
this._buildTabs();
|
|
this._applyCacheSettings();
|
|
this._applyServiceWorkersTestingSettings();
|
|
this._addKeysToWindow();
|
|
this._addReloadKeys();
|
|
this._addHostListeners();
|
|
this._registerOverlays();
|
|
if (!this._hostOptions || this._hostOptions.zoom === true) {
|
|
ZoomKeys.register(this.win);
|
|
}
|
|
|
|
this._componentMount.addEventListener("keypress", this._onToolbarArrowKeypress);
|
|
|
|
this.webconsolePanel = this.doc.querySelector("#toolbox-panel-webconsole");
|
|
this.webconsolePanel.height = Services.prefs.getIntPref(SPLITCONSOLE_HEIGHT_PREF);
|
|
this.webconsolePanel.addEventListener("resize", this._saveSplitConsoleHeight);
|
|
|
|
let buttonsPromise = 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).
|
|
let 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.
|
|
this.component.setCanRender();
|
|
|
|
yield this.selectTool(this._defaultToolId);
|
|
|
|
// 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();
|
|
}
|
|
|
|
yield promise.all([
|
|
splitConsolePromise,
|
|
buttonsPromise,
|
|
framesPromise
|
|
]);
|
|
|
|
// 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.
|
|
let 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) {
|
|
yield performanceFrontConnection;
|
|
}
|
|
|
|
this.emit("ready");
|
|
this._isOpenDeferred.resolve();
|
|
}.bind(this)).then(null, console.error.bind(console));
|
|
},
|
|
|
|
/**
|
|
* 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/toolbox-controller");
|
|
},
|
|
|
|
// Return HostType id for telemetry
|
|
_getTelemetryHostId: function () {
|
|
switch (this.hostType) {
|
|
case Toolbox.HostType.BOTTOM: return 0;
|
|
case Toolbox.HostType.SIDE: return 1;
|
|
case Toolbox.HostType.WINDOW: return 2;
|
|
case Toolbox.HostType.CUSTOM: return 3;
|
|
default: return 9;
|
|
}
|
|
},
|
|
|
|
_pingTelemetry: function () {
|
|
this._telemetry.toolOpened("toolbox");
|
|
|
|
this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
|
|
this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS,
|
|
Services.appinfo.is64Bit ? 1 : 0);
|
|
this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM,
|
|
system.getScreenDimensions());
|
|
this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
|
|
},
|
|
|
|
/**
|
|
* 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 {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} isTargetSupported - Function to automatically enable/disable
|
|
* the button based on the target. If the target don't support
|
|
* the button feature, this method should return false.
|
|
* @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.
|
|
* @property {Boolean} autoToggle - If true, the checked state is going to be
|
|
* automatically toggled on click.
|
|
*/
|
|
_createButtonState: function (options) {
|
|
let isCheckedValue = false;
|
|
const { id, className, description, onClick, isInStartContainer, setup, teardown,
|
|
isTargetSupported, isChecked, autoToggle } = options;
|
|
const toolbox = this;
|
|
const button = {
|
|
id,
|
|
className,
|
|
description,
|
|
onClick(event) {
|
|
if (typeof onClick == "function") {
|
|
onClick(event, toolbox);
|
|
}
|
|
if (autoToggle) {
|
|
button.isChecked = !button.isChecked;
|
|
}
|
|
},
|
|
isTargetSupported,
|
|
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");
|
|
},
|
|
// 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
|
|
};
|
|
if (typeof setup == "function") {
|
|
let onChange = () => {
|
|
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;
|
|
},
|
|
|
|
_buildOptions: function () {
|
|
let selectOptions = (name, event) => {
|
|
// Flip back to the last used panel if we are already
|
|
// on the options panel.
|
|
if (this.currentToolId === "options" &&
|
|
gDevTools.getToolDefinition(this.lastUsedToolId)) {
|
|
this.selectTool(this.lastUsedToolId);
|
|
} else {
|
|
this.selectTool("options");
|
|
}
|
|
// Prevent the opening of bookmarks window on toolbox.options.key
|
|
event.preventDefault();
|
|
};
|
|
this.shortcuts.on(L10N.getStr("toolbox.options.key"), selectOptions);
|
|
this.shortcuts.on(L10N.getStr("toolbox.help.key"), selectOptions);
|
|
},
|
|
|
|
_splitConsoleOnKeypress: function (e) {
|
|
if (e.keyCode === KeyCodes.DOM_VK_ESCAPE) {
|
|
this.toggleSplitConsole();
|
|
// If the debugger is paused, don't let the ESC key stop any pending
|
|
// navigation.
|
|
if (this._threadClient.state == "paused") {
|
|
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: function (key, handler, whichTool) {
|
|
this.shortcuts.on(key, (name, event) => {
|
|
if (this.currentToolId === whichTool && this.isSplitConsoleFocused()) {
|
|
handler();
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
},
|
|
|
|
_addReloadKeys: function () {
|
|
[
|
|
["reload", false],
|
|
["reload2", false],
|
|
["forceReload", true],
|
|
["forceReload2", true]
|
|
].forEach(([id, force]) => {
|
|
let key = L10N.getStr("toolbox." + id + ".key");
|
|
this.shortcuts.on(key, (name, event) => {
|
|
this.reloadTarget(force);
|
|
|
|
// Prevent Firefox shortcuts from reloading the page
|
|
event.preventDefault();
|
|
});
|
|
});
|
|
},
|
|
|
|
_addHostListeners: function () {
|
|
this.shortcuts.on(L10N.getStr("toolbox.nextTool.key"),
|
|
(name, event) => {
|
|
this.selectNextTool();
|
|
event.preventDefault();
|
|
});
|
|
this.shortcuts.on(L10N.getStr("toolbox.previousTool.key"),
|
|
(name, event) => {
|
|
this.selectPreviousTool();
|
|
event.preventDefault();
|
|
});
|
|
this.shortcuts.on(L10N.getStr("toolbox.minimize.key"),
|
|
(name, event) => {
|
|
this._toggleMinimizeMode();
|
|
event.preventDefault();
|
|
});
|
|
this.shortcuts.on(L10N.getStr("toolbox.toggleHost.key"),
|
|
(name, event) => {
|
|
this.switchToPreviousHost();
|
|
event.preventDefault();
|
|
});
|
|
|
|
this.doc.addEventListener("keypress", this._splitConsoleOnKeypress);
|
|
this.doc.addEventListener("focus", this._onFocus, true);
|
|
this.win.addEventListener("unload", this.destroy);
|
|
this.win.addEventListener("message", this._onBrowserMessage, true);
|
|
},
|
|
|
|
_removeHostListeners: function () {
|
|
// The host iframe's contentDocument may already be gone.
|
|
if (this.doc) {
|
|
this.doc.removeEventListener("keypress", this._splitConsoleOnKeypress);
|
|
this.doc.removeEventListener("focus", this._onFocus, true);
|
|
this.win.removeEventListener("unload", this.destroy);
|
|
this.win.removeEventListener("message", this._onBrowserMessage, true);
|
|
}
|
|
},
|
|
|
|
// Called whenever the chrome send a message
|
|
_onBrowserMessage: function (event) {
|
|
if (!event.data) {
|
|
return;
|
|
}
|
|
switch (event.data.name) {
|
|
case "switched-host":
|
|
this._onSwitchedHost(event.data);
|
|
break;
|
|
case "host-minimized":
|
|
if (this.hostType == Toolbox.HostType.BOTTOM) {
|
|
this._onBottomHostMinimized();
|
|
}
|
|
break;
|
|
case "host-maximized":
|
|
if (this.hostType == Toolbox.HostType.BOTTOM) {
|
|
this._onBottomHostMaximized();
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
_registerOverlays: function () {
|
|
registerHarOverlay(this);
|
|
},
|
|
|
|
_saveSplitConsoleHeight: function () {
|
|
Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF,
|
|
this.webconsolePanel.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: function () {
|
|
let deck = this.doc.getElementById("toolbox-deck");
|
|
let webconsolePanel = this.webconsolePanel;
|
|
let splitter = this.doc.getElementById("toolbox-console-splitter");
|
|
let openedConsolePanel = this.currentToolId === "webconsole";
|
|
|
|
if (openedConsolePanel) {
|
|
deck.setAttribute("collapsed", "true");
|
|
splitter.setAttribute("hidden", "true");
|
|
webconsolePanel.removeAttribute("collapsed");
|
|
} else {
|
|
deck.removeAttribute("collapsed");
|
|
if (this.splitConsole) {
|
|
webconsolePanel.removeAttribute("collapsed");
|
|
splitter.removeAttribute("hidden");
|
|
} else {
|
|
webconsolePanel.setAttribute("collapsed", "true");
|
|
splitter.setAttribute("hidden", "true");
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Adds the keys and commands to the Toolbox Window in window mode.
|
|
*/
|
|
_addKeysToWindow: function () {
|
|
if (this.hostType != Toolbox.HostType.WINDOW) {
|
|
return;
|
|
}
|
|
|
|
let doc = this.win.parent.document;
|
|
|
|
for (let [id, toolDefinition] of gDevTools.getToolDefinitionMap()) {
|
|
// Prevent multiple entries for the same tool.
|
|
if (!toolDefinition.key || doc.getElementById("key_" + id)) {
|
|
continue;
|
|
}
|
|
|
|
let toolId = id;
|
|
let key = doc.createElement("key");
|
|
|
|
key.id = "key_" + toolId;
|
|
|
|
if (toolDefinition.key.startsWith("VK_")) {
|
|
key.setAttribute("keycode", toolDefinition.key);
|
|
} else {
|
|
key.setAttribute("key", toolDefinition.key);
|
|
}
|
|
|
|
key.setAttribute("modifiers", toolDefinition.modifiers);
|
|
// needed. See bug 371900
|
|
key.setAttribute("oncommand", "void(0);");
|
|
key.addEventListener("command", () => {
|
|
this.selectTool(toolId).then(() => this.fireCustomKey(toolId));
|
|
}, true);
|
|
doc.getElementById("toolbox-keyset").appendChild(key);
|
|
}
|
|
|
|
// Add key for toggling the browser console from the detached window
|
|
if (!doc.getElementById("key_browserconsole")) {
|
|
let key = doc.createElement("key");
|
|
key.id = "key_browserconsole";
|
|
|
|
key.setAttribute("key", L10N.getStr("browserConsoleCmd.commandkey"));
|
|
key.setAttribute("modifiers", "accel,shift");
|
|
// needed. See bug 371900
|
|
key.setAttribute("oncommand", "void(0)");
|
|
key.addEventListener("command", () => {
|
|
HUDService.toggleBrowserConsole();
|
|
}, true);
|
|
doc.getElementById("toolbox-keyset").appendChild(key);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* 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: function (toolId) {
|
|
let 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/notification-box");
|
|
|
|
NotificationBox = this.React.createFactory(NotificationBox);
|
|
|
|
// Render NotificationBox and assign priority levels to it.
|
|
let box = this.doc.getElementById("toolbox-notificationbox");
|
|
this._notificationBox = Object.assign(
|
|
this.ReactDOM.render(NotificationBox({}), box),
|
|
PriorityLevels);
|
|
}
|
|
return this._notificationBox;
|
|
},
|
|
|
|
/**
|
|
* Build the buttons for changing hosts. Called every time
|
|
* the host changes.
|
|
*/
|
|
_buildDockButtons: function () {
|
|
if (!this._target.isLocalTab) {
|
|
this.component.setDockButtonsEnabled(false);
|
|
return;
|
|
}
|
|
|
|
// Bottom-type host can be minimized, add a button for this.
|
|
if (this.hostType == Toolbox.HostType.BOTTOM) {
|
|
this.component.setCanMinimize(true);
|
|
|
|
// Maximize again when a tool gets selected.
|
|
this.on("before-select", this._onToolSelectWhileMinimized);
|
|
// Maximize and stop listening before the host type changes.
|
|
this.once("host-will-change", this._onBottomHostWillChange);
|
|
}
|
|
|
|
this.component.setDockButtonsEnabled(true);
|
|
this.component.setCanCloseToolbox(this.hostType !== Toolbox.HostType.WINDOW);
|
|
|
|
let sideEnabled = Services.prefs.getBoolPref(this._prefs.SIDE_ENABLED);
|
|
|
|
let hostTypes = [];
|
|
for (let type in Toolbox.HostType) {
|
|
let position = Toolbox.HostType[type];
|
|
if (position == this.hostType ||
|
|
position == Toolbox.HostType.CUSTOM ||
|
|
(!sideEnabled && position == Toolbox.HostType.SIDE)) {
|
|
continue;
|
|
}
|
|
|
|
hostTypes.push({
|
|
position,
|
|
switchHost: this.switchHost.bind(this, position)
|
|
});
|
|
}
|
|
|
|
this.component.setHostTypes(hostTypes);
|
|
},
|
|
|
|
_onBottomHostMinimized: function () {
|
|
this.component.setMinimizeState("minimized");
|
|
},
|
|
|
|
_onBottomHostMaximized: function () {
|
|
this.component.setMinimizeState("maximized");
|
|
},
|
|
|
|
_onToolSelectWhileMinimized: function () {
|
|
this.postMessage({
|
|
name: "maximize-host"
|
|
});
|
|
},
|
|
|
|
postMessage: function (msg) {
|
|
// We sometime try to send messages in middle of destroy(), where the
|
|
// toolbox iframe may already be detached and no longer have a parent.
|
|
if (this.win.parent) {
|
|
// 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.win.parent.postMessage(msg, "*");
|
|
}
|
|
},
|
|
|
|
_onBottomHostWillChange: function () {
|
|
this.postMessage({
|
|
name: "maximize-host"
|
|
});
|
|
|
|
this.off("before-select", this._onToolSelectWhileMinimized);
|
|
},
|
|
|
|
_toggleMinimizeMode: function () {
|
|
if (this.hostType !== Toolbox.HostType.BOTTOM) {
|
|
return;
|
|
}
|
|
|
|
// Calculate the height to which the host should be minimized so the
|
|
// tabbar is still visible.
|
|
let toolbarHeight = this._componentMount.getBoxQuads({box: "content"})[0].bounds
|
|
.height;
|
|
this.postMessage({
|
|
name: "toggle-minimize-mode",
|
|
toolbarHeight
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Initiate ToolboxTabs React component and all it's properties. Do the initial render.
|
|
*/
|
|
_buildTabs: function () {
|
|
// 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.isTargetSupported(this._target) && definition.id !== "options");
|
|
|
|
this.optionsDefinition = definitions.find(({id}) => id === "options");
|
|
// The options tool is treated slightly differently, and is in a different area.
|
|
this.component.setOptionsPanel(definitions.find(({id}) => id === "options"));
|
|
},
|
|
|
|
_mountReactComponent: function () {
|
|
// Ensure the toolbar doesn't try to render until the tool is ready.
|
|
const element = this.React.createElement(this.ToolboxController, {
|
|
L10N,
|
|
currentToolId: this.currentToolId,
|
|
selectTool: this.selectTool,
|
|
closeToolbox: this.destroy,
|
|
focusButton: this._onToolbarFocus,
|
|
toggleMinimizeMode: this._toggleMinimizeMode,
|
|
});
|
|
|
|
this.component = this.ReactDOM.render(element, this._componentMount);
|
|
},
|
|
|
|
/**
|
|
* Reset tabindex attributes across all focusable elements inside the toolbar.
|
|
* Only have one element with tabindex=0 at a time to make sure that tabbing
|
|
* results in navigating away from the toolbar container.
|
|
* @param {FocusEvent} event
|
|
*/
|
|
_onToolbarFocus: function (id) {
|
|
this.component.setFocusedButton(id);
|
|
},
|
|
|
|
/**
|
|
* On left/right arrow press, attempt to move the focus inside the toolbar to
|
|
* the previous/next focusable element. This is not in the React component
|
|
* as it is difficult to coordinate between different component elements.
|
|
* The components are responsible for setting the correct tabindex value
|
|
* for if they are the focused element.
|
|
* @param {KeyboardEvent} event
|
|
*/
|
|
_onToolbarArrowKeypress: function (event) {
|
|
let { key, target, ctrlKey, shiftKey, altKey, metaKey } = event;
|
|
|
|
// If any of the modifier keys are pressed do not attempt navigation as it
|
|
// might conflict with global shortcuts (Bug 1327972).
|
|
if (ctrlKey || shiftKey || altKey || metaKey) {
|
|
return;
|
|
}
|
|
|
|
let buttons = [...this._componentMount.querySelectorAll("button")];
|
|
let curIndex = buttons.indexOf(target);
|
|
|
|
if (curIndex === -1) {
|
|
console.warn(target + " is not found among Developer Tools tab bar " +
|
|
"focusable elements.");
|
|
return;
|
|
}
|
|
|
|
let newTarget;
|
|
|
|
if (key === "ArrowLeft") {
|
|
// Do nothing if already at the beginning.
|
|
if (curIndex === 0) {
|
|
return;
|
|
}
|
|
newTarget = buttons[curIndex - 1];
|
|
} else if (key === "ArrowRight") {
|
|
// Do nothing if already at the end.
|
|
if (curIndex === buttons.length - 1) {
|
|
return;
|
|
}
|
|
newTarget = buttons[curIndex + 1];
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
newTarget.focus();
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
},
|
|
|
|
/**
|
|
* Add buttons to the UI as specified in devtools/client/definitions.js
|
|
*/
|
|
_buildButtons: Task.async(function* () {
|
|
// Beyond the normal preference filtering
|
|
this.toolbarButtons = [
|
|
this._buildPickerButton(),
|
|
this._buildFrameButton(),
|
|
yield this._buildNoAutoHideButton()
|
|
];
|
|
|
|
ToolboxButtons.forEach(definition => {
|
|
let button = this._createButtonState(definition);
|
|
this.toolbarButtons.push(button);
|
|
});
|
|
|
|
this.component.setToolboxButtons(this.toolbarButtons);
|
|
}),
|
|
|
|
/**
|
|
* Button to select a frame for the inspector to target.
|
|
*/
|
|
_buildFrameButton() {
|
|
this.frameButton = this._createButtonState({
|
|
id: "command-button-frames",
|
|
description: L10N.getStr("toolbox.frames.tooltip"),
|
|
onClick: this.showFramesMenu,
|
|
isTargetSupported: target => {
|
|
return target.activeTab && target.activeTab.traits.frames;
|
|
}
|
|
});
|
|
|
|
return this.frameButton;
|
|
},
|
|
|
|
/**
|
|
* Button that disables/enables auto-hiding XUL pop-ups. When enabled, XUL
|
|
* pop-ups will not automatically close when they lose focus.
|
|
*/
|
|
_buildNoAutoHideButton: Task.async(function* () {
|
|
this.autohideButton = this._createButtonState({
|
|
id: "command-button-noautohide",
|
|
description: L10N.getStr("toolbox.noautohide.tooltip"),
|
|
onClick: this._toggleNoAutohide,
|
|
isTargetSupported: target => target.chrome
|
|
});
|
|
|
|
this._isDisableAutohideEnabled().then(enabled => {
|
|
this.autohideButton.isChecked = enabled;
|
|
});
|
|
|
|
return this.autohideButton;
|
|
}),
|
|
|
|
/**
|
|
* Toggle the picker, but also decide whether or not the highlighter should
|
|
* focus the window. This is only desirable when the toolbox is mounted to the
|
|
* window. When devtools is free floating, then the target window should not
|
|
* pop in front of the viewer when the picker is clicked.
|
|
*/
|
|
_onPickerClick: function () {
|
|
let focus = this.hostType === Toolbox.HostType.BOTTOM ||
|
|
this.hostType === Toolbox.HostType.SIDE;
|
|
this.highlighterUtils.togglePicker(focus);
|
|
},
|
|
|
|
/**
|
|
* If the picker is activated, then allow the Escape key to deactivate the
|
|
* functionality instead of the default behavior of toggling the console.
|
|
*/
|
|
_onPickerKeypress: function (event) {
|
|
if (event.keyCode === KeyCodes.DOM_VK_ESCAPE) {
|
|
this.highlighterUtils.cancelPicker();
|
|
// Stop the console from toggling.
|
|
event.stopImmediatePropagation();
|
|
}
|
|
},
|
|
|
|
_onPickerStarted: function () {
|
|
this.doc.addEventListener("keypress", this._onPickerKeypress, true);
|
|
},
|
|
|
|
_onPickerStopped: function () {
|
|
this.doc.removeEventListener("keypress", this._onPickerKeypress, true);
|
|
},
|
|
|
|
/**
|
|
* The element picker button enables the ability to select a DOM node by clicking
|
|
* it on the page.
|
|
*/
|
|
_buildPickerButton() {
|
|
this.pickerButton = this._createButtonState({
|
|
id: "command-button-pick",
|
|
description: L10N.getStr("pickButton.tooltip"),
|
|
onClick: this._onPickerClick,
|
|
isInStartContainer: true,
|
|
isTargetSupported: target => {
|
|
return target.activeTab && target.activeTab.traits.frames;
|
|
}
|
|
});
|
|
|
|
return this.pickerButton;
|
|
},
|
|
|
|
/**
|
|
* Apply the current cache setting from devtools.cache.disabled to this
|
|
* toolbox's tab.
|
|
*/
|
|
_applyCacheSettings: function () {
|
|
let pref = "devtools.cache.disabled";
|
|
let cacheDisabled = Services.prefs.getBoolPref(pref);
|
|
|
|
if (this.target.activeTab) {
|
|
this.target.activeTab.reconfigure({"cacheDisabled": cacheDisabled});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Apply the current service workers testing setting from
|
|
* devtools.serviceWorkers.testing.enabled to this toolbox's tab.
|
|
*/
|
|
_applyServiceWorkersTestingSettings: function () {
|
|
let pref = "devtools.serviceWorkers.testing.enabled";
|
|
let serviceWorkersTestingEnabled =
|
|
Services.prefs.getBoolPref(pref) || false;
|
|
|
|
if (this.target.activeTab) {
|
|
this.target.activeTab.reconfigure({
|
|
"serviceWorkersTestingEnabled": serviceWorkersTestingEnabled
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Return all toolbox buttons (command buttons, plus any others that were
|
|
* added manually).
|
|
|
|
/**
|
|
* Update the visibility of the buttons.
|
|
*/
|
|
updateToolboxButtonsVisibility() {
|
|
this.toolbarButtons.forEach(button => {
|
|
button.isVisible = this._commandIsVisible(button);
|
|
});
|
|
this.component.setToolboxButtons(this.toolbarButtons);
|
|
},
|
|
|
|
/**
|
|
* Ensure the visibility of each toolbox button matches the preference value.
|
|
*/
|
|
_commandIsVisible: function (button) {
|
|
const {
|
|
isTargetSupported,
|
|
visibilityswitch
|
|
} = button;
|
|
|
|
let visible = true;
|
|
try {
|
|
visible = Services.prefs.getBoolPref(visibilityswitch);
|
|
} catch (ex) {
|
|
// Do nothing.
|
|
}
|
|
|
|
if (isTargetSupported) {
|
|
return visible && isTargetSupported(this.target);
|
|
}
|
|
|
|
return visible;
|
|
},
|
|
|
|
/**
|
|
* Build a panel for a tool definition.
|
|
*
|
|
* @param {string} toolDefinition
|
|
* Tool definition of the tool to build a tab for.
|
|
*/
|
|
_buildPanelForTool: function (toolDefinition) {
|
|
if (!toolDefinition.isTargetSupported(this._target)) {
|
|
return;
|
|
}
|
|
|
|
let deck = this.doc.getElementById("toolbox-deck");
|
|
let id = toolDefinition.id;
|
|
|
|
if (toolDefinition.ordinal == undefined || toolDefinition.ordinal < 0) {
|
|
toolDefinition.ordinal = MAX_ORDINAL;
|
|
}
|
|
|
|
if (!toolDefinition.bgTheme) {
|
|
toolDefinition.bgTheme = "theme-toolbar";
|
|
}
|
|
let panel = this.doc.createElement("vbox");
|
|
panel.className = "toolbox-panel " + toolDefinition.bgTheme;
|
|
|
|
// There is already a container for the webconsole frame.
|
|
if (!this.doc.getElementById("toolbox-panel-" + id)) {
|
|
panel.id = "toolbox-panel-" + id;
|
|
}
|
|
|
|
deck.appendChild(panel);
|
|
|
|
this._addKeysToWindow();
|
|
},
|
|
|
|
/**
|
|
* Lazily created map of the additional tools registered to this toolbox.
|
|
*
|
|
* @returns {Map<string, object>}
|
|
* a map of the tools definitions registered to this
|
|
* particular toolbox (the key is the toolId string, the value
|
|
* is the tool definition plain javascript object).
|
|
*/
|
|
get additionalToolDefinitions() {
|
|
if (!this._additionalToolDefinitions) {
|
|
this._additionalToolDefinitions = new Map();
|
|
}
|
|
|
|
return this._additionalToolDefinitions;
|
|
},
|
|
|
|
/**
|
|
* Retrieve the array of the additional tools registered to this toolbox.
|
|
*
|
|
* @return {Array<object>}
|
|
* the array of additional tool definitions registered on this toolbox.
|
|
*/
|
|
getAdditionalTools() {
|
|
if (this._additionalToolDefinitions) {
|
|
return Array.from(this.additionalToolDefinitions.values());
|
|
}
|
|
return [];
|
|
},
|
|
|
|
/**
|
|
* Get the additional tools that have been registered and are visible.
|
|
*
|
|
* @return {Array<object>}
|
|
* the array of additional tool definitions registered on this toolbox.
|
|
*/
|
|
getVisibleAdditionalTools() {
|
|
return this.visibleAdditionalTools
|
|
.map(toolId => this.additionalToolDefinitions.get(toolId));
|
|
},
|
|
|
|
/**
|
|
* Test the existence of a additional tools registered to this toolbox by tool id.
|
|
*
|
|
* @param {string} toolId
|
|
* the id of the tool to test for existence.
|
|
*
|
|
* @return {boolean}
|
|
*
|
|
*/
|
|
hasAdditionalTool(toolId) {
|
|
return this.additionalToolDefinitions.has(toolId);
|
|
},
|
|
|
|
/**
|
|
* Register and load an additional tool on this particular toolbox.
|
|
*
|
|
* @param {object} definition
|
|
* the additional tool definition to register and add to this toolbox.
|
|
*/
|
|
addAdditionalTool(definition) {
|
|
if (!definition.id) {
|
|
throw new Error("Tool definition id is missing");
|
|
}
|
|
|
|
if (this.isToolRegistered(definition.id)) {
|
|
throw new Error("Tool definition already registered: " +
|
|
definition.id);
|
|
}
|
|
this.additionalToolDefinitions.set(definition.id, definition);
|
|
this.visibleAdditionalTools = [...this.visibleAdditionalTools, definition.id];
|
|
|
|
this._combineAndSortPanelDefinitions();
|
|
this._buildPanelForTool(definition);
|
|
},
|
|
|
|
/**
|
|
* Unregister and unload an additional tool from this particular toolbox.
|
|
*
|
|
* @param {string} toolId
|
|
* the id of the additional tool to unregister and remove.
|
|
*/
|
|
removeAdditionalTool(toolId) {
|
|
if (!this.hasAdditionalTool(toolId)) {
|
|
throw new Error("Tool definition not registered to this toolbox: " +
|
|
toolId);
|
|
}
|
|
|
|
this.additionalToolDefinitions.delete(toolId);
|
|
this.visibleAdditionalTools = this.visibleAdditionalTools
|
|
.filter(id => id !== toolId);
|
|
this.unloadTool(toolId);
|
|
},
|
|
|
|
/**
|
|
* Ensure the tool with the given id is loaded.
|
|
*
|
|
* @param {string} id
|
|
* The id of the tool to load.
|
|
*/
|
|
loadTool: function (id) {
|
|
if (id === "inspector" && !this._inspector) {
|
|
return this.initInspector().then(() => {
|
|
return this.loadTool(id);
|
|
});
|
|
}
|
|
|
|
let deferred = defer();
|
|
let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
|
|
|
|
if (iframe) {
|
|
let panel = this._toolPanels.get(id);
|
|
if (panel) {
|
|
deferred.resolve(panel);
|
|
} else {
|
|
this.once(id + "-ready", initializedPanel => {
|
|
deferred.resolve(initializedPanel);
|
|
});
|
|
}
|
|
return deferred.promise;
|
|
}
|
|
|
|
// Retrieve the tool definition (from the global or the per-toolbox tool maps)
|
|
let definition = this.getToolDefinition(id);
|
|
|
|
if (!definition) {
|
|
deferred.reject(new Error("no such tool id " + id));
|
|
return deferred.promise;
|
|
}
|
|
|
|
iframe = this.doc.createElement("iframe");
|
|
iframe.className = "toolbox-panel-iframe";
|
|
iframe.id = "toolbox-panel-iframe-" + id;
|
|
iframe.setAttribute("flex", 1);
|
|
iframe.setAttribute("forceOwnRefreshDriver", "");
|
|
iframe.tooltip = "aHTMLTooltip";
|
|
iframe.style.visibility = "hidden";
|
|
|
|
gDevTools.emit(id + "-init", this, iframe);
|
|
this.emit(id + "-init", iframe);
|
|
|
|
// If no parent yet, append the frame into default location.
|
|
if (!iframe.parentNode) {
|
|
let vbox = this.doc.getElementById("toolbox-panel-" + id);
|
|
vbox.appendChild(iframe);
|
|
vbox.visibility = "visible";
|
|
}
|
|
|
|
let onLoad = () => {
|
|
// Prevent flicker while loading by waiting to make visible until now.
|
|
iframe.style.visibility = "visible";
|
|
|
|
// Try to set the dir attribute as early as possible.
|
|
this.setIframeDocumentDir(iframe);
|
|
|
|
// The build method should return a panel instance, so events can
|
|
// be fired with the panel as an argument. However, in order to keep
|
|
// backward compatibility with existing extensions do a check
|
|
// for a promise return value.
|
|
let built = definition.build(iframe.contentWindow, this);
|
|
|
|
if (!(typeof built.then == "function")) {
|
|
let panel = built;
|
|
iframe.panel = panel;
|
|
|
|
// The panel instance is expected to fire (and listen to) various
|
|
// framework events, so make sure it's properly decorated with
|
|
// appropriate API (on, off, once, emit).
|
|
// In this case we decorate panel instances directly returned by
|
|
// the tool definition 'build' method.
|
|
if (typeof panel.emit == "undefined") {
|
|
EventEmitter.decorate(panel);
|
|
}
|
|
|
|
gDevTools.emit(id + "-build", this, panel);
|
|
this.emit(id + "-build", panel);
|
|
|
|
// The panel can implement an 'open' method for asynchronous
|
|
// initialization sequence.
|
|
if (typeof panel.open == "function") {
|
|
built = panel.open();
|
|
} else {
|
|
let buildDeferred = defer();
|
|
buildDeferred.resolve(panel);
|
|
built = buildDeferred.promise;
|
|
}
|
|
}
|
|
|
|
// Wait till the panel is fully ready and fire 'ready' events.
|
|
promise.resolve(built).then((panel) => {
|
|
this._toolPanels.set(id, panel);
|
|
|
|
// Make sure to decorate panel object with event API also in case
|
|
// where the tool definition 'build' method returns only a promise
|
|
// and the actual panel instance is available as soon as the
|
|
// promise is resolved.
|
|
if (typeof panel.emit == "undefined") {
|
|
EventEmitter.decorate(panel);
|
|
}
|
|
|
|
gDevTools.emit(id + "-ready", this, panel);
|
|
this.emit(id + "-ready", panel);
|
|
|
|
deferred.resolve(panel);
|
|
}, console.error);
|
|
};
|
|
|
|
iframe.setAttribute("src", definition.url);
|
|
if (definition.panelLabel) {
|
|
iframe.setAttribute("aria-label", definition.panelLabel);
|
|
}
|
|
|
|
// Depending on the host, iframe.contentWindow is not always
|
|
// defined at this moment. If it is not defined, we use an
|
|
// event listener on the iframe DOM node. If it's defined,
|
|
// we use the chromeEventHandler. We can't use a listener
|
|
// on the DOM node every time because this won't work
|
|
// if the (xul chrome) iframe is loaded in a content docshell.
|
|
if (iframe.contentWindow) {
|
|
let domHelper = new DOMHelpers(iframe.contentWindow);
|
|
domHelper.onceDOMReady(onLoad);
|
|
} else {
|
|
let callback = () => {
|
|
iframe.removeEventListener("DOMContentLoaded", callback);
|
|
onLoad();
|
|
};
|
|
|
|
iframe.addEventListener("DOMContentLoaded", callback);
|
|
}
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
/**
|
|
* Set the dir attribute on the content document element of the provided iframe.
|
|
*
|
|
* @param {IFrameElement} iframe
|
|
*/
|
|
setIframeDocumentDir: function (iframe) {
|
|
let docEl = iframe.contentWindow && iframe.contentWindow.document.documentElement;
|
|
if (!docEl || docEl.namespaceURI !== HTML_NS) {
|
|
// Bail out if the content window or document is not ready or if the document is not
|
|
// HTML.
|
|
return;
|
|
}
|
|
|
|
if (docEl.hasAttribute("dir")) {
|
|
// Set the dir attribute value only if dir is already present on the document.
|
|
let top = this.win.top;
|
|
let topDocEl = top.document.documentElement;
|
|
let isRtl = top.getComputedStyle(topDocEl).direction === "rtl";
|
|
docEl.setAttribute("dir", isRtl ? "rtl" : "ltr");
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Mark all in collection as unselected; and id as selected
|
|
* @param {string} collection
|
|
* DOM collection of items
|
|
* @param {string} id
|
|
* The Id of the item within the collection to select
|
|
*/
|
|
selectSingleNode: function (collection, id) {
|
|
[...collection].forEach(node => {
|
|
if (node.id === id) {
|
|
node.setAttribute("selected", "true");
|
|
node.setAttribute("aria-selected", "true");
|
|
} else {
|
|
node.removeAttribute("selected");
|
|
node.removeAttribute("aria-selected");
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Switch to the tool with the given id
|
|
*
|
|
* @param {string} id
|
|
* The id of the tool to switch to
|
|
*/
|
|
selectTool: function (id) {
|
|
this.emit("before-select", id);
|
|
|
|
if (this.currentToolId == id) {
|
|
let panel = this._toolPanels.get(id);
|
|
if (panel) {
|
|
// We have a panel instance, so the tool is already fully loaded.
|
|
|
|
// re-focus tool to get key events again
|
|
this.focusTool(id);
|
|
|
|
// Return the existing panel in order to have a consistent return value.
|
|
return promise.resolve(panel);
|
|
}
|
|
// Otherwise, if there is no panel instance, it is still loading,
|
|
// so we are racing another call to selectTool with the same id.
|
|
return this.once("select").then(() => promise.resolve(this._toolPanels.get(id)));
|
|
}
|
|
|
|
if (!this.isReady) {
|
|
throw new Error("Can't select tool, wait for toolbox 'ready' event");
|
|
}
|
|
|
|
// Check if the tool exists.
|
|
if (this.panelDefinitions.find((definition) => definition.id === id) ||
|
|
id === "options" ||
|
|
this.additionalToolDefinitions.get(id)) {
|
|
if (this.currentToolId) {
|
|
this._telemetry.toolClosed(this.currentToolId);
|
|
}
|
|
this._telemetry.toolOpened(id);
|
|
} else {
|
|
throw new Error("No tool found");
|
|
}
|
|
|
|
// and select the right iframe
|
|
let toolboxPanels = this.doc.querySelectorAll(".toolbox-panel");
|
|
this.selectSingleNode(toolboxPanels, "toolbox-panel-" + id);
|
|
|
|
this.lastUsedToolId = this.currentToolId;
|
|
this.currentToolId = id;
|
|
this._refreshConsoleDisplay();
|
|
if (id != "options") {
|
|
Services.prefs.setCharPref(this._prefs.LAST_TOOL, id);
|
|
}
|
|
|
|
return this.loadTool(id).then(panel => {
|
|
// focus the tool's frame to start receiving key events
|
|
this.focusTool(id);
|
|
|
|
this.emit("select", id);
|
|
this.emit(id + "-selected", panel);
|
|
return panel;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Focus a tool's panel by id
|
|
* @param {string} id
|
|
* The id of tool to focus
|
|
*/
|
|
focusTool: function (id, state = true) {
|
|
let iframe = this.doc.getElementById("toolbox-panel-iframe-" + id);
|
|
|
|
if (state) {
|
|
iframe.focus();
|
|
} else {
|
|
iframe.blur();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Focus split console's input line
|
|
*/
|
|
focusConsoleInput: function () {
|
|
let consolePanel = this.getPanel("webconsole");
|
|
if (consolePanel) {
|
|
consolePanel.focusInput();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* If the console is split and we are focusing an element outside
|
|
* of the console, then store the newly focused element, so that
|
|
* it can be restored once the split console closes.
|
|
*/
|
|
_onFocus: function ({originalTarget}) {
|
|
// Ignore any non element nodes, or any elements contained
|
|
// within the webconsole frame.
|
|
let webconsoleURL = gDevTools.getToolDefinition("webconsole").url;
|
|
if (originalTarget.nodeType !== 1 ||
|
|
originalTarget.baseURI === webconsoleURL) {
|
|
return;
|
|
}
|
|
|
|
this._lastFocusedElement = originalTarget;
|
|
},
|
|
|
|
/**
|
|
* Opens the split console.
|
|
*
|
|
* @returns {Promise} a promise that resolves once the tool has been
|
|
* loaded and focused.
|
|
*/
|
|
openSplitConsole: function () {
|
|
this._splitConsole = true;
|
|
Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, true);
|
|
this._refreshConsoleDisplay();
|
|
|
|
return this.loadTool("webconsole").then(() => {
|
|
this.emit("split-console");
|
|
this.focusConsoleInput();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Closes the split console.
|
|
*
|
|
* @returns {Promise} a promise that resolves once the tool has been
|
|
* closed.
|
|
*/
|
|
closeSplitConsole: function () {
|
|
this._splitConsole = false;
|
|
Services.prefs.setBoolPref(SPLITCONSOLE_ENABLED_PREF, false);
|
|
this._refreshConsoleDisplay();
|
|
this.emit("split-console");
|
|
|
|
if (this._lastFocusedElement) {
|
|
this._lastFocusedElement.focus();
|
|
}
|
|
return promise.resolve();
|
|
},
|
|
|
|
/**
|
|
* Toggles the split state of the webconsole. If the webconsole panel
|
|
* is already selected then this command is ignored.
|
|
*
|
|
* @returns {Promise} a promise that resolves once the tool has been
|
|
* opened or closed.
|
|
*/
|
|
toggleSplitConsole: function () {
|
|
if (this.currentToolId !== "webconsole") {
|
|
return this.splitConsole ?
|
|
this.closeSplitConsole() :
|
|
this.openSplitConsole();
|
|
}
|
|
|
|
return promise.resolve();
|
|
},
|
|
|
|
/**
|
|
* Tells the target tab to reload.
|
|
*/
|
|
reloadTarget: function (force) {
|
|
this.target.activeTab.reload({ force: force });
|
|
},
|
|
|
|
/**
|
|
* Loads the tool next to the currently selected tool.
|
|
*/
|
|
selectNextTool: function () {
|
|
const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
|
|
let definition = this.panelDefinitions[index + 1];
|
|
if (!definition) {
|
|
definition = index === -1
|
|
? this.panelDefinitions[0]
|
|
: this.optionsDefinition;
|
|
}
|
|
return this.selectTool(definition.id);
|
|
},
|
|
|
|
/**
|
|
* Loads the tool just left to the currently selected tool.
|
|
*/
|
|
selectPreviousTool: function () {
|
|
const index = this.panelDefinitions.findIndex(({id}) => id === this.currentToolId);
|
|
let definition = this.panelDefinitions[index - 1];
|
|
if (!definition) {
|
|
definition = index === -1
|
|
? this.panelDefinitions[this.panelDefinitions.length - 1]
|
|
: this.optionsDefinition;
|
|
}
|
|
return this.selectTool(definition.id);
|
|
},
|
|
|
|
/**
|
|
* Highlights the tool's tab if it is not the currently selected tool.
|
|
*
|
|
* @param {string} id
|
|
* The id of the tool to highlight
|
|
*/
|
|
highlightTool: Task.async(function* (id) {
|
|
if (!this.component) {
|
|
yield this.isOpen;
|
|
}
|
|
this.component.highlightTool(id);
|
|
}),
|
|
|
|
/**
|
|
* De-highlights the tool's tab.
|
|
*
|
|
* @param {string} id
|
|
* The id of the tool to unhighlight
|
|
*/
|
|
unhighlightTool: Task.async(function* (id) {
|
|
if (!this.component) {
|
|
yield this.isOpen;
|
|
}
|
|
this.component.unhighlightTool(id);
|
|
}),
|
|
|
|
/**
|
|
* Raise the toolbox host.
|
|
*/
|
|
raise: function () {
|
|
this.postMessage({
|
|
name: "raise-host"
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Refresh the host's title.
|
|
*/
|
|
_refreshHostTitle: function () {
|
|
let title;
|
|
if (this.target.name && this.target.name != this.target.url) {
|
|
title = L10N.getFormatStr("toolbox.titleTemplate2", this.target.name,
|
|
this.target.url);
|
|
} else {
|
|
title = L10N.getFormatStr("toolbox.titleTemplate1", this.target.url);
|
|
}
|
|
this.postMessage({
|
|
name: "set-host-title",
|
|
title
|
|
});
|
|
},
|
|
|
|
// Returns an instance of the preference actor
|
|
get _preferenceFront() {
|
|
return this.isOpen.then(() => {
|
|
return this.target.root.then(rootForm => {
|
|
return getPreferenceFront(this.target.client, rootForm);
|
|
});
|
|
});
|
|
},
|
|
|
|
_toggleNoAutohide: Task.async(function* () {
|
|
let front = yield this._preferenceFront;
|
|
let toggledValue = !(yield this._isDisableAutohideEnabled(front));
|
|
|
|
front.setBoolPref(DISABLE_AUTOHIDE_PREF, toggledValue);
|
|
|
|
this.autohideButton.isChecked = toggledValue;
|
|
}),
|
|
|
|
_isDisableAutohideEnabled: Task.async(function* (prefFront) {
|
|
// Ensure that the tools are open, and the button is visible.
|
|
yield this.isOpen;
|
|
if (!this.autohideButton.isVisible) {
|
|
return false;
|
|
}
|
|
|
|
// If no prefFront was provided, then get one.
|
|
if (!prefFront) {
|
|
prefFront = yield this._preferenceFront;
|
|
}
|
|
|
|
return yield prefFront.getBoolPref(DISABLE_AUTOHIDE_PREF);
|
|
}),
|
|
|
|
_listFrames: function (event) {
|
|
if (!this._target.activeTab || !this._target.activeTab.traits.frames) {
|
|
// We are not targetting a regular TabActor
|
|
// it can be either an addon or browser toolbox actor
|
|
return promise.resolve();
|
|
}
|
|
let packet = {
|
|
to: this._target.form.actor,
|
|
type: "listFrames"
|
|
};
|
|
return this._target.client.request(packet, resp => {
|
|
this._updateFrames(null, { frames: resp.frames });
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show a drop down menu that allows the user to switch frames.
|
|
*/
|
|
showFramesMenu: function (event) {
|
|
let menu = new Menu();
|
|
let target = event.target;
|
|
|
|
// Generate list of menu items from the list of frames.
|
|
this.frameMap.forEach(frame => {
|
|
// A frame is checked if it's the selected one.
|
|
let checked = frame.id == this.selectedFrameId;
|
|
|
|
// Create menu item.
|
|
menu.append(new MenuItem({
|
|
label: frame.url,
|
|
type: "radio",
|
|
checked,
|
|
click: () => {
|
|
this.onSelectFrame(frame.id);
|
|
}
|
|
}));
|
|
});
|
|
|
|
menu.once("open").then(() => {
|
|
this.frameButton.isChecked = true;
|
|
});
|
|
|
|
menu.once("close").then(() => {
|
|
this.frameButton.isChecked = false;
|
|
});
|
|
|
|
// Show a drop down menu with frames.
|
|
// XXX Missing menu API for specifying target (anchor)
|
|
// and relative position to it. See also:
|
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
|
|
let rect = target.getBoundingClientRect();
|
|
let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
|
|
let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
|
|
menu.popup(rect.left + screenX, rect.bottom + screenY, this);
|
|
|
|
return menu;
|
|
},
|
|
|
|
/**
|
|
* Select a frame by sending 'switchToFrame' packet to the backend.
|
|
*/
|
|
onSelectFrame: function (frameId) {
|
|
// Send packet to the backend to select specified frame and
|
|
// wait for 'frameUpdate' event packet to update the UI.
|
|
let packet = {
|
|
to: this._target.form.actor,
|
|
type: "switchToFrame",
|
|
windowId: frameId
|
|
};
|
|
this._target.client.request(packet);
|
|
},
|
|
|
|
/**
|
|
* A handler for 'frameUpdate' packets received from the backend.
|
|
* Following properties might be set on the packet:
|
|
*
|
|
* destroyAll {Boolean}: All frames have been destroyed.
|
|
* selected {Number}: A frame has been selected
|
|
* frames {Array}: list of frames. Every frame can have:
|
|
* id {Number}: frame ID
|
|
* url {String}: frame URL
|
|
* title {String}: frame title
|
|
* destroy {Boolean}: Set to true if destroyed
|
|
* parentID {Number}: ID of the parent frame (not set
|
|
* for top level window)
|
|
*/
|
|
_updateFrames: function (event, data) {
|
|
if (!Services.prefs.getBoolPref("devtools.command-button-frames.enabled")) {
|
|
return;
|
|
}
|
|
|
|
// We may receive this event before the toolbox is ready.
|
|
if (!this.isReady) {
|
|
return;
|
|
}
|
|
|
|
// Store (synchronize) data about all existing frames on the backend
|
|
if (data.destroyAll) {
|
|
this.frameMap.clear();
|
|
this.selectedFrameId = null;
|
|
} else if (data.selected) {
|
|
this.selectedFrameId = data.selected;
|
|
} else if (data.frames) {
|
|
data.frames.forEach(frame => {
|
|
if (frame.destroy) {
|
|
this.frameMap.delete(frame.id);
|
|
|
|
// Reset the currently selected frame if it's destroyed.
|
|
if (this.selectedFrameId == frame.id) {
|
|
this.selectedFrameId = null;
|
|
}
|
|
} else {
|
|
this.frameMap.set(frame.id, frame);
|
|
}
|
|
});
|
|
}
|
|
|
|
// If there is no selected frame select the first top level
|
|
// frame by default. Note that there might be more top level
|
|
// frames in case of the BrowserToolbox.
|
|
if (!this.selectedFrameId) {
|
|
let frames = [...this.frameMap.values()];
|
|
let topFrames = frames.filter(frame => !frame.parentID);
|
|
this.selectedFrameId = topFrames.length ? topFrames[0].id : null;
|
|
}
|
|
|
|
// Check out whether top frame is currently selected.
|
|
// Note that only child frame has parentID.
|
|
let frame = this.frameMap.get(this.selectedFrameId);
|
|
let topFrameSelected = frame ? !frame.parentID : false;
|
|
this._framesButtonChecked = false;
|
|
|
|
// If non-top level frame is selected the toolbar button is
|
|
// marked as 'checked' indicating that a child frame is active.
|
|
if (!topFrameSelected && this.selectedFrameId) {
|
|
this._framesButtonChecked = false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Switch to the last used host for the toolbox UI.
|
|
*/
|
|
switchToPreviousHost: function () {
|
|
return this.switchHost("previous");
|
|
},
|
|
|
|
/**
|
|
* Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
|
|
* and focus the window when done.
|
|
*
|
|
* @param {string} hostType
|
|
* The host type of the new host object
|
|
*/
|
|
switchHost: function (hostType) {
|
|
if (hostType == this.hostType || !this._target.isLocalTab) {
|
|
return null;
|
|
}
|
|
|
|
this.emit("host-will-change", hostType);
|
|
|
|
// ToolboxHostManager is going to call swapFrameLoaders which mess up with
|
|
// focus. We have to blur before calling it in order to be able to restore
|
|
// the focus after, in _onSwitchedHost.
|
|
this.focusTool(this.currentToolId, false);
|
|
|
|
// Host code on the chrome side will send back a message once the host
|
|
// switched
|
|
this.postMessage({
|
|
name: "switch-host",
|
|
hostType
|
|
});
|
|
|
|
return this.once("host-changed");
|
|
},
|
|
|
|
_onSwitchedHost: function ({ hostType }) {
|
|
this._hostType = hostType;
|
|
|
|
this._buildDockButtons();
|
|
this._addKeysToWindow();
|
|
|
|
// We blurred the tools at start of switchHost, but also when clicking on
|
|
// host switching button. We now have to restore the focus.
|
|
this.focusTool(this.currentToolId, true);
|
|
|
|
this.emit("host-changed");
|
|
this._telemetry.log(HOST_HISTOGRAM, this._getTelemetryHostId());
|
|
},
|
|
|
|
/**
|
|
* Test the availability of a tool (both globally registered tools and
|
|
* additional tools registered to this toolbox) by tool id.
|
|
*
|
|
* @param {string} toolId
|
|
* Id of the tool definition to search in the per-toolbox or globally
|
|
* registered tools.
|
|
*
|
|
* @returns {bool}
|
|
* Returns true if the tool is registered globally or on this toolbox.
|
|
*/
|
|
isToolRegistered: function (toolId) {
|
|
return !!this.getToolDefinition(toolId);
|
|
},
|
|
|
|
/**
|
|
* Return the tool definition registered globally or additional tools registered
|
|
* to this toolbox.
|
|
*
|
|
* @param {string} toolId
|
|
* Id of the tool definition to retrieve for the per-toolbox and globally
|
|
* registered tools.
|
|
*
|
|
* @returns {object}
|
|
* The plain javascript object that represents the requested tool definition.
|
|
*/
|
|
getToolDefinition: function (toolId) {
|
|
return gDevTools.getToolDefinition(toolId) ||
|
|
this.additionalToolDefinitions.get(toolId);
|
|
},
|
|
|
|
/**
|
|
* Internal helper that removes a loaded tool from the toolbox,
|
|
* it removes a loaded tool panel and tab from the toolbox without removing
|
|
* its definition, so that it can still be listed in options and re-added later.
|
|
*
|
|
* @param {string} toolId
|
|
* Id of the tool to be removed.
|
|
*/
|
|
unloadTool: function (toolId) {
|
|
if (typeof toolId != "string") {
|
|
throw new Error("Unexpected non-string toolId received.");
|
|
}
|
|
|
|
if (this._toolPanels.has(toolId)) {
|
|
let instance = this._toolPanels.get(toolId);
|
|
instance.destroy();
|
|
this._toolPanels.delete(toolId);
|
|
}
|
|
|
|
let panel = this.doc.getElementById("toolbox-panel-" + toolId);
|
|
|
|
// Select another tool.
|
|
if (this.currentToolId == toolId) {
|
|
let index = this.panelDefinitions.findIndex(({id}) => id === toolId);
|
|
let nextTool = this.panelDefinitions[index + 1];
|
|
let previousTool = this.panelDefinitions[index - 1];
|
|
let toolNameToSelect;
|
|
|
|
if (nextTool) {
|
|
toolNameToSelect = nextTool.id;
|
|
}
|
|
if (previousTool) {
|
|
toolNameToSelect = previousTool.id;
|
|
}
|
|
if (toolNameToSelect) {
|
|
this.selectTool(toolNameToSelect);
|
|
}
|
|
}
|
|
|
|
// Remove this tool from the current panel definitions.
|
|
this.panelDefinitions = this.panelDefinitions.filter(({id}) => id !== toolId);
|
|
this.visibleAdditionalTools = this.visibleAdditionalTools
|
|
.filter(id => id !== toolId);
|
|
this._combineAndSortPanelDefinitions();
|
|
|
|
if (panel) {
|
|
panel.remove();
|
|
}
|
|
|
|
if (this.hostType == Toolbox.HostType.WINDOW) {
|
|
let doc = this.win.parent.document;
|
|
let key = doc.getElementById("key_" + toolId);
|
|
if (key) {
|
|
key.remove();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the tool-registered event.
|
|
* @param {string} event
|
|
* Name of the event ("tool-registered")
|
|
* @param {string} toolId
|
|
* Id of the tool that was registered
|
|
*/
|
|
_toolRegistered: function (event, toolId) {
|
|
// Tools can either be in the global devtools, or added to this specific toolbox
|
|
// as an additional tool.
|
|
let definition = gDevTools.getToolDefinition(toolId);
|
|
let isAdditionalTool = false;
|
|
if (!definition) {
|
|
definition = this.additionalToolDefinitions.get(toolId);
|
|
isAdditionalTool = true;
|
|
}
|
|
|
|
if (definition.isTargetSupported(this._target)) {
|
|
if (isAdditionalTool) {
|
|
this.visibleAdditionalTools = [...this.visibleAdditionalTools, toolId];
|
|
this._combineAndSortPanelDefinitions();
|
|
} else {
|
|
this.panelDefinitions = this.panelDefinitions.concat(definition);
|
|
}
|
|
this._buildPanelForTool(definition);
|
|
|
|
// Emit the event so tools can listen to it from the toolbox level
|
|
// instead of gDevTools.
|
|
this.emit("tool-registered", toolId);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handler for the tool-unregistered event.
|
|
* @param {string} event
|
|
* Name of the event ("tool-unregistered")
|
|
* @param {string} toolId
|
|
* id of the tool that was unregistered
|
|
*/
|
|
_toolUnregistered: function (event, toolId) {
|
|
this.unloadTool(toolId);
|
|
// Emit the event so tools can listen to it from the toolbox level
|
|
// instead of gDevTools
|
|
this.emit("tool-unregistered", toolId);
|
|
},
|
|
|
|
/**
|
|
* Initialize the inspector/walker/selection/highlighter fronts.
|
|
* Returns a promise that resolves when the fronts are initialized
|
|
*/
|
|
initInspector: function () {
|
|
if (!this._initInspector) {
|
|
this._initInspector = Task.spawn(function* () {
|
|
this._inspector = InspectorFront(this._target.client, this._target.form);
|
|
let pref = "devtools.inspector.showAllAnonymousContent";
|
|
let showAllAnonymousContent = Services.prefs.getBoolPref(pref);
|
|
this._walker = yield this._inspector.getWalker({ showAllAnonymousContent });
|
|
this._selection = new Selection(this._walker);
|
|
|
|
if (this.highlighterUtils.isRemoteHighlightable()) {
|
|
this.walker.on("highlighter-ready", this._highlighterReady);
|
|
this.walker.on("highlighter-hide", this._highlighterHidden);
|
|
|
|
let autohide = !flags.testing;
|
|
this._highlighter = yield this._inspector.getHighlighter(autohide);
|
|
}
|
|
}.bind(this));
|
|
}
|
|
return this._initInspector;
|
|
},
|
|
|
|
/**
|
|
* Destroy the inspector/walker/selection fronts
|
|
* Returns a promise that resolves when the fronts are destroyed
|
|
*/
|
|
destroyInspector: function () {
|
|
if (this._destroyingInspector) {
|
|
return this._destroyingInspector;
|
|
}
|
|
|
|
this._destroyingInspector = Task.spawn(function* () {
|
|
if (!this._inspector) {
|
|
return;
|
|
}
|
|
|
|
// Ensure that the inspector isn't still being initiated, otherwise race conditions
|
|
// in the initialization process can throw errors.
|
|
yield this._initInspector;
|
|
|
|
// Releasing the walker (if it has been created)
|
|
// This can fail, but in any case, we want to continue destroying the
|
|
// inspector/highlighter/selection
|
|
// FF42+: Inspector actor starts managing Walker actor and auto destroy it.
|
|
if (this._walker && !this.walker.traits.autoReleased) {
|
|
try {
|
|
yield this._walker.release();
|
|
} catch (e) {
|
|
// Do nothing;
|
|
}
|
|
}
|
|
|
|
yield this.highlighterUtils.stopPicker();
|
|
yield this._inspector.destroy();
|
|
if (this._highlighter) {
|
|
// Note that if the toolbox is closed, this will work fine, but will fail
|
|
// in case the browser is closed and will trigger a noSuchActor message.
|
|
// We ignore the promise that |_hideBoxModel| returns, since we should still
|
|
// proceed with the rest of destruction if it fails.
|
|
// FF42+ now does the cleanup from the actor.
|
|
if (!this.highlighter.traits.autoHideOnDestroy) {
|
|
this.highlighterUtils.unhighlight();
|
|
}
|
|
yield this._highlighter.destroy();
|
|
}
|
|
if (this._selection) {
|
|
this._selection.destroy();
|
|
}
|
|
|
|
if (this.walker) {
|
|
this.walker.off("highlighter-ready", this._highlighterReady);
|
|
this.walker.off("highlighter-hide", this._highlighterHidden);
|
|
}
|
|
|
|
this._inspector = null;
|
|
this._highlighter = null;
|
|
this._selection = null;
|
|
this._walker = null;
|
|
}.bind(this));
|
|
return this._destroyingInspector;
|
|
},
|
|
|
|
/**
|
|
* Get the toolbox's notification component
|
|
*
|
|
* @return The notification box component.
|
|
*/
|
|
getNotificationBox: function () {
|
|
return this.notificationBox;
|
|
},
|
|
|
|
/**
|
|
* Remove all UI elements, detach from target and clear up
|
|
*/
|
|
destroy: function () {
|
|
// If several things call destroy then we give them all the same
|
|
// destruction promise so we're sure to destroy only once
|
|
if (this._destroyer) {
|
|
return this._destroyer;
|
|
}
|
|
let deferred = defer();
|
|
this._destroyer = deferred.promise;
|
|
|
|
this.emit("destroy");
|
|
|
|
this._target.off("navigate", this._refreshHostTitle);
|
|
this._target.off("frame-update", this._updateFrames);
|
|
this.off("select", this._refreshHostTitle);
|
|
this.off("host-changed", this._refreshHostTitle);
|
|
this.off("ready", this._showDevEditionPromo);
|
|
|
|
gDevTools.off("tool-registered", this._toolRegistered);
|
|
gDevTools.off("tool-unregistered", this._toolUnregistered);
|
|
|
|
Services.prefs.removeObserver("devtools.cache.disabled", this._applyCacheSettings);
|
|
Services.prefs.removeObserver("devtools.serviceWorkers.testing.enabled",
|
|
this._applyServiceWorkersTestingSettings);
|
|
|
|
this._lastFocusedElement = null;
|
|
if (this._sourceMapService) {
|
|
this._sourceMapService.destroy();
|
|
this._sourceMapService = null;
|
|
}
|
|
|
|
if (this.webconsolePanel) {
|
|
this._saveSplitConsoleHeight();
|
|
this.webconsolePanel.removeEventListener("resize",
|
|
this._saveSplitConsoleHeight);
|
|
this.webconsolePanel = null;
|
|
}
|
|
if (this.textBoxContextMenuPopup) {
|
|
this.textBoxContextMenuPopup.removeEventListener("popupshowing",
|
|
this._updateTextBoxMenuItems, true);
|
|
this.textBoxContextMenuPopup = null;
|
|
}
|
|
if (this._componentMount) {
|
|
this._componentMount.removeEventListener("keypress", this._onToolbarArrowKeypress);
|
|
this.ReactDOM.unmountComponentAtNode(this._componentMount);
|
|
this._componentMount = null;
|
|
}
|
|
|
|
let outstanding = [];
|
|
for (let [id, panel] of this._toolPanels) {
|
|
try {
|
|
gDevTools.emit(id + "-destroy", this, panel);
|
|
this.emit(id + "-destroy", panel);
|
|
|
|
outstanding.push(panel.destroy());
|
|
} catch (e) {
|
|
// We don't want to stop here if any panel fail to close.
|
|
console.error("Panel " + id + ":", e);
|
|
}
|
|
}
|
|
|
|
this.browserRequire = null;
|
|
|
|
// Now that we are closing the toolbox we can re-enable the cache settings
|
|
// and disable the service workers testing settings for the current tab.
|
|
// FF41+ automatically cleans up state in actor on disconnect.
|
|
if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
|
|
this.target.activeTab.reconfigure({
|
|
"cacheDisabled": false,
|
|
"serviceWorkersTestingEnabled": false
|
|
});
|
|
}
|
|
|
|
// Destroying the walker and inspector fronts
|
|
outstanding.push(this.destroyInspector());
|
|
|
|
// Destroy the profiler connection
|
|
outstanding.push(this.destroyPerformance());
|
|
|
|
// Destroy the preference front
|
|
outstanding.push(this.destroyPreference());
|
|
|
|
// Detach the thread
|
|
detachThread(this._threadClient);
|
|
this._threadClient = null;
|
|
|
|
// Unregister buttons listeners
|
|
this.toolbarButtons.forEach(button => {
|
|
if (typeof button.teardown == "function") {
|
|
// teardown arguments have already been bound in _createButtonState
|
|
button.teardown();
|
|
}
|
|
});
|
|
|
|
// We need to grab a reference to win before this._host is destroyed.
|
|
let win = this.win;
|
|
|
|
this._telemetry.toolClosed("toolbox");
|
|
this._telemetry.destroy();
|
|
|
|
// Finish all outstanding tasks (which means finish destroying panels and
|
|
// then destroying the host, successfully or not) before destroying the
|
|
// target.
|
|
deferred.resolve(settleAll(outstanding)
|
|
.catch(console.error)
|
|
.then(() => {
|
|
this._removeHostListeners();
|
|
|
|
// `location` may already be null if the toolbox document is already
|
|
// in process of destruction. Otherwise if it is still around, ensure
|
|
// releasing toolbox document and triggering cleanup thanks to unload
|
|
// event. We do that precisely here, before nullifying the target as
|
|
// various cleanup code depends on the target attribute to be still
|
|
// defined.
|
|
if (win.location) {
|
|
win.location.replace("about:blank");
|
|
}
|
|
|
|
// Targets need to be notified that the toolbox is being torn down.
|
|
// This is done after other destruction tasks since it may tear down
|
|
// fronts and the debugger transport which earlier destroy methods may
|
|
// require to complete.
|
|
if (!this._target) {
|
|
return null;
|
|
}
|
|
let target = this._target;
|
|
this._target = null;
|
|
this.highlighterUtils.release();
|
|
target.off("close", this.destroy);
|
|
return target.destroy();
|
|
}, console.error).then(() => {
|
|
this.emit("destroyed");
|
|
|
|
// Free _host after the call to destroyed in order to let a chance
|
|
// to destroyed listeners to still query toolbox attributes
|
|
this._host = null;
|
|
this._win = null;
|
|
this._toolPanels.clear();
|
|
|
|
// Force GC to prevent long GC pauses when running tests and to free up
|
|
// memory in general when the toolbox is closed.
|
|
if (flags.testing) {
|
|
win.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIDOMWindowUtils)
|
|
.garbageCollect();
|
|
}
|
|
}).then(null, console.error));
|
|
|
|
let leakCheckObserver = ({wrappedJSObject: barrier}) => {
|
|
// Make the leak detector wait until this toolbox is properly destroyed.
|
|
barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
|
|
this._destroyer);
|
|
};
|
|
|
|
let topic = "shutdown-leaks-before-check";
|
|
Services.obs.addObserver(leakCheckObserver, topic, false);
|
|
this._destroyer.then(() => {
|
|
Services.obs.removeObserver(leakCheckObserver, topic);
|
|
});
|
|
|
|
return this._destroyer;
|
|
},
|
|
|
|
_highlighterReady: function () {
|
|
this.emit("highlighter-ready");
|
|
},
|
|
|
|
_highlighterHidden: function () {
|
|
this.emit("highlighter-hide");
|
|
},
|
|
|
|
/**
|
|
* For displaying the promotional Doorhanger on first opening of
|
|
* the developer tools, promoting the Developer Edition.
|
|
*/
|
|
_showDevEditionPromo: function () {
|
|
// Do not display in browser toolbox
|
|
if (this.target.chrome) {
|
|
return;
|
|
}
|
|
showDoorhanger({ window: this.win, type: "deveditionpromo" });
|
|
},
|
|
|
|
/**
|
|
* Enable / disable necessary textbox menu items using globalOverlay.js.
|
|
*/
|
|
_updateTextBoxMenuItems: function () {
|
|
let window = this.win;
|
|
["cmd_undo", "cmd_delete", "cmd_cut",
|
|
"cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
|
|
},
|
|
|
|
/**
|
|
* Open the textbox context menu at given coordinates.
|
|
* Panels in the toolbox can call this on contextmenu events with event.screenX/Y
|
|
* instead of having to implement their own copy/paste/selectAll menu.
|
|
* @param {Number} x
|
|
* @param {Number} y
|
|
*/
|
|
openTextBoxContextMenu: function (x, y) {
|
|
this.textBoxContextMenuPopup.openPopupAtScreen(x, y, true);
|
|
},
|
|
|
|
/**
|
|
* Connects to the Gecko Profiler when the developer tools are open. This is
|
|
* necessary because of the WebConsole's `profile` and `profileEnd` methods.
|
|
*/
|
|
initPerformance: Task.async(function* () {
|
|
// If target does not have profiler actor (addons), do not
|
|
// even register the shared performance connection.
|
|
if (!this.target.hasActor("profiler")) {
|
|
return promise.resolve();
|
|
}
|
|
|
|
if (this._performanceFrontConnection) {
|
|
return this._performanceFrontConnection.promise;
|
|
}
|
|
|
|
this._performanceFrontConnection = defer();
|
|
this._performance = createPerformanceFront(this._target);
|
|
yield this.performance.connect();
|
|
|
|
// Emit an event when connected, but don't wait on startup for this.
|
|
this.emit("profiler-connected");
|
|
|
|
this.performance.on("*", this._onPerformanceFrontEvent);
|
|
this._performanceFrontConnection.resolve(this.performance);
|
|
return this._performanceFrontConnection.promise;
|
|
}),
|
|
|
|
/**
|
|
* Disconnects the underlying Performance actor. If the connection
|
|
* has not finished initializing, as opening a toolbox does not wait,
|
|
* the performance connection destroy method will wait for it on its own.
|
|
*/
|
|
destroyPerformance: Task.async(function* () {
|
|
if (!this.performance) {
|
|
return;
|
|
}
|
|
// If still connecting to performance actor, allow the
|
|
// actor to resolve its connection before attempting to destroy.
|
|
if (this._performanceFrontConnection) {
|
|
yield this._performanceFrontConnection.promise;
|
|
}
|
|
this.performance.off("*", this._onPerformanceFrontEvent);
|
|
yield this.performance.destroy();
|
|
this._performance = null;
|
|
}),
|
|
|
|
/**
|
|
* Destroy the preferences actor when the toolbox is unloaded.
|
|
*/
|
|
destroyPreference: Task.async(function* () {
|
|
let front = yield this._preferenceFront;
|
|
front.destroy();
|
|
}),
|
|
|
|
/**
|
|
* Called when any event comes from the PerformanceFront. If the performance tool is
|
|
* already loaded when the first event comes in, immediately unbind this handler, as
|
|
* this is only used to queue up observed recordings before the performance tool can
|
|
* handle them, which will only occur when `console.profile()` recordings are started
|
|
* before the tool loads.
|
|
*/
|
|
_onPerformanceFrontEvent: Task.async(function* (eventName, recording) {
|
|
if (this.getPanel("performance")) {
|
|
this.performance.off("*", this._onPerformanceFrontEvent);
|
|
return;
|
|
}
|
|
|
|
this._performanceQueuedRecordings = this._performanceQueuedRecordings || [];
|
|
let recordings = this._performanceQueuedRecordings;
|
|
|
|
// Before any console recordings, we'll get a `console-profile-start` event
|
|
// warning us that a recording will come later (via `recording-started`), so
|
|
// start to boot up the tool and populate the tool with any other recordings
|
|
// observed during that time.
|
|
if (eventName === "console-profile-start" && !this._performanceToolOpenedViaConsole) {
|
|
this._performanceToolOpenedViaConsole = this.loadTool("performance");
|
|
let panel = yield this._performanceToolOpenedViaConsole;
|
|
yield panel.open();
|
|
|
|
panel.panelWin.PerformanceController.populateWithRecordings(recordings);
|
|
this.performance.off("*", this._onPerformanceFrontEvent);
|
|
}
|
|
|
|
// Otherwise, if it's a recording-started event, we've already started loading
|
|
// the tool, so just store this recording in our array to be later populated
|
|
// once the tool loads.
|
|
if (eventName === "recording-started") {
|
|
recordings.push(recording);
|
|
}
|
|
}),
|
|
|
|
/**
|
|
* Returns gViewSourceUtils for viewing source.
|
|
*/
|
|
get gViewSourceUtils() {
|
|
return this.win.gViewSourceUtils;
|
|
},
|
|
|
|
/**
|
|
* Opens source in style editor. Falls back to plain "view-source:".
|
|
* @see devtools/client/shared/source-utils.js
|
|
*/
|
|
viewSourceInStyleEditor: function (sourceURL, sourceLine) {
|
|
return viewSource.viewSourceInStyleEditor(this, sourceURL, sourceLine);
|
|
},
|
|
|
|
/**
|
|
* Opens source in debugger. Falls back to plain "view-source:".
|
|
* @see devtools/client/shared/source-utils.js
|
|
*/
|
|
viewSourceInDebugger: function (sourceURL, sourceLine) {
|
|
return viewSource.viewSourceInDebugger(this, sourceURL, sourceLine);
|
|
},
|
|
|
|
/**
|
|
* Opens source in scratchpad. Falls back to plain "view-source:".
|
|
* TODO The `sourceURL` for scratchpad instances are like `Scratchpad/1`.
|
|
* If instances are scoped one-per-browser-window, then we should be able
|
|
* to infer the URL from this toolbox, or use the built in scratchpad IN
|
|
* the toolbox.
|
|
*
|
|
* @see devtools/client/shared/source-utils.js
|
|
*/
|
|
viewSourceInScratchpad: function (sourceURL, sourceLine) {
|
|
return viewSource.viewSourceInScratchpad(sourceURL, sourceLine);
|
|
},
|
|
|
|
/**
|
|
* Opens source in plain "view-source:".
|
|
* @see devtools/client/shared/source-utils.js
|
|
*/
|
|
viewSource: function (sourceURL, sourceLine) {
|
|
return viewSource.viewSource(this, sourceURL, sourceLine);
|
|
},
|
|
};
|