gecko-dev/devtools/startup/DevToolsShim.jsm

337 строки
10 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyGetter(this, "DevtoolsStartup", () => {
return Cc["@mozilla.org/devtools/startup-clh;1"].getService(
Ci.nsICommandLineHandler
).wrappedJSObject;
});
// We don't want to spend time initializing the full loader here so we create
// our own lazy require.
XPCOMUtils.defineLazyGetter(this, "Telemetry", function() {
const { require } = ChromeUtils.import(
"resource://devtools/shared/Loader.jsm"
);
// eslint-disable-next-line no-shadow
const Telemetry = require("devtools/client/shared/telemetry");
return Telemetry;
});
const DEVTOOLS_ENABLED_PREF = "devtools.enabled";
const DEVTOOLS_POLICY_DISABLED_PREF = "devtools.policy.disabled";
this.EXPORTED_SYMBOLS = ["DevToolsShim"];
function removeItem(array, callback) {
const index = array.findIndex(callback);
if (index >= 0) {
array.splice(index, 1);
}
}
/**
* DevToolsShim is a singleton that provides a set of helpers to interact with DevTools,
* that work whether Devtools are enabled or not.
*
* It can be used to start listening to devtools events before DevTools are ready. As soon
* as DevTools are enabled, the DevToolsShim will forward all the requests received until
* then to the real DevTools instance.
*/
this.DevToolsShim = {
_gDevTools: null,
listeners: [],
get telemetry() {
if (!this._telemetry) {
this._telemetry = new Telemetry();
this._telemetry.setEventRecordingEnabled(true);
}
return this._telemetry;
},
/**
* Returns true if DevTools are enabled for the current profile. If devtools are not
* enabled, initializing DevTools will open the onboarding page. Some entry points
* should no-op in this case.
*/
isEnabled: function() {
const enabled = Services.prefs.getBoolPref(DEVTOOLS_ENABLED_PREF);
return enabled && !this.isDisabledByPolicy();
},
/**
* Returns true if the devtools are completely disabled and can not be enabled. All
* entry points should return without throwing, initDevTools should never be called.
*/
isDisabledByPolicy: function() {
return Services.prefs.getBoolPref(DEVTOOLS_POLICY_DISABLED_PREF, false);
},
/**
* Check if DevTools have already been initialized.
*
* @return {Boolean} true if DevTools are initialized.
*/
isInitialized: function() {
return !!this._gDevTools;
},
/**
* Returns the array of the existing toolboxes. This method is part of the compatibility
* layer for webextensions.
*
* @return {Array<Toolbox>}
* An array of toolboxes.
*/
getToolboxes: function() {
if (this.isInitialized()) {
return this._gDevTools.getToolboxes();
}
return [];
},
/**
* Register an instance of gDevTools. Should be called by DevTools during startup.
*
* @param {DevTools} a devtools instance (from client/framework/devtools)
*/
register: function(gDevTools) {
this._gDevTools = gDevTools;
this._onDevToolsRegistered();
this._gDevTools.emit("devtools-registered");
},
/**
* Unregister the current instance of gDevTools. Should be called by DevTools during
* shutdown.
*/
unregister: function() {
if (this.isInitialized()) {
this._gDevTools.emit("devtools-unregistered");
this._gDevTools = null;
}
},
/**
* The following methods can be called before DevTools are initialized:
* - on
* - off
*
* If DevTools are not initialized when calling the method, DevToolsShim will call the
* appropriate method as soon as a gDevTools instance is registered.
*/
/**
* This method is used by browser/components/extensions/ext-devtools.js for the events:
* - toolbox-created
* - toolbox-destroyed
*/
on: function(event, listener) {
if (this.isInitialized()) {
this._gDevTools.on(event, listener);
} else {
this.listeners.push([event, listener]);
}
},
/**
* This method is currently only used by devtools code, but is kept here for consistency
* with on().
*/
off: function(event, listener) {
if (this.isInitialized()) {
this._gDevTools.off(event, listener);
} else {
removeItem(this.listeners, ([e, l]) => e === event && l === listener);
}
},
/**
* Called from SessionStore.jsm in mozilla-central when saving the current state.
*
* @param {Object} state
* A SessionStore state object that gets modified by reference
*/
saveDevToolsSession: function(state) {
if (!this.isInitialized()) {
return;
}
this._gDevTools.saveDevToolsSession(state);
},
/**
* Called from SessionStore.jsm in mozilla-central when restoring a previous session.
* Will always be called, even if the session does not contain DevTools related items.
*/
restoreDevToolsSession: function(session) {
if (!this.isEnabled()) {
return;
}
const { scratchpads, browserConsole, browserToolbox } = session;
const hasDevToolsData =
browserConsole || browserToolbox || (scratchpads && scratchpads.length);
if (!hasDevToolsData) {
// Do not initialize DevTools unless there is DevTools specific data in the session.
return;
}
this.initDevTools("SessionRestore");
this._gDevTools.restoreDevToolsSession(session);
},
/**
* Called from nsContextMenu.js in mozilla-central when using the Inspect Accessibility
* context menu item.
*
* @param {XULTab} tab
* The browser tab on which inspect accessibility was used.
* @param {Array} selectors
* An array of CSS selectors to find the target accessible object. Several
* selectors can be needed if the element is nested in frames and not directly
* in the root document.
* @return {Promise} a promise that resolves when the accessible node is selected in the
* accessibility inspector or that resolves immediately if DevTools are not
* enabled.
*/
inspectA11Y: function(tab, selectors) {
if (!this.isEnabled()) {
if (!this.isDisabledByPolicy()) {
DevtoolsStartup.openInstallPage("ContextMenu");
}
return Promise.resolve();
}
// Record the timing at which this event started in order to compute later in
// gDevTools.showToolbox, the complete time it takes to open the toolbox.
// i.e. especially take `DevtoolsStartup.initDevTools` into account.
const startTime = Cu.now();
this.initDevTools("ContextMenu");
return this._gDevTools.inspectA11Y(tab, selectors, startTime);
},
/**
* Called from nsContextMenu.js in mozilla-central when using the Inspect Element
* context menu item.
*
* @param {XULTab} tab
* The browser tab on which inspect node was used.
* @param {Array} selectors
* An array of CSS selectors to find the target node. Several selectors can be
* needed if the element is nested in frames and not directly in the root
* document. The selectors are ordered starting with the root document and
* ending with the deepest nested frame.
* @return {Promise} a promise that resolves when the node is selected in the inspector
* markup view or that resolves immediately if DevTools are not enabled.
*/
inspectNode: function(tab, selectors) {
if (!this.isEnabled()) {
if (!this.isDisabledByPolicy()) {
DevtoolsStartup.openInstallPage("ContextMenu");
}
return Promise.resolve();
}
// Record the timing at which this event started in order to compute later in
// gDevTools.showToolbox, the complete time it takes to open the toolbox.
// i.e. especially take `DevtoolsStartup.initDevTools` into account.
const startTime = Cu.now();
this.initDevTools("ContextMenu");
return this._gDevTools.inspectNode(tab, selectors, startTime);
},
_onDevToolsRegistered: function() {
// Register all pending event listeners on the real gDevTools object.
for (const [event, listener] of this.listeners) {
this._gDevTools.on(event, listener);
}
this.listeners = [];
},
/**
* Initialize DevTools via DevToolsStartup if needed. This method throws if DevTools are
* not enabled.. If the entry point is supposed to trigger the onboarding, call it
* explicitly via DevtoolsStartup.openInstallPage().
*
* @param {String} reason
* optional, if provided should be a valid entry point for DEVTOOLS_ENTRY_POINT
* in toolkit/components/telemetry/Histograms.json
*/
initDevTools: function(reason) {
if (!this.isEnabled()) {
throw new Error("DevTools are not enabled and can not be initialized.");
}
if (reason) {
const window = Services.wm.getMostRecentWindow("navigator:browser");
this.telemetry.addEventProperty(
window,
"open",
"tools",
null,
"shortcut",
""
);
this.telemetry.addEventProperty(
window,
"open",
"tools",
null,
"entrypoint",
reason
);
}
if (!this.isInitialized()) {
DevtoolsStartup.initDevTools(reason);
}
},
};
/**
* Compatibility layer for webextensions.
*
* Those methods are called only after a DevTools webextension was loaded in DevTools,
* therefore DevTools should always be available when they are called.
*/
const webExtensionsMethods = [
"createTargetForTab",
"createWebExtensionInspectedWindowFront",
"getTargetForTab",
"getTheme",
"openBrowserConsole",
];
for (const method of webExtensionsMethods) {
this.DevToolsShim[method] = function() {
if (!this.isEnabled()) {
throw new Error(
"Could not call a DevToolsShim webextension method ('" +
method +
"'): DevTools are not initialized."
);
}
this.initDevTools();
return this._gDevTools[method].apply(this._gDevTools, arguments);
};
}