Source code

Revision control

Copy as Markdown

Other Tools

/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
const {
FrontClassWithSpec,
registerFront,
const {
inspectorSpec,
loader.lazyRequireGetter(
this,
"captureScreenshot",
true
);
const TELEMETRY_EYEDROPPER_OPENED = "DEVTOOLS_EYEDROPPER_OPENED_COUNT";
const TELEMETRY_EYEDROPPER_OPENED_MENU =
"DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT";
const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
"devtools.inspector.showAllAnonymousContent";
const telemetry = new Telemetry();
/**
* Client side of the inspector actor, which is used to create
* inspector-related actors, including the walker.
*/
class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
constructor(client, targetFront, parentFront) {
super(client, targetFront, parentFront);
this._client = client;
this._highlighters = new Map();
// Attribute name from which to retrieve the actorID out of the target actor's form
this.formAttributeName = "inspectorActor";
// Map of highlighter types to unsettled promises to create a highlighter of that type
this._pendingGetHighlighterMap = new Map();
this.noopStylesheetListener = () => {};
}
// async initialization
async initialize() {
if (this.initialized) {
return this.initialized;
}
// Watch STYLESHEET resources to fill the ResourceCommand cache.
// StyleRule front's `get parentStyleSheet()` will query the cache to
// retrieve the resource corresponding to the parent stylesheet of a rule.
const { resourceCommand } = this.targetFront.commands;
// Backup resourceCommand, targetFront.commands might be null in `destroy`.
this.resourceCommand = resourceCommand;
await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
onAvailable: this.noopStylesheetListener,
});
// Bail out if the inspector is closed while watchResources was pending
if (this.isDestroyed()) {
return null;
}
this.initialized = await Promise.all([
this._getWalker(),
this._getPageStyle(),
]);
return this.initialized;
}
async _getWalker() {
const showAllAnonymousContent = Services.prefs.getBoolPref(
SHOW_ALL_ANONYMOUS_CONTENT_PREF
);
this.walker = await this.getWalker({
showAllAnonymousContent,
});
// We need to reparent the RootNode of remote iframe Walkers
// so that their parent is the NodeFront of the <iframe>
// element, coming from another process/target/WalkerFront.
await this.walker.reparentRemoteFrame();
}
hasHighlighter(type) {
return this._highlighters.has(type);
}
async _getPageStyle() {
this.pageStyle = await super.getPageStyle();
}
async getCompatibilityFront() {
if (!this._compatibility) {
this._compatibility = await super.getCompatibility();
}
return this._compatibility;
}
destroy() {
if (this.isDestroyed()) {
return;
}
this._compatibility = null;
const { resourceCommand } = this;
resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], {
onAvailable: this.noopStylesheetListener,
});
this.resourceCommand = null;
this.walker = null;
// CustomHighlighter fronts are managed by InspectorFront and so will be
// automatically destroyed. But we have to clear the `_highlighters`
// Map as well as explicitly call `finalize` request on all of them.
this.destroyHighlighters();
super.destroy();
}
destroyHighlighters() {
for (const type of this._highlighters.keys()) {
if (this._highlighters.has(type)) {
const highlighter = this._highlighters.get(type);
if (!highlighter.isDestroyed()) {
highlighter.finalize();
}
this._highlighters.delete(type);
}
}
}
async getHighlighterByType(typeName) {
let highlighter = null;
try {
highlighter = await super.getHighlighterByType(typeName);
} catch (_) {
throw new Error(
"The target doesn't support " +
`creating highlighters by types or ${typeName} is unknown`
);
}
return highlighter;
}
getKnownHighlighter(type) {
return this._highlighters.get(type);
}
/**
* Return a highlighter instance of the given type.
* If an instance was previously created, return it. Else, create and return a new one.
*
* Store a promise for the request to create a new highlighter. If another request
* comes in before that promise is resolved, wait for it to resolve and return the
* highlighter instance it resolved with instead of creating a new request.
*
* @param {String} type
* Highlighter type
* @return {Promise}
* Promise which resolves with a highlighter instance of the given type
*/
async getOrCreateHighlighterByType(type) {
let front = this._highlighters.get(type);
let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type);
if (!front && !pendingGetHighlighter) {
pendingGetHighlighter = (async () => {
const highlighter = await this.getHighlighterByType(type);
this._highlighters.set(type, highlighter);
this._pendingGetHighlighterMap.delete(type);
return highlighter;
})();
this._pendingGetHighlighterMap.set(type, pendingGetHighlighter);
}
if (pendingGetHighlighter) {
front = await pendingGetHighlighter;
}
return front;
}
async pickColorFromPage(options) {
let screenshot = null;
// @backward-compat { version 87 } ScreenshotContentActor was only added in 87.
// When connecting to older server, the eyedropper will use drawWindow
// to retrieve the screenshot of the page (that's a decent fallback,
// even if it doesn't handle remote frames).
if (this.targetFront.hasActor("screenshotContent")) {
try {
// We use the screenshot actors as it can retrieve an image of the current viewport,
// handling remote frame if need be.
const { data } = await captureScreenshot(this.targetFront, {
browsingContextID: this.targetFront.browsingContextID,
disableFlash: true,
ignoreDprForFileScale: true,
});
screenshot = data;
} catch (e) {
// We simply log the error and still call pickColorFromPage as it will default to
// use drawWindow in order to get the screenshot of the page (that's a decent
// fallback, even if it doesn't handle remote frames).
console.error(
"Error occured when taking a screenshot for the eyedropper",
e
);
}
}
await super.pickColorFromPage({
...options,
screenshot,
});
if (options?.fromMenu) {
telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED_MENU).add(true);
} else {
telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED).add(true);
}
}
/**
* Given a node grip, return a NodeFront on the right context.
*
* @param {Object} grip: The node grip.
* @returns {Promise<NodeFront|null>} A promise that resolves with a NodeFront or null
* if the NodeFront couldn't be created/retrieved.
*/
async getNodeFrontFromNodeGrip(grip) {
return this.getNodeActorFromContentDomReference(grip.contentDomReference);
}
async getNodeActorFromContentDomReference(contentDomReference) {
const { browsingContextId } = contentDomReference;
// If the contentDomReference lives in the same browsing context id than the
// current one, we can directly use the current walker.
if (this.targetFront.browsingContextID === browsingContextId) {
return this.walker.getNodeActorFromContentDomReference(
contentDomReference
);
}
// If the contentDomReference has a different browsing context than the current one,
// we are either in Fission or in the Multiprocess Browser Toolbox, so we need to
// retrieve the walker of the WindowGlobalTarget.
// Get the target for this remote frame element
// Tab and Process Descriptors expose a Watcher, which should be used to
// fetch the node's target.
let target;
const { watcherFront } = this.targetFront.commands;
if (watcherFront) {
target = await watcherFront.getWindowGlobalTarget(browsingContextId);
} else {
// For descriptors which don't expose a watcher (e.g. WebExtension)
// we used to call RootActor::getBrowsingContextDescriptor, but it was
// removed in FF77.
// Support for watcher in WebExtension descriptors is Bug 1644341.
throw new Error(
`Unable to call getNodeActorFromContentDomReference for ${this.targetFront.actorID}`
);
}
const { walker } = await target.getFront("inspector");
return walker.getNodeActorFromContentDomReference(contentDomReference);
}
}
exports.InspectorFront = InspectorFront;
registerFront(InspectorFront);