gecko-dev/devtools/startup/DevToolsShim.jsm
Julian Descottes c72c1dcdcc Bug 1588773 - Use ContentDOMReference for context menu Inspect Element r=mconley,pbro
Depends on D49941

Using ContentDOMReference instead of creating an array of selectors makes inspect element more stable in case the page is modified between after the contextmenu opens.
It will also make the feature easier to make fission compatible

Differential Revision: https://phabricator.services.mozilla.com/D49303

--HG--
extra : moz-landing-system : lando
2019-10-28 09:10:29 +00:00

333 lines
9.8 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 { browserConsole, browserToolbox } = session;
const hasDevToolsData = browserConsole || browserToolbox;
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 {ElementIdentifier} domReference
* Identifier generated by ContentDOMReference. It is a unique pair of
* BrowsingContext ID and a numeric ID.
* @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, domReference) {
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, domReference, 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 {ElementIdentifier} domReference
* Identifier generated by ContentDOMReference. It is a unique pair of
* BrowsingContext ID and a numeric ID.
* @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, domReference) {
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, domReference, 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);
};
}