diff options
Diffstat (limited to 'devtools/client/fronts/inspector.js')
-rw-r--r-- | devtools/client/fronts/inspector.js | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/devtools/client/fronts/inspector.js b/devtools/client/fronts/inspector.js new file mode 100644 index 0000000000..116993078b --- /dev/null +++ b/devtools/client/fronts/inspector.js @@ -0,0 +1,282 @@ +/* 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, +} = 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 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); |