2015-09-16 08:34:21 +03:00
|
|
|
/* 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";
|
|
|
|
|
2017-03-12 15:06:00 +03:00
|
|
|
const { Ci, Cu } = require("chrome");
|
2015-09-16 08:34:21 +03:00
|
|
|
|
2016-06-04 01:20:45 +03:00
|
|
|
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
2015-09-21 20:04:18 +03:00
|
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
2015-09-16 08:34:21 +03:00
|
|
|
const events = require("sdk/event/core");
|
2016-05-06 10:19:00 +03:00
|
|
|
const protocol = require("devtools/shared/protocol");
|
2016-06-14 06:57:00 +03:00
|
|
|
const Services = require("Services");
|
2015-09-21 20:04:18 +03:00
|
|
|
const { isWindowIncluded } = require("devtools/shared/layout/utils");
|
2016-05-11 12:09:46 +03:00
|
|
|
const { highlighterSpec, customHighlighterSpec } = require("devtools/shared/specs/highlighters");
|
2016-09-23 10:57:37 +03:00
|
|
|
const { isXUL } = require("./highlighters/utils/markup");
|
2015-09-16 08:34:21 +03:00
|
|
|
const { SimpleOutlineHighlighter } = require("./highlighters/simple-outline");
|
|
|
|
|
|
|
|
const HIGHLIGHTER_PICKED_TIMER = 1000;
|
2016-06-14 06:57:00 +03:00
|
|
|
const IS_OSX = Services.appinfo.OS === "Darwin";
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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,
|
2016-09-13 23:35:16 +03:00
|
|
|
* box model view, console, debugger, ... as well as select elements with the
|
2015-09-16 08:34:21 +03:00
|
|
|
* pointer (pick).
|
|
|
|
*
|
|
|
|
* Other types of highlighter actors exist and can be accessed via the
|
|
|
|
* InspectorActor's 'getHighlighterByType' method.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The HighlighterActor class
|
|
|
|
*/
|
2017-01-16 20:51:00 +03:00
|
|
|
exports.HighlighterActor = protocol.ActorClassWithSpec(highlighterSpec, {
|
2016-05-17 21:25:54 +03:00
|
|
|
initialize: function (inspector, autohide) {
|
2015-09-16 08:34:21 +03:00
|
|
|
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);
|
|
|
|
|
2016-10-19 15:19:48 +03:00
|
|
|
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();
|
|
|
|
}
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
// 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;
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
form: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
return {
|
|
|
|
actor: this.actorID,
|
|
|
|
traits: {
|
|
|
|
autoHideOnDestroy: true
|
|
|
|
}
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_createHighlighter: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_destroyHighlighter: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
if (this._highlighter) {
|
|
|
|
if (!this._isPreviousWindowXUL) {
|
|
|
|
this._highlighter.off("ready", this._highlighterReady);
|
|
|
|
this._highlighter.off("hide", this._highlighterHidden);
|
|
|
|
}
|
|
|
|
this._highlighter.destroy();
|
|
|
|
this._highlighter = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_onNavigate: function ({isTopLevel}) {
|
2015-09-16 08:34:21 +03:00
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
destroy: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
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.
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
showBoxModel: function (node, options = {}) {
|
2016-09-23 10:57:37 +03:00
|
|
|
if (!node || !this._highlighter.show(node.rawNode, options)) {
|
2015-09-16 08:34:21 +03:00
|
|
|
this._highlighter.hide();
|
|
|
|
}
|
2016-05-10 12:07:03 +03:00
|
|
|
},
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Hide the box model highlighting if it was shown before
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
hideBoxModel: function () {
|
2016-10-24 17:52:23 +03:00
|
|
|
if (this._highlighter) {
|
|
|
|
this._highlighter.hide();
|
|
|
|
}
|
2016-05-10 12:07:03 +03:00
|
|
|
},
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
_isEventAllowed: function ({view}) {
|
2015-09-16 08:34:21 +03:00
|
|
|
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,
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
pick: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2016-10-18 10:50:51 +03:00
|
|
|
// If shift is pressed, this is only a preview click, send the event to
|
|
|
|
// the client, but don't stop picking.
|
|
|
|
if (event.shiftKey) {
|
2017-01-16 20:51:00 +03:00
|
|
|
events.emit(this._walker, "picker-node-previewed",
|
|
|
|
this._findAndAttachElement(event));
|
2016-10-18 10:50:51 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-09-16 08:34:21 +03:00
|
|
|
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
|
2016-06-14 06:57:00 +03:00
|
|
|
* ESC/CTRL+SHIFT+C: Cancels picker, picks currentNode
|
2015-09-16 08:34:21 +03:00
|
|
|
*/
|
|
|
|
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;
|
2016-06-14 06:57:00 +03:00
|
|
|
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");
|
|
|
|
}
|
2017-01-16 20:51:00 +03:00
|
|
|
return;
|
2015-09-16 08:34:21 +03:00
|
|
|
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;
|
2016-05-10 12:07:03 +03:00
|
|
|
},
|
2015-09-16 08:34:21 +03:00
|
|
|
|
2016-10-06 21:40:53 +03:00
|
|
|
/**
|
|
|
|
* This pick method also focuses the highlighter's target window.
|
|
|
|
*/
|
2017-01-16 20:51:00 +03:00
|
|
|
pickAndFocus: function () {
|
2016-10-06 21:40:53 +03:00
|
|
|
// Go ahead and pass on the results to help future-proof this method.
|
|
|
|
let pickResults = this.pick();
|
|
|
|
this._highlighterEnv.window.focus();
|
|
|
|
return pickResults;
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_findAndAttachElement: function (event) {
|
2015-09-16 08:34:21 +03:00
|
|
|
// 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);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_startPickerListeners: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_stopPickerListeners: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
let target = this._highlighterEnv.pageListenerTarget;
|
2017-03-12 15:06:00 +03:00
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-09-16 08:34:21 +03:00
|
|
|
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);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_highlighterReady: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
events.emit(this._inspector.walker, "highlighter-ready");
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
_highlighterHidden: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
events.emit(this._inspector.walker, "highlighter-hide");
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
cancelPick: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
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.
|
|
|
|
*/
|
2017-01-16 20:51:00 +03:00
|
|
|
exports.CustomHighlighterActor = protocol.ActorClassWithSpec(customHighlighterSpec, {
|
2015-09-16 08:34:21 +03:00
|
|
|
/**
|
|
|
|
* 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()
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
initialize: function (inspector, typeName) {
|
2015-09-16 08:34:21 +03:00
|
|
|
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;
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
destroy: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
protocol.Actor.prototype.destroy.call(this);
|
|
|
|
this.finalize();
|
|
|
|
this._inspector = null;
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
release: function () {},
|
2016-03-17 17:59:03 +03:00
|
|
|
|
2015-09-16 08:34:21 +03:00
|
|
|
/**
|
|
|
|
* 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
|
2015-09-21 20:07:31 +03:00
|
|
|
* devtools/server/actor/inspector).
|
2015-09-16 08:34:21 +03:00
|
|
|
* Note however that some highlighters use this argument merely as a context
|
2017-03-13 23:35:50 +03:00
|
|
|
* node: The SelectHighlighter for instance uses it as a base node to run the
|
|
|
|
* provided CSS selector on.
|
2015-09-16 08:34:21 +03:00
|
|
|
*
|
|
|
|
* @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+)
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
show: function (node, options) {
|
2016-09-23 10:57:37 +03:00
|
|
|
if (!node || !this._highlighter) {
|
2015-09-16 08:34:21 +03:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this._highlighter.show(node.rawNode, options);
|
2016-05-11 12:09:46 +03:00
|
|
|
},
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Hide the highlighter if it was shown before
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
hide: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
if (this._highlighter) {
|
|
|
|
this._highlighter.hide();
|
|
|
|
}
|
2016-05-11 12:09:46 +03:00
|
|
|
},
|
2015-09-16 08:34:21 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Kill this actor. This method is called automatically just before the actor
|
|
|
|
* is destroyed.
|
|
|
|
*/
|
2016-05-17 21:25:54 +03:00
|
|
|
finalize: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
if (this._highlighter) {
|
|
|
|
this._highlighter.destroy();
|
|
|
|
this._highlighter = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._highlighterEnv) {
|
|
|
|
this._highlighterEnv.destroy();
|
|
|
|
this._highlighterEnv = null;
|
|
|
|
}
|
2016-05-11 12:09:46 +03:00
|
|
|
}
|
2015-09-16 08:34:21 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The HighlighterEnvironment is an object that holds all the required data for
|
|
|
|
* highlighters to work: the window, docShell, event listener target, ...
|
2016-11-22 18:21:24 +03:00
|
|
|
* It also emits "will-navigate", "navigate" and "window-ready" events,
|
|
|
|
* similarly to the TabActor.
|
2015-09-16 08:34:21 +03:00
|
|
|
*
|
|
|
|
* 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() {
|
2016-11-22 18:21:24 +03:00
|
|
|
this.relayTabActorWindowReady = this.relayTabActorWindowReady.bind(this);
|
2015-09-16 08:34:21 +03:00
|
|
|
this.relayTabActorNavigate = this.relayTabActorNavigate.bind(this);
|
|
|
|
this.relayTabActorWillNavigate = this.relayTabActorWillNavigate.bind(this);
|
|
|
|
|
|
|
|
EventEmitter.decorate(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.HighlighterEnvironment = HighlighterEnvironment;
|
|
|
|
|
|
|
|
HighlighterEnvironment.prototype = {
|
2016-05-17 21:25:54 +03:00
|
|
|
initFromTabActor: function (tabActor) {
|
2015-09-16 08:34:21 +03:00
|
|
|
this._tabActor = tabActor;
|
2016-11-22 18:21:24 +03:00
|
|
|
events.on(this._tabActor, "window-ready", this.relayTabActorWindowReady);
|
2015-09-16 08:34:21 +03:00
|
|
|
events.on(this._tabActor, "navigate", this.relayTabActorNavigate);
|
|
|
|
events.on(this._tabActor, "will-navigate", this.relayTabActorWillNavigate);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
initFromWindow: function (win) {
|
2015-09-16 08:34:21 +03:00
|
|
|
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
|
|
|
|
]),
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
onStateChange: function (progress, request, flag) {
|
2015-09-16 08:34:21 +03:00
|
|
|
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_STATE_WINDOW |
|
|
|
|
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
|
|
|
|
},
|
|
|
|
|
|
|
|
get isInitialized() {
|
|
|
|
return this._win || this._tabActor;
|
|
|
|
},
|
|
|
|
|
2016-08-22 15:59:08 +03:00
|
|
|
get isXUL() {
|
|
|
|
return isXUL(this.window);
|
|
|
|
},
|
|
|
|
|
2015-09-16 08:34:21 +03:00
|
|
|
get window() {
|
|
|
|
if (!this.isInitialized) {
|
|
|
|
throw new Error("Initialize HighlighterEnvironment with a tabActor " +
|
|
|
|
"or window first");
|
|
|
|
}
|
2017-03-12 15:06:00 +03:00
|
|
|
let win = this._tabActor ? this._tabActor.window : this._win;
|
|
|
|
|
|
|
|
return Cu.isDeadWrapper(win) ? null : win;
|
2015-09-16 08:34:21 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
get document() {
|
2017-03-12 15:06:00 +03:00
|
|
|
return this.window && this.window.document;
|
2015-09-16 08:34:21 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
get docShell() {
|
2017-03-12 15:06:00 +03:00
|
|
|
return this.window &&
|
|
|
|
this.window.QueryInterface(Ci.nsIInterfaceRequestor)
|
2015-09-16 08:34:21 +03:00
|
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
|
|
.QueryInterface(Ci.nsIDocShell);
|
|
|
|
},
|
|
|
|
|
|
|
|
get webProgress() {
|
2017-03-12 15:06:00 +03:00
|
|
|
return this.docShell &&
|
|
|
|
this.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
2015-09-16 08:34:21 +03:00
|
|
|
.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;
|
|
|
|
}
|
2017-03-12 15:06:00 +03:00
|
|
|
return this.docShell && this.docShell.chromeEventHandler;
|
2015-09-16 08:34:21 +03:00
|
|
|
},
|
|
|
|
|
2016-11-22 18:21:24 +03:00
|
|
|
relayTabActorWindowReady: function (data) {
|
|
|
|
this.emit("window-ready", data);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
relayTabActorNavigate: function (data) {
|
2015-09-16 08:34:21 +03:00
|
|
|
this.emit("navigate", data);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
relayTabActorWillNavigate: function (data) {
|
2015-09-16 08:34:21 +03:00
|
|
|
this.emit("will-navigate", data);
|
|
|
|
},
|
|
|
|
|
2016-05-17 21:25:54 +03:00
|
|
|
destroy: function () {
|
2015-09-16 08:34:21 +03:00
|
|
|
if (this._tabActor) {
|
2016-11-22 18:21:24 +03:00
|
|
|
events.off(this._tabActor, "window-ready", this.relayTabActorWindowReady);
|
2015-09-16 08:34:21 +03:00
|
|
|
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;
|
|
|
|
|
2016-08-19 20:26:39 +03:00
|
|
|
const { CssGridHighlighter } = require("./highlighters/css-grid");
|
|
|
|
register(CssGridHighlighter);
|
|
|
|
exports.CssGridHighlighter = CssGridHighlighter;
|
|
|
|
|
2015-09-16 08:34:21 +03:00
|
|
|
const { CssTransformHighlighter } = require("./highlighters/css-transform");
|
|
|
|
register(CssTransformHighlighter);
|
|
|
|
exports.CssTransformHighlighter = CssTransformHighlighter;
|
|
|
|
|
|
|
|
const { SelectorHighlighter } = require("./highlighters/selector");
|
|
|
|
register(SelectorHighlighter);
|
|
|
|
exports.SelectorHighlighter = SelectorHighlighter;
|
|
|
|
|
|
|
|
const { GeometryEditorHighlighter } = require("./highlighters/geometry-editor");
|
|
|
|
register(GeometryEditorHighlighter);
|
|
|
|
exports.GeometryEditorHighlighter = GeometryEditorHighlighter;
|
|
|
|
|
|
|
|
const { RulersHighlighter } = require("./highlighters/rulers");
|
|
|
|
register(RulersHighlighter);
|
|
|
|
exports.RulersHighlighter = RulersHighlighter;
|
2015-09-28 16:14:00 +03:00
|
|
|
|
|
|
|
const { MeasuringToolHighlighter } = require("./highlighters/measuring-tool");
|
|
|
|
register(MeasuringToolHighlighter);
|
|
|
|
exports.MeasuringToolHighlighter = MeasuringToolHighlighter;
|
2016-07-21 15:35:14 +03:00
|
|
|
|
|
|
|
const { EyeDropper } = require("./highlighters/eye-dropper");
|
|
|
|
register(EyeDropper);
|
|
|
|
exports.EyeDropper = EyeDropper;
|
2017-04-04 13:03:03 +03:00
|
|
|
|
|
|
|
const { PausedDebuggerOverlay } = require("./highlighters/paused-debugger");
|
|
|
|
register(PausedDebuggerOverlay);
|
|
|
|
exports.PausedDebuggerOverlay = PausedDebuggerOverlay;
|