123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716 |
- /* 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 } = require("chrome");
- const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
- const EventEmitter = require("devtools/shared/event-emitter");
- const events = require("sdk/event/core");
- const protocol = require("devtools/shared/protocol");
- const Services = require("Services");
- const { isWindowIncluded } = require("devtools/shared/layout/utils");
- const { highlighterSpec, customHighlighterSpec } = require("devtools/shared/specs/highlighters");
- const { isXUL } = require("./highlighters/utils/markup");
- const { SimpleOutlineHighlighter } = require("./highlighters/simple-outline");
- const HIGHLIGHTER_PICKED_TIMER = 1000;
- const IS_OSX = Services.appinfo.OS === "Darwin";
- /**
- * The registration mechanism for highlighters provide a quick way to
- * have modular highlighters, instead of a hard coded list.
- * It allow us to split highlighers in sub modules, and add them dynamically
- * using add-on (useful for 3rd party developers, or prototyping)
- *
- * Note that currently, highlighters added using add-ons, can only work on
- * Firefox desktop, or Fennec if the same add-on is installed in both.
- */
- 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.
- * If no `typeName` is provided, is looking for a `typeName` property in
- * the prototype's constructor.
- */
- const register = (constructor, typeName = constructor.prototype.typeName) => {
- if (!typeName) {
- throw Error("No type's name found, or provided.");
- }
- if (highlighterTypes.has(typeName)) {
- throw Error(`${typeName} is already registered.`);
- }
- highlighterTypes.set(typeName, constructor);
- };
- exports.register = register;
- /**
- * The Highlighter is the server-side entry points for any tool that wishes to
- * highlight elements in some way in the content document.
- *
- * A little bit of vocabulary:
- * - <something>HighlighterActor classes are the actors that can be used from
- * the client. They do very little else than instantiate a given
- * <something>Highlighter and use it to highlight elements.
- * - <something>Highlighter classes aren't actors, they're just JS classes that
- * know how to create and attach the actual highlighter elements on top of the
- * content
- *
- * The most used highlighter actor is the HighlighterActor which can be
- * conveniently retrieved via the InspectorActor's 'getHighlighter' method.
- * The InspectorActor will always return the same instance of
- * HighlighterActor if asked several times and this instance is used in the
- * toolbox to highlighter elements's box-model from the markup-view,
- * box model view, console, debugger, ... as well as select elements with the
- * pointer (pick).
- *
- * Other types of highlighter actors exist and can be accessed via the
- * InspectorActor's 'getHighlighterByType' method.
- */
- /**
- * The HighlighterActor class
- */
- var HighlighterActor = exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
- initialize: function (inspector, autohide) {
- protocol.Actor.prototype.initialize.call(this, null);
- this._autohide = autohide;
- this._inspector = inspector;
- this._walker = this._inspector.walker;
- this._tabActor = this._inspector.tabActor;
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(this._tabActor);
- this._highlighterReady = this._highlighterReady.bind(this);
- this._highlighterHidden = this._highlighterHidden.bind(this);
- this._onNavigate = this._onNavigate.bind(this);
- let doc = this._tabActor.window.document;
- // Only try to create the highlighter when the document is loaded,
- // otherwise, wait for the navigate event to fire.
- if (doc.documentElement && doc.readyState != "uninitialized") {
- this._createHighlighter();
- }
- // Listen to navigation events to switch from the BoxModelHighlighter to the
- // SimpleOutlineHighlighter, and back, if the top level window changes.
- events.on(this._tabActor, "navigate", this._onNavigate);
- },
- get conn() {
- return this._inspector && this._inspector.conn;
- },
- form: function () {
- return {
- actor: this.actorID,
- traits: {
- autoHideOnDestroy: true
- }
- };
- },
- _createHighlighter: function () {
- this._isPreviousWindowXUL = isXUL(this._tabActor.window);
- if (!this._isPreviousWindowXUL) {
- this._highlighter = new BoxModelHighlighter(this._highlighterEnv,
- this._inspector);
- this._highlighter.on("ready", this._highlighterReady);
- this._highlighter.on("hide", this._highlighterHidden);
- } else {
- this._highlighter = new SimpleOutlineHighlighter(this._highlighterEnv);
- }
- },
- _destroyHighlighter: function () {
- if (this._highlighter) {
- if (!this._isPreviousWindowXUL) {
- this._highlighter.off("ready", this._highlighterReady);
- this._highlighter.off("hide", this._highlighterHidden);
- }
- this._highlighter.destroy();
- this._highlighter = null;
- }
- },
- _onNavigate: function ({isTopLevel}) {
- // Skip navigation events for non top-level windows, or if the document
- // doesn't exist anymore.
- if (!isTopLevel || !this._tabActor.window.document.documentElement) {
- return;
- }
- // Only rebuild the highlighter if the window type changed.
- if (isXUL(this._tabActor.window) !== this._isPreviousWindowXUL) {
- this._destroyHighlighter();
- this._createHighlighter();
- }
- },
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- this.hideBoxModel();
- this._destroyHighlighter();
- events.off(this._tabActor, "navigate", this._onNavigate);
- this._highlighterEnv.destroy();
- this._highlighterEnv = null;
- this._autohide = null;
- this._inspector = null;
- this._walker = null;
- this._tabActor = null;
- },
- /**
- * Display the box model highlighting on a given NodeActor.
- * There is only one instance of the box model highlighter, so calling this
- * method several times won't display several highlighters, it will just move
- * the highlighter instance to these nodes.
- *
- * @param NodeActor The node to be highlighted
- * @param Options See the request part for existing options. Note that not
- * all options may be supported by all types of highlighters.
- */
- showBoxModel: function (node, options = {}) {
- if (!node || !this._highlighter.show(node.rawNode, options)) {
- this._highlighter.hide();
- }
- },
- /**
- * Hide the box model highlighting if it was shown before
- */
- hideBoxModel: function () {
- if (this._highlighter) {
- this._highlighter.hide();
- }
- },
- /**
- * Returns `true` if the event was dispatched from a window included in
- * the current highlighter environment; or if the highlighter environment has
- * chrome privileges
- *
- * The method is specifically useful on B2G, where we do not want that events
- * from app or main process are processed if we're inspecting the content.
- *
- * @param {Event} event
- * The event to allow
- * @return {Boolean}
- */
- _isEventAllowed: function ({view}) {
- let { window } = this._highlighterEnv;
- return window instanceof Ci.nsIDOMChromeWindow ||
- isWindowIncluded(window, view);
- },
- /**
- * Pick a node on click, and highlight hovered nodes in the process.
- *
- * This method doesn't respond anything interesting, however, it starts
- * mousemove, and click listeners on the content document to fire
- * events and let connected clients know when nodes are hovered over or
- * clicked.
- *
- * Once a node is picked, events will cease, and listeners will be removed.
- */
- _isPicking: false,
- _hoveredNode: null,
- _currentNode: null,
- pick: function () {
- if (this._isPicking) {
- return null;
- }
- this._isPicking = true;
- this._preventContentEvent = event => {
- event.stopPropagation();
- event.preventDefault();
- };
- this._onPick = event => {
- this._preventContentEvent(event);
- if (!this._isEventAllowed(event)) {
- return;
- }
- // If shift is pressed, this is only a preview click, send the event to
- // the client, but don't stop picking.
- if (event.shiftKey) {
- events.emit(this._walker, "picker-node-previewed", this._findAndAttachElement(event));
- return;
- }
- this._stopPickerListeners();
- this._isPicking = false;
- if (this._autohide) {
- this._tabActor.window.setTimeout(() => {
- this._highlighter.hide();
- }, HIGHLIGHTER_PICKED_TIMER);
- }
- if (!this._currentNode) {
- this._currentNode = this._findAndAttachElement(event);
- }
- events.emit(this._walker, "picker-node-picked", this._currentNode);
- };
- this._onHovered = event => {
- this._preventContentEvent(event);
- if (!this._isEventAllowed(event)) {
- return;
- }
- this._currentNode = this._findAndAttachElement(event);
- if (this._hoveredNode !== this._currentNode.node) {
- this._highlighter.show(this._currentNode.node.rawNode);
- events.emit(this._walker, "picker-node-hovered", this._currentNode);
- this._hoveredNode = this._currentNode.node;
- }
- };
- this._onKey = event => {
- if (!this._currentNode || !this._isPicking) {
- return;
- }
- this._preventContentEvent(event);
- if (!this._isEventAllowed(event)) {
- return;
- }
- let currentNode = this._currentNode.node.rawNode;
- /**
- * KEY: Action/scope
- * LEFT_KEY: wider or parent
- * RIGHT_KEY: narrower or child
- * ENTER/CARRIAGE_RETURN: Picks currentNode
- * ESC/CTRL+SHIFT+C: Cancels picker, picks currentNode
- */
- switch (event.keyCode) {
- // Wider.
- case Ci.nsIDOMKeyEvent.DOM_VK_LEFT:
- if (!currentNode.parentElement) {
- return;
- }
- currentNode = currentNode.parentElement;
- break;
- // Narrower.
- case Ci.nsIDOMKeyEvent.DOM_VK_RIGHT:
- if (!currentNode.children.length) {
- return;
- }
- // Set firstElementChild by default
- let child = currentNode.firstElementChild;
- // If currentNode is parent of hoveredNode, then
- // previously selected childNode is set
- let hoveredNode = this._hoveredNode.rawNode;
- for (let sibling of currentNode.children) {
- if (sibling.contains(hoveredNode) || sibling === hoveredNode) {
- child = sibling;
- }
- }
- currentNode = child;
- break;
- // Select the element.
- case Ci.nsIDOMKeyEvent.DOM_VK_RETURN:
- this._onPick(event);
- return;
- // Cancel pick mode.
- case Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE:
- this.cancelPick();
- events.emit(this._walker, "picker-node-canceled");
- return;
- case Ci.nsIDOMKeyEvent.DOM_VK_C:
- if ((IS_OSX && event.metaKey && event.altKey) ||
- (!IS_OSX && event.ctrlKey && event.shiftKey)) {
- this.cancelPick();
- events.emit(this._walker, "picker-node-canceled");
- return;
- }
- default: return;
- }
- // Store currently attached element
- this._currentNode = this._walker.attachElement(currentNode);
- this._highlighter.show(this._currentNode.node.rawNode);
- events.emit(this._walker, "picker-node-hovered", this._currentNode);
- };
- this._startPickerListeners();
- return null;
- },
- /**
- * This pick method also focuses the highlighter's target window.
- */
- pickAndFocus: function() {
- // Go ahead and pass on the results to help future-proof this method.
- let pickResults = this.pick();
- this._highlighterEnv.window.focus();
- return pickResults;
- },
- _findAndAttachElement: function (event) {
- // originalTarget allows access to the "real" element before any retargeting
- // is applied, such as in the case of XBL anonymous elements. See also
- // https://developer.mozilla.org/docs/XBL/XBL_1.0_Reference/Anonymous_Content#Event_Flow_and_Targeting
- let node = event.originalTarget || event.target;
- return this._walker.attachElement(node);
- },
- _startPickerListeners: function () {
- let target = this._highlighterEnv.pageListenerTarget;
- target.addEventListener("mousemove", this._onHovered, true);
- target.addEventListener("click", this._onPick, true);
- target.addEventListener("mousedown", this._preventContentEvent, true);
- target.addEventListener("mouseup", this._preventContentEvent, true);
- target.addEventListener("dblclick", this._preventContentEvent, true);
- target.addEventListener("keydown", this._onKey, true);
- target.addEventListener("keyup", this._preventContentEvent, true);
- },
- _stopPickerListeners: function () {
- let target = this._highlighterEnv.pageListenerTarget;
- target.removeEventListener("mousemove", this._onHovered, true);
- target.removeEventListener("click", this._onPick, true);
- target.removeEventListener("mousedown", this._preventContentEvent, true);
- target.removeEventListener("mouseup", this._preventContentEvent, true);
- target.removeEventListener("dblclick", this._preventContentEvent, true);
- target.removeEventListener("keydown", this._onKey, true);
- target.removeEventListener("keyup", this._preventContentEvent, true);
- },
- _highlighterReady: function () {
- events.emit(this._inspector.walker, "highlighter-ready");
- },
- _highlighterHidden: function () {
- events.emit(this._inspector.walker, "highlighter-hide");
- },
- cancelPick: function () {
- if (this._isPicking) {
- this._highlighter.hide();
- this._stopPickerListeners();
- this._isPicking = false;
- this._hoveredNode = null;
- }
- }
- });
- /**
- * A generic highlighter actor class that instantiate a highlighter given its
- * type name and allows to show/hide it.
- */
- var CustomHighlighterActor = exports.CustomHighlighterActor = protocol.ActorClassWithSpec(customHighlighterSpec, {
- /**
- * Create a highlighter instance given its typename
- * The typename must be one of HIGHLIGHTER_CLASSES and the class must
- * implement constructor(tabActor), show(node), hide(), destroy()
- */
- initialize: function (inspector, typeName) {
- protocol.Actor.prototype.initialize.call(this, null);
- this._inspector = inspector;
- let constructor = highlighterTypes.get(typeName);
- if (!constructor) {
- let list = [...highlighterTypes.keys()];
- throw new Error(`${typeName} isn't a valid highlighter class (${list})`);
- }
- // The assumption is that all custom highlighters need the canvasframe
- // container to append their elements, so if this is a XUL window, bail out.
- if (!isXUL(this._inspector.tabActor.window)) {
- this._highlighterEnv = new HighlighterEnvironment();
- this._highlighterEnv.initFromTabActor(inspector.tabActor);
- this._highlighter = new constructor(this._highlighterEnv);
- } else {
- throw new Error("Custom " + typeName +
- "highlighter cannot be created in a XUL window");
- }
- },
- get conn() {
- return this._inspector && this._inspector.conn;
- },
- destroy: function () {
- protocol.Actor.prototype.destroy.call(this);
- this.finalize();
- this._inspector = null;
- },
- release: function () {},
- /**
- * 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 RectHighlighter for instance uses it to calculate the absolute
- * position of the provided rect. The SelectHighlighter 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
- * (FF41+)
- */
- show: function (node, options) {
- if (!node || !this._highlighter) {
- return false;
- }
- return this._highlighter.show(node.rawNode, options);
- },
- /**
- * Hide the highlighter if it was shown before
- */
- hide: function () {
- if (this._highlighter) {
- this._highlighter.hide();
- }
- },
- /**
- * Kill this actor. This method is called automatically just before the actor
- * is destroyed.
- */
- finalize: function () {
- if (this._highlighter) {
- 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" and "navigate" events, similarly to the
- * TabActor.
- *
- * It can be initialized either from a TabActor (which is the most frequent way
- * of using it, since highlighters are usually initialized by the
- * HighlighterActor or CustomHighlighterActor, which have a tabActor reference).
- * It can also be initialized just with a window object (which is useful for
- * when a highlighter is used outside of the debugger server context, for
- * instance from a gcli command).
- */
- function HighlighterEnvironment() {
- this.relayTabActorNavigate = this.relayTabActorNavigate.bind(this);
- this.relayTabActorWillNavigate = this.relayTabActorWillNavigate.bind(this);
- EventEmitter.decorate(this);
- }
- exports.HighlighterEnvironment = HighlighterEnvironment;
- HighlighterEnvironment.prototype = {
- initFromTabActor: function (tabActor) {
- this._tabActor = tabActor;
- events.on(this._tabActor, "navigate", this.relayTabActorNavigate);
- events.on(this._tabActor, "will-navigate", this.relayTabActorWillNavigate);
- },
- initFromWindow: function (win) {
- this._win = win;
- // We need a progress listener to know when the window will navigate/has
- // navigated.
- let self = this;
- this.listener = {
- QueryInterface: XPCOMUtils.generateQI([
- Ci.nsIWebProgressListener,
- Ci.nsISupportsWeakReference,
- Ci.nsISupports
- ]),
- onStateChange: function (progress, request, flag) {
- let isStart = flag & Ci.nsIWebProgressListener.STATE_START;
- let isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
- let isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;
- let 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_STATUS |
- Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
- Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
- },
- get isInitialized() {
- return this._win || this._tabActor;
- },
- get isXUL() {
- return isXUL(this.window);
- },
- get window() {
- if (!this.isInitialized) {
- throw new Error("Initialize HighlighterEnvironment with a tabActor " +
- "or window first");
- }
- return this._tabActor ? this._tabActor.window : this._win;
- },
- get document() {
- return this.window.document;
- },
- get docShell() {
- return this.window.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIWebNavigation)
- .QueryInterface(Ci.nsIDocShell);
- },
- get webProgress() {
- return 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 TabActor *and* if we're in the
- * Browser Toolbox (to inspect firefox desktop): the tabActor is the
- * RootActor, in which case, the window property can be used to listen to
- * events.
- * - With firefox desktop, that tabActor is a BrowserTabActor, and with B2G,
- * a ContentActor (which overrides BrowserTabActor). In both cases 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._tabActor && this._tabActor.isRootActor) {
- return this.window;
- }
- return this.docShell.chromeEventHandler;
- },
- relayTabActorNavigate: function (data) {
- this.emit("navigate", data);
- },
- relayTabActorWillNavigate: function (data) {
- this.emit("will-navigate", data);
- },
- destroy: function () {
- if (this._tabActor) {
- events.off(this._tabActor, "navigate", this.relayTabActorNavigate);
- events.off(this._tabActor, "will-navigate", this.relayTabActorWillNavigate);
- }
- // 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._tabActor = null;
- this._win = null;
- }
- };
- const { BoxModelHighlighter } = require("./highlighters/box-model");
- register(BoxModelHighlighter);
- exports.BoxModelHighlighter = BoxModelHighlighter;
- const { CssGridHighlighter } = require("./highlighters/css-grid");
- register(CssGridHighlighter);
- exports.CssGridHighlighter = CssGridHighlighter;
- const { CssTransformHighlighter } = require("./highlighters/css-transform");
- register(CssTransformHighlighter);
- exports.CssTransformHighlighter = CssTransformHighlighter;
- const { SelectorHighlighter } = require("./highlighters/selector");
- register(SelectorHighlighter);
- exports.SelectorHighlighter = SelectorHighlighter;
- const { RectHighlighter } = require("./highlighters/rect");
- register(RectHighlighter);
- exports.RectHighlighter = RectHighlighter;
- const { GeometryEditorHighlighter } = require("./highlighters/geometry-editor");
- register(GeometryEditorHighlighter);
- exports.GeometryEditorHighlighter = GeometryEditorHighlighter;
- const { RulersHighlighter } = require("./highlighters/rulers");
- register(RulersHighlighter);
- exports.RulersHighlighter = RulersHighlighter;
- const { MeasuringToolHighlighter } = require("./highlighters/measuring-tool");
- register(MeasuringToolHighlighter);
- exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
- const { EyeDropper } = require("./highlighters/eye-dropper");
- register(EyeDropper);
- exports.EyeDropper = EyeDropper;
|