diff options
Diffstat (limited to '')
-rw-r--r-- | devtools/server/actors/highlighters.js | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/devtools/server/actors/highlighters.js b/devtools/server/actors/highlighters.js new file mode 100644 index 0000000000..9677632af9 --- /dev/null +++ b/devtools/server/actors/highlighters.js @@ -0,0 +1,393 @@ +/* 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 { Ci, Cu } = require("chrome"); + +const ChromeUtils = require("ChromeUtils"); +const EventEmitter = require("devtools/shared/event-emitter"); +const protocol = require("devtools/shared/protocol"); +const { customHighlighterSpec } = require("devtools/shared/specs/highlighters"); + +loader.lazyRequireGetter( + this, + "isXUL", + "devtools/server/actors/highlighters/utils/markup", + true +); + +/** + * The registration mechanism for highlighters provides a quick way to + * have modular highlighters instead of a hard coded list. + */ +const highlighterTypes = new Map(); + +/** + * Returns `true` if a highlighter for the given `typeName` is registered, + * `false` otherwise. + */ +const isTypeRegistered = typeName => highlighterTypes.has(typeName); +exports.isTypeRegistered = isTypeRegistered; + +/** + * Registers a given constructor as highlighter, for the `typeName` given. + */ +const registerHighlighter = (typeName, modulePath) => { + if (highlighterTypes.has(typeName)) { + throw Error(`${typeName} is already registered.`); + } + + highlighterTypes.set(typeName, modulePath); +}; + +/** + * CustomHighlighterActor is a generic Actor that instantiates a custom implementation of + * a highlighter class given its type name which must be registered in `highlighterTypes`. + * CustomHighlighterActor proxies calls to methods of the highlighter class instance: + * constructor(targetActor), show(node, options), hide(), destroy() + */ +exports.CustomHighlighterActor = protocol.ActorClassWithSpec( + customHighlighterSpec, + { + /** + * Create a highlighter instance given its typeName. + */ + initialize: function(parent, typeName) { + protocol.Actor.prototype.initialize.call(this, null); + + this._parent = parent; + + const modulePath = highlighterTypes.get(typeName); + if (!modulePath) { + const list = [...highlighterTypes.keys()]; + + throw new Error( + `${typeName} isn't a valid highlighter class (${list})` + ); + } + + const constructor = require(modulePath)[typeName]; + // The assumption is that custom highlighters either need the canvasframe + // container to append their elements and thus a non-XUL window or they have + // to define a static XULSupported flag that indicates that the highlighter + // supports XUL windows. Otherwise, bail out. + if (!isXUL(this._parent.targetActor.window) || constructor.XULSupported) { + this._highlighterEnv = new HighlighterEnvironment(); + this._highlighterEnv.initFromTargetActor(parent.targetActor); + this._highlighter = new constructor(this._highlighterEnv); + if (this._highlighter.on) { + this._highlighter.on( + "highlighter-event", + this._onHighlighterEvent.bind(this) + ); + } + } else { + throw new Error( + "Custom " + typeName + "highlighter cannot be created in a XUL window" + ); + } + }, + + get conn() { + return this._parent && this._parent.conn; + }, + + destroy: function() { + protocol.Actor.prototype.destroy.call(this); + this.finalize(); + this._parent = null; + }, + + release: function() {}, + + /** + * Get current instance of the highlighter object. + */ + get instance() { + return this._highlighter; + }, + + /** + * Show the highlighter. + * This calls through to the highlighter instance's |show(node, options)| + * method. + * + * Most custom highlighters are made to highlight DOM nodes, hence the first + * NodeActor argument (NodeActor as in devtools/server/actor/inspector). + * Note however that some highlighters use this argument merely as a context + * node: The SelectorHighlighter for instance uses it as a base node to run the + * provided CSS selector on. + * + * @param {NodeActor} The node to be highlighted + * @param {Object} Options for the custom highlighter + * @return {Boolean} True, if the highlighter has been successfully shown + */ + show: function(node, options) { + if (!this._highlighter) { + return null; + } + + const rawNode = node?.rawNode; + + return this._highlighter.show(rawNode, options); + }, + + /** + * Hide the highlighter if it was shown before + */ + hide: function() { + if (this._highlighter) { + this._highlighter.hide(); + } + }, + + /** + * Upon receiving an event from the highlighter, forward it to the client. + */ + _onHighlighterEvent: function(data) { + this.emit("highlighter-event", data); + }, + + /** + * Destroy the custom highlighter implementation. + * This method is called automatically just before the actor is destroyed. + */ + finalize: function() { + if (this._highlighter) { + if (this._highlighter.off) { + this._highlighter.off( + "highlighter-event", + this._onHighlighterEvent.bind(this) + ); + } + this._highlighter.destroy(); + this._highlighter = null; + } + + if (this._highlighterEnv) { + this._highlighterEnv.destroy(); + this._highlighterEnv = null; + } + }, + } +); + +/** + * The HighlighterEnvironment is an object that holds all the required data for + * highlighters to work: the window, docShell, event listener target, ... + * It also emits "will-navigate", "navigate" and "window-ready" events, + * similarly to the BrowsingContextTargetActor. + * + * It can be initialized either from a BrowsingContextTargetActor (which is the + * most frequent way of using it, since highlighters are initialized by + * CustomHighlighterActor, which has a targetActor reference). + * It can also be initialized just with a window object (which is + * useful for when a highlighter is used outside of the devtools server context. + */ +function HighlighterEnvironment() { + this.relayTargetActorWindowReady = this.relayTargetActorWindowReady.bind( + this + ); + this.relayTargetActorNavigate = this.relayTargetActorNavigate.bind(this); + this.relayTargetActorWillNavigate = this.relayTargetActorWillNavigate.bind( + this + ); + + EventEmitter.decorate(this); +} + +exports.HighlighterEnvironment = HighlighterEnvironment; + +HighlighterEnvironment.prototype = { + initFromTargetActor: function(targetActor) { + this._targetActor = targetActor; + this._targetActor.on("window-ready", this.relayTargetActorWindowReady); + this._targetActor.on("navigate", this.relayTargetActorNavigate); + this._targetActor.on("will-navigate", this.relayTargetActorWillNavigate); + }, + + initFromWindow: function(win) { + this._win = win; + + // We need a progress listener to know when the window will navigate/has + // navigated. + const self = this; + this.listener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onStateChange: function(progress, request, flag) { + const isStart = flag & Ci.nsIWebProgressListener.STATE_START; + const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP; + const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW; + const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT; + + if (progress.DOMWindow !== win) { + return; + } + + if (isDocument && isStart) { + // One of the earliest events that tells us a new URI is being loaded + // in this window. + self.emit("will-navigate", { + window: win, + isTopLevel: true, + }); + } + if (isWindow && isStop) { + self.emit("navigate", { + window: win, + isTopLevel: true, + }); + } + }, + }; + + this.webProgress.addProgressListener( + this.listener, + Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + }, + + get isInitialized() { + return this._win || this._targetActor; + }, + + get isXUL() { + return isXUL(this.window); + }, + + get window() { + if (!this.isInitialized) { + throw new Error( + "Initialize HighlighterEnvironment with a targetActor " + + "or window first" + ); + } + const win = this._targetActor ? this._targetActor.window : this._win; + + try { + return Cu.isDeadWrapper(win) ? null : win; + } catch (e) { + // win is null + return null; + } + }, + + get document() { + return this.window && this.window.document; + }, + + get docShell() { + return this.window && this.window.docShell; + }, + + get webProgress() { + return ( + this.docShell && + this.docShell + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress) + ); + }, + + /** + * Get the right target for listening to events on the page. + * - If the environment was initialized from a BrowsingContextTargetActor + * *and* if we're in the Browser Toolbox (to inspect Firefox Desktop): the + * targetActor is the RootActor, in which case, the window property can be + * used to listen to events. + * - With Firefox Desktop, the targetActor is a FrameTargetActor, and we use + * the chromeEventHandler which gives us a target we can use to listen to + * events, even from nested iframes. + * - If the environment was initialized from a window, we also use the + * chromeEventHandler. + */ + get pageListenerTarget() { + if (this._targetActor && this._targetActor.isRootActor) { + return this.window; + } + return this.docShell && this.docShell.chromeEventHandler; + }, + + relayTargetActorWindowReady: function(data) { + this.emit("window-ready", data); + }, + + relayTargetActorNavigate: function(data) { + this.emit("navigate", data); + }, + + relayTargetActorWillNavigate: function(data) { + this.emit("will-navigate", data); + }, + + destroy: function() { + if (this._targetActor) { + this._targetActor.off("window-ready", this.relayTargetActorWindowReady); + this._targetActor.off("navigate", this.relayTargetActorNavigate); + this._targetActor.off("will-navigate", this.relayTargetActorWillNavigate); + } + + // In case the environment was initialized from a window, we need to remove + // the progress listener. + if (this._win) { + try { + this.webProgress.removeProgressListener(this.listener); + } catch (e) { + // Which may fail in case the window was already destroyed. + } + } + + this._targetActor = null; + this._win = null; + }, +}; + +// This constant object is created to make the calls array more +// readable. Otherwise, linting rules force some array defs to span 4 +// lines instead, which is much harder to parse. +const HIGHLIGHTERS = { + accessible: "devtools/server/actors/highlighters/accessible", + boxModel: "devtools/server/actors/highlighters/box-model", + cssGrid: "devtools/server/actors/highlighters/css-grid", + cssTransform: "devtools/server/actors/highlighters/css-transform", + eyeDropper: "devtools/server/actors/highlighters/eye-dropper", + flexbox: "devtools/server/actors/highlighters/flexbox", + fonts: "devtools/server/actors/highlighters/fonts", + geometryEditor: "devtools/server/actors/highlighters/geometry-editor", + measuringTool: "devtools/server/actors/highlighters/measuring-tool", + pausedDebugger: "devtools/server/actors/highlighters/paused-debugger", + rulers: "devtools/server/actors/highlighters/rulers", + selector: "devtools/server/actors/highlighters/selector", + shapes: "devtools/server/actors/highlighters/shapes", + tabbingOrder: "devtools/server/actors/highlighters/tabbing-order", +}; + +// Each array in this array is called as register(arr[0], arr[1]). +const registerCalls = [ + ["AccessibleHighlighter", HIGHLIGHTERS.accessible], + ["BoxModelHighlighter", HIGHLIGHTERS.boxModel], + ["CssGridHighlighter", HIGHLIGHTERS.cssGrid], + ["CssTransformHighlighter", HIGHLIGHTERS.cssTransform], + ["EyeDropper", HIGHLIGHTERS.eyeDropper], + ["FlexboxHighlighter", HIGHLIGHTERS.flexbox], + ["FontsHighlighter", HIGHLIGHTERS.fonts], + ["GeometryEditorHighlighter", HIGHLIGHTERS.geometryEditor], + ["MeasuringToolHighlighter", HIGHLIGHTERS.measuringTool], + ["PausedDebuggerOverlay", HIGHLIGHTERS.pausedDebugger], + ["RulersHighlighter", HIGHLIGHTERS.rulers], + ["SelectorHighlighter", HIGHLIGHTERS.selector], + ["ShapesHighlighter", HIGHLIGHTERS.shapes], + ["TabbingOrderHighlighter", HIGHLIGHTERS.tabbingOrder], +]; + +// Register each highlighter above. +registerCalls.forEach(arr => { + registerHighlighter(arr[0], arr[1]); +}); |