Files
sousa-gecko/devtools/client/fronts/inspector.js
T
Alexandre Poirot 0b3488bea7 Bug 1247553 - [devtools] Use Target Actor configuration to make rulers highlighter global and persist over navigations. r=devtools-reviewers,nchevobbe
While the frontend doesn't need passing the isShown state from the
actor, the tests require to pass it via highlighter's actor's form.
The frontend can derivate the current state by looking at target actor
configuration.

Differential Revision: https://phabricator.services.mozilla.com/D306098
2026-06-23 05:54:35 +00:00

340 lines
11 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 {
FrontClassWithSpec,
registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
inspectorSpec,
} = require("resource://devtools/shared/specs/inspector.js");
loader.lazyRequireGetter(
this,
"captureScreenshot",
"resource://devtools/client/shared/screenshot.js",
true
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TYPES: "resource://devtools/shared/highlighters.mjs",
});
const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
"devtools.inspector.showAllAnonymousContent";
const SHOW_COMMENTS_PREF = "devtools.markup.showComments";
/**
* 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,
});
// @backward-compat { version 154 } Firefox 154 started supporting toggling
// global highlighters via Target Actor Configuration. We no longer need to watch
// for will-navigate to clear frontend highlighters and can remove this code.
const { configuration } =
this.targetFront.commands.targetConfigurationCommand;
if ("enabledHighlighters" in configuration) {
await resourceCommand.watchResources(
[resourceCommand.TYPES.DOCUMENT_EVENT],
{
onAvailable: this.#documentEventListener,
}
);
}
// Bail out if the inspector is closed while watchResources was pending
if (this.isDestroyed()) {
return null;
}
const promises = [this._getWalker(), this._getPageStyle()];
if (this.targetFront.commands.descriptorFront.isTabDescriptor) {
promises.push(this._enableViewportSizeOnResizeHighlighter());
}
this.initialized = await Promise.all(promises);
return this.initialized;
}
async _getWalker() {
const showAllAnonymousContent = Services.prefs.getBoolPref(
SHOW_ALL_ANONYMOUS_CONTENT_PREF
);
const showComments = Services.prefs.getBoolPref(SHOW_COMMENTS_PREF);
this.walker = await this.getWalker({
showAllAnonymousContent,
showComments,
});
// 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();
}
// @backward-compat { version 154 } Firefox 154 started supporting toggling
// global highlighters via Target Actor Configuration. We no longer need to watch
// for will-navigate to clear frontend highlighters and can remove this code.
#documentEventListener = resources => {
const willNavigate = resources.some(
resource =>
resource.name == "will-navigate" && resource.targetFront.isTopLevel
);
if (!willNavigate) {
return;
}
// Manually clear the highlighters on the frontend, to replicate what happens
// on the backend and avoid keeping defunct enabled highlighters
this._highlighters.clear();
};
hasHighlighter(type) {
return this._highlighters.has(type);
}
async _getPageStyle() {
this.pageStyle = await super.getPageStyle();
}
async _enableViewportSizeOnResizeHighlighter() {
const highlighter = await this.getOrCreateHighlighterByType(
lazy.TYPES.VIEWPORT_SIZE_ON_RESIZE
);
await highlighter.show(this);
}
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,
});
resourceCommand.unwatchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
onAvailable: this.#documentEventListener,
});
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 `destroy` request on all of them.
this.destroyHighlighters();
super.destroy();
}
destroyHighlighters() {
for (const type of this._highlighters.keys()) {
this.destroyHighlighterByType(type);
}
}
destroyHighlighterByType(type) {
if (!this._highlighters.has(type)) {
return;
}
const highlighter = this._highlighters.get(type);
highlighter.destroy();
this._highlighters.delete(type);
}
/**
* Retrieve the HighlighterFront for a given highlighter type.
*
* @param {string} typeName
* Highlighter type coming from devtools/shared/highlighters.mjs
* @param {boolean} forceNew
* In case you are always expecting to instantiate a new Front instance,
* set this argument to true.
* @return {HighlighterFront}
*/
async getHighlighterByType(typeName, forceNew = false) {
let highlighter = null;
try {
highlighter = await super.getHighlighterByType(typeName, forceNew);
} 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 || front.isDestroyed()) && !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) {
Glean.devtools.menuEyedropperOpenedCount.add(1);
} else {
Glean.devtools.eyedropperOpenedCount.add(1);
}
}
/**
* 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);
}
}
registerFront(InspectorFront);