/* 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 {Cu, Cc, Ci} = require("chrome"); const Services = require("Services"); const protocol = require("devtools/server/protocol"); const {Arg, Option, method} = protocol; const events = require("sdk/event/core"); // Make sure the domnode type is known here require("devtools/server/actors/inspector"); Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); // FIXME: add ":visited" and ":link" after bug 713106 is fixed const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted"; let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } "; HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } "; const XHTML_NS = "http://www.w3.org/1999/xhtml"; const HIGHLIGHTER_PICKED_TIMER = 1000; /** * The HighlighterActor is the server-side entry points for any tool that wishes * to highlight elements in the content document. * * The highlighter can be retrieved via the inspector's getHighlighter method. */ /** * The HighlighterActor class */ let HighlighterActor = protocol.ActorClass({ typeName: "highlighter", initialize: function(inspector) { protocol.Actor.prototype.initialize.call(this, null); this._inspector = inspector; this._walker = this._inspector.walker; this._tabActor = this._inspector.tabActor; if (this._supportsBoxModelHighlighter()) { this._boxModelHighlighter = new BoxModelHighlighter(this._tabActor); } else { this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor); } }, get conn() this._inspector && this._inspector.conn, /** * Can the host support the box model highlighter which requires a parent * XUL node to attach itself. */ _supportsBoxModelHighlighter: function() { return this._tabActor.browser && !!this._tabActor.browser.parentNode; }, destroy: function() { protocol.Actor.prototype.destroy.call(this); if (this._boxModelHighlighter) { this._boxModelHighlighter.destroy(); this._boxModelHighlighter = 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. The simple * outline highlighter for instance does not scrollIntoView */ showBoxModel: method(function(node, options={}) { if (node && this._isNodeValidForHighlighting(node.rawNode)) { this._boxModelHighlighter.show(node.rawNode, options); } else { this._boxModelHighlighter.hide(); } }, { request: { node: Arg(0, "domnode"), scrollIntoView: Option(1) } }), _isNodeValidForHighlighting: function(node) { // Is it null or dead? let isNotDead = node && !Cu.isDeadWrapper(node); // Is it connected to the document? let isConnected = false; try { let doc = node.ownerDocument; isConnected = (doc && doc.defaultView && doc.documentElement.contains(node)); } catch (e) { // "can't access dead object" error } // Is it an element node let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE; return isNotDead && isConnected && isElementNode; }, /** * Hide the box model highlighting if it was shown before */ hideBoxModel: method(function() { this._boxModelHighlighter.hide(); }, { request: {} }), /** * 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, pick: method(function() { if (this._isPicking) { return null; } this._isPicking = true; this._preventContentEvent = event => { event.stopPropagation(); event.preventDefault(); }; this._onPick = event => { this._preventContentEvent(event); this._stopPickerListeners(); this._isPicking = false; this._tabActor.window.setTimeout(() => { this._boxModelHighlighter.hide(); }, HIGHLIGHTER_PICKED_TIMER); events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event)); }; this._onHovered = event => { this._preventContentEvent(event); let res = this._findAndAttachElement(event); if (this._hoveredNode !== res.node) { this._boxModelHighlighter.show(res.node.rawNode); events.emit(this._walker, "picker-node-hovered", res); this._hoveredNode = res.node; } }; this._tabActor.window.focus(); this._startPickerListeners(); return null; }), _findAndAttachElement: function(event) { let doc = event.target.ownerDocument; let x = event.clientX; let y = event.clientY; let node = doc.elementFromPoint(x, y); return this._walker.attachElement(node); }, /** * Get the right target for listening to mouse events while in pick mode. * - On a firefox desktop content page: tabActor is a BrowserTabActor from * which the browser property will give us a target we can use to listen to * events, even in nested iframes. * - On B2G: tabActor is a ContentActor which doesn't have a browser but * since it overrides BrowserTabActor, it does get a browser property * anyway, which points to its window object. * - When using the Browser Toolbox (to inspect firefox desktop): tabActor is * the RootActor, in which case, the window property can be used to listen * to events */ _getPickerListenerTarget: function() { let actor = this._tabActor; return actor.isRootActor ? actor.window : actor.chromeEventHandler; }, _startPickerListeners: function() { let target = this._getPickerListenerTarget(); 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); }, _stopPickerListeners: function() { let target = this._getPickerListenerTarget(); 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); }, cancelPick: method(function() { if (this._isPicking) { this._boxModelHighlighter.hide(); this._stopPickerListeners(); this._isPicking = false; this._hoveredNode = null; } }) }); exports.HighlighterActor = HighlighterActor; /** * The HighlighterFront class */ let HighlighterFront = protocol.FrontClass(HighlighterActor, {}); /** * The BoxModelHighlighter is the class that actually draws the the box model * regions on top of a node. * It is used by the HighlighterActor. * * Usage example: * * let h = new BoxModelHighlighter(browser); * h.show(node); * h.hide(); * h.destroy(); * * Structure: * * * * * * * * * tagname#id.class1.class2 * * * * * */ function BoxModelHighlighter(tabActor) { this.browser = tabActor.browser; this.win = tabActor.window; this.chromeDoc = this.browser.ownerDocument; this.chromeWin = this.chromeDoc.defaultView; this.layoutHelpers = new LayoutHelpers(this.win); this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin); this.transitionDisabler = null; this.pageEventsMuter = null; this._update = this._update.bind(this); this.currentNode = null; this._initMarkup(); } BoxModelHighlighter.prototype = { _initMarkup: function() { let stack = this.browser.parentNode; this.highlighterContainer = this.chromeDoc.createElement("stack"); this.highlighterContainer.className = "highlighter-container"; this.outline = this.chromeDoc.createElement("box"); this.outline.className = "highlighter-outline"; let outlineContainer = this.chromeDoc.createElement("box"); outlineContainer.appendChild(this.outline); outlineContainer.className = "highlighter-outline-container"; this.highlighterContainer.appendChild(outlineContainer); let infobarContainer = this.chromeDoc.createElement("box"); infobarContainer.className = "highlighter-nodeinfobar-container"; this.highlighterContainer.appendChild(infobarContainer); // Insert the highlighter right after the browser stack.insertBefore(this.highlighterContainer, stack.childNodes[1]); // Building the infobar let infobarPositioner = this.chromeDoc.createElement("box"); infobarPositioner.className = "highlighter-nodeinfobar-positioner"; infobarPositioner.setAttribute("position", "top"); infobarPositioner.setAttribute("disabled", "true"); let nodeInfobar = this.chromeDoc.createElement("hbox"); nodeInfobar.className = "highlighter-nodeinfobar"; let arrowBoxTop = this.chromeDoc.createElement("box"); arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top"; let arrowBoxBottom = this.chromeDoc.createElement("box"); arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"; let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span"); tagNameLabel.className = "highlighter-nodeinfobar-tagname"; let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span"); idLabel.className = "highlighter-nodeinfobar-id"; let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); classesBox.className = "highlighter-nodeinfobar-classes"; let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span"); pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes"; // Add some content to force a better boundingClientRect pseudoClassesBox.textContent = " "; // let texthbox = this.chromeDoc.createElement("hbox"); texthbox.className = "highlighter-nodeinfobar-text"; texthbox.setAttribute("align", "center"); texthbox.setAttribute("flex", "1"); texthbox.appendChild(tagNameLabel); texthbox.appendChild(idLabel); texthbox.appendChild(classesBox); texthbox.appendChild(pseudoClassesBox); nodeInfobar.appendChild(texthbox); infobarPositioner.appendChild(arrowBoxTop); infobarPositioner.appendChild(nodeInfobar); infobarPositioner.appendChild(arrowBoxBottom); infobarContainer.appendChild(infobarPositioner); let barHeight = infobarPositioner.getBoundingClientRect().height; this.nodeInfo = { tagNameLabel: tagNameLabel, idLabel: idLabel, classesBox: classesBox, pseudoClassesBox: pseudoClassesBox, positioner: infobarPositioner, barHeight: barHeight, }; }, /** * Destroy the nodes. Remove listeners. */ destroy: function() { this.hide(); this.chromeWin.clearTimeout(this.transitionDisabler); this.chromeWin.clearTimeout(this.pageEventsMuter); this._contentRect = null; this._highlightRect = null; this.outline = null; this.nodeInfo = null; this.highlighterContainer.remove(); this.highlighterContainer = null; this.win = null this.browser = null; this.chromeDoc = null; this.chromeWin = null; this.currentNode = null; }, /** * Show the highlighter on a given node * * @param {DOMNode} node */ show: function(node, options={}) { if (!this.currentNode || node !== this.currentNode) { this.currentNode = node; this._showInfobar(); this._computeZoomFactor(); this._detachPageListeners(); this._attachPageListeners(); this._update(); this._trackMutations(); if (options.scrollIntoView) { this.chromeLayoutHelper.scrollIntoViewIfNeeded(node); } } }, _trackMutations: function() { if (this.currentNode) { let win = this.currentNode.ownerDocument.defaultView; this.currentNodeObserver = new win.MutationObserver(this._update); this.currentNodeObserver.observe(this.currentNode, {attributes: true}); } }, _untrackMutations: function() { if (this.currentNode) { if (this.currentNodeObserver) { // The following may fail with a "can't access dead object" exception // when the actor is being destroyed try { this.currentNodeObserver.disconnect(); } catch (e) {} this.currentNodeObserver = null; } } }, /** * Update the highlighter on the current highlighted node (the one that was * passed as an argument to show(node)). * Should be called whenever node size or attributes change * @param {Boolean} brieflyDisableTransitions * In case _update is called during scrolling or repaint, set this * to true to avoid transitions */ _update: function(brieflyDisableTransitions) { if (this.currentNode) { let rect = this.layoutHelpers.getDirtyRect(this.currentNode); if (this._highlightRectangle(rect, brieflyDisableTransitions)) { this._moveInfobar(); this._updateInfobar(); } else { // Nothing to highlight (0px rectangle like a