toolkit/devtools/server/actors/highlighter.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/devtools/server/actors/highlighter.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,891 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +const {Cu, Cc, Ci} = require("chrome");
    1.11 +const Services = require("Services");
    1.12 +const protocol = require("devtools/server/protocol");
    1.13 +const {Arg, Option, method} = protocol;
    1.14 +const events = require("sdk/event/core");
    1.15 +
    1.16 +const EventEmitter = require("devtools/toolkit/event-emitter");
    1.17 +const GUIDE_STROKE_WIDTH = 1;
    1.18 +
    1.19 +// Make sure the domnode type is known here
    1.20 +require("devtools/server/actors/inspector");
    1.21 +
    1.22 +Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
    1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.24 +
    1.25 +// FIXME: add ":visited" and ":link" after bug 713106 is fixed
    1.26 +const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
    1.27 +const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
    1.28 +let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
    1.29 +HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
    1.30 +const XHTML_NS = "http://www.w3.org/1999/xhtml";
    1.31 +const SVG_NS = "http://www.w3.org/2000/svg";
    1.32 +const HIGHLIGHTER_PICKED_TIMER = 1000;
    1.33 +const INFO_BAR_OFFSET = 5;
    1.34 +
    1.35 +/**
    1.36 + * The HighlighterActor is the server-side entry points for any tool that wishes
    1.37 + * to highlight elements in the content document.
    1.38 + *
    1.39 + * The highlighter can be retrieved via the inspector's getHighlighter method.
    1.40 + */
    1.41 +
    1.42 +/**
    1.43 + * The HighlighterActor class
    1.44 + */
    1.45 +let HighlighterActor = protocol.ActorClass({
    1.46 +  typeName: "highlighter",
    1.47 +
    1.48 +  initialize: function(inspector, autohide) {
    1.49 +    protocol.Actor.prototype.initialize.call(this, null);
    1.50 +
    1.51 +    this._autohide = autohide;
    1.52 +    this._inspector = inspector;
    1.53 +    this._walker = this._inspector.walker;
    1.54 +    this._tabActor = this._inspector.tabActor;
    1.55 +
    1.56 +    this._highlighterReady = this._highlighterReady.bind(this);
    1.57 +    this._highlighterHidden = this._highlighterHidden.bind(this);
    1.58 +
    1.59 +    if (this._supportsBoxModelHighlighter()) {
    1.60 +      this._boxModelHighlighter =
    1.61 +        new BoxModelHighlighter(this._tabActor, this._inspector);
    1.62 +
    1.63 +        this._boxModelHighlighter.on("ready", this._highlighterReady);
    1.64 +        this._boxModelHighlighter.on("hide", this._highlighterHidden);
    1.65 +    } else {
    1.66 +      this._boxModelHighlighter = new SimpleOutlineHighlighter(this._tabActor);
    1.67 +    }
    1.68 +  },
    1.69 +
    1.70 +  get conn() this._inspector && this._inspector.conn,
    1.71 +
    1.72 +  /**
    1.73 +   * Can the host support the box model highlighter which requires a parent
    1.74 +   * XUL node to attach itself.
    1.75 +   */
    1.76 +  _supportsBoxModelHighlighter: function() {
    1.77 +    // Note that <browser>s on Fennec also have a XUL parentNode but the box
    1.78 +    // model highlighter doesn't display correctly on Fennec (bug 993190)
    1.79 +    return this._tabActor.browser &&
    1.80 +           !!this._tabActor.browser.parentNode &&
    1.81 +           Services.appinfo.ID !== "{aa3c5121-dab2-40e2-81ca-7ea25febc110}";
    1.82 +  },
    1.83 +
    1.84 +  destroy: function() {
    1.85 +    protocol.Actor.prototype.destroy.call(this);
    1.86 +    if (this._boxModelHighlighter) {
    1.87 +      this._boxModelHighlighter.off("ready", this._highlighterReady);
    1.88 +      this._boxModelHighlighter.off("hide", this._highlighterHidden);
    1.89 +      this._boxModelHighlighter.destroy();
    1.90 +      this._boxModelHighlighter = null;
    1.91 +    }
    1.92 +    this._autohide = null;
    1.93 +    this._inspector = null;
    1.94 +    this._walker = null;
    1.95 +    this._tabActor = null;
    1.96 +  },
    1.97 +
    1.98 +  /**
    1.99 +   * Display the box model highlighting on a given NodeActor.
   1.100 +   * There is only one instance of the box model highlighter, so calling this
   1.101 +   * method several times won't display several highlighters, it will just move
   1.102 +   * the highlighter instance to these nodes.
   1.103 +   *
   1.104 +   * @param NodeActor The node to be highlighted
   1.105 +   * @param Options See the request part for existing options. Note that not
   1.106 +   * all options may be supported by all types of highlighters.
   1.107 +   */
   1.108 +  showBoxModel: method(function(node, options={}) {
   1.109 +    if (node && this._isNodeValidForHighlighting(node.rawNode)) {
   1.110 +      this._boxModelHighlighter.show(node.rawNode, options);
   1.111 +    } else {
   1.112 +      this._boxModelHighlighter.hide();
   1.113 +    }
   1.114 +  }, {
   1.115 +    request: {
   1.116 +      node: Arg(0, "domnode"),
   1.117 +      region: Option(1)
   1.118 +    }
   1.119 +  }),
   1.120 +
   1.121 +  _isNodeValidForHighlighting: function(node) {
   1.122 +    // Is it null or dead?
   1.123 +    let isNotDead = node && !Cu.isDeadWrapper(node);
   1.124 +
   1.125 +    // Is it connected to the document?
   1.126 +    let isConnected = false;
   1.127 +    try {
   1.128 +      let doc = node.ownerDocument;
   1.129 +      isConnected = (doc && doc.defaultView && doc.documentElement.contains(node));
   1.130 +    } catch (e) {
   1.131 +      // "can't access dead object" error
   1.132 +    }
   1.133 +
   1.134 +    // Is it an element node
   1.135 +    let isElementNode = node.nodeType === Ci.nsIDOMNode.ELEMENT_NODE;
   1.136 +
   1.137 +    return isNotDead && isConnected && isElementNode;
   1.138 +  },
   1.139 +
   1.140 +  /**
   1.141 +   * Hide the box model highlighting if it was shown before
   1.142 +   */
   1.143 +  hideBoxModel: method(function() {
   1.144 +    this._boxModelHighlighter.hide();
   1.145 +  }, {
   1.146 +    request: {}
   1.147 +  }),
   1.148 +
   1.149 +  /**
   1.150 +   * Pick a node on click, and highlight hovered nodes in the process.
   1.151 +   *
   1.152 +   * This method doesn't respond anything interesting, however, it starts
   1.153 +   * mousemove, and click listeners on the content document to fire
   1.154 +   * events and let connected clients know when nodes are hovered over or
   1.155 +   * clicked.
   1.156 +   *
   1.157 +   * Once a node is picked, events will cease, and listeners will be removed.
   1.158 +   */
   1.159 +  _isPicking: false,
   1.160 +  _hoveredNode: null,
   1.161 +
   1.162 +  pick: method(function() {
   1.163 +    if (this._isPicking) {
   1.164 +      return null;
   1.165 +    }
   1.166 +    this._isPicking = true;
   1.167 +
   1.168 +    this._preventContentEvent = event => {
   1.169 +      event.stopPropagation();
   1.170 +      event.preventDefault();
   1.171 +    };
   1.172 +
   1.173 +    this._onPick = event => {
   1.174 +      this._preventContentEvent(event);
   1.175 +      this._stopPickerListeners();
   1.176 +      this._isPicking = false;
   1.177 +      if (this._autohide) {
   1.178 +        this._tabActor.window.setTimeout(() => {
   1.179 +          this._boxModelHighlighter.hide();
   1.180 +        }, HIGHLIGHTER_PICKED_TIMER);
   1.181 +      }
   1.182 +      events.emit(this._walker, "picker-node-picked", this._findAndAttachElement(event));
   1.183 +    };
   1.184 +
   1.185 +    this._onHovered = event => {
   1.186 +      this._preventContentEvent(event);
   1.187 +      let res = this._findAndAttachElement(event);
   1.188 +      if (this._hoveredNode !== res.node) {
   1.189 +        this._boxModelHighlighter.show(res.node.rawNode);
   1.190 +        events.emit(this._walker, "picker-node-hovered", res);
   1.191 +        this._hoveredNode = res.node;
   1.192 +      }
   1.193 +    };
   1.194 +
   1.195 +    this._tabActor.window.focus();
   1.196 +    this._startPickerListeners();
   1.197 +
   1.198 +    return null;
   1.199 +  }),
   1.200 +
   1.201 +  _findAndAttachElement: function(event) {
   1.202 +    let doc = event.target.ownerDocument;
   1.203 +
   1.204 +    let x = event.clientX;
   1.205 +    let y = event.clientY;
   1.206 +
   1.207 +    let node = doc.elementFromPoint(x, y);
   1.208 +    return this._walker.attachElement(node);
   1.209 +  },
   1.210 +
   1.211 +  /**
   1.212 +   * Get the right target for listening to mouse events while in pick mode.
   1.213 +   * - On a firefox desktop content page: tabActor is a BrowserTabActor from
   1.214 +   *   which the browser property will give us a target we can use to listen to
   1.215 +   *   events, even in nested iframes.
   1.216 +   * - On B2G: tabActor is a ContentActor which doesn't have a browser but
   1.217 +   *   since it overrides BrowserTabActor, it does get a browser property
   1.218 +   *   anyway, which points to its window object.
   1.219 +   * - When using the Browser Toolbox (to inspect firefox desktop): tabActor is
   1.220 +   *   the RootActor, in which case, the window property can be used to listen
   1.221 +   *   to events
   1.222 +   */
   1.223 +  _getPickerListenerTarget: function() {
   1.224 +    let actor = this._tabActor;
   1.225 +    return actor.isRootActor ? actor.window : actor.chromeEventHandler;
   1.226 +  },
   1.227 +
   1.228 +  _startPickerListeners: function() {
   1.229 +    let target = this._getPickerListenerTarget();
   1.230 +    target.addEventListener("mousemove", this._onHovered, true);
   1.231 +    target.addEventListener("click", this._onPick, true);
   1.232 +    target.addEventListener("mousedown", this._preventContentEvent, true);
   1.233 +    target.addEventListener("mouseup", this._preventContentEvent, true);
   1.234 +    target.addEventListener("dblclick", this._preventContentEvent, true);
   1.235 +  },
   1.236 +
   1.237 +  _stopPickerListeners: function() {
   1.238 +    let target = this._getPickerListenerTarget();
   1.239 +    target.removeEventListener("mousemove", this._onHovered, true);
   1.240 +    target.removeEventListener("click", this._onPick, true);
   1.241 +    target.removeEventListener("mousedown", this._preventContentEvent, true);
   1.242 +    target.removeEventListener("mouseup", this._preventContentEvent, true);
   1.243 +    target.removeEventListener("dblclick", this._preventContentEvent, true);
   1.244 +  },
   1.245 +
   1.246 +  _highlighterReady: function() {
   1.247 +    events.emit(this._inspector.walker, "highlighter-ready");
   1.248 +  },
   1.249 +
   1.250 +  _highlighterHidden: function() {
   1.251 +    events.emit(this._inspector.walker, "highlighter-hide");
   1.252 +  },
   1.253 +
   1.254 +  cancelPick: method(function() {
   1.255 +    if (this._isPicking) {
   1.256 +      this._boxModelHighlighter.hide();
   1.257 +      this._stopPickerListeners();
   1.258 +      this._isPicking = false;
   1.259 +      this._hoveredNode = null;
   1.260 +    }
   1.261 +  })
   1.262 +});
   1.263 +
   1.264 +exports.HighlighterActor = HighlighterActor;
   1.265 +
   1.266 +/**
   1.267 + * The HighlighterFront class
   1.268 + */
   1.269 +let HighlighterFront = protocol.FrontClass(HighlighterActor, {});
   1.270 +
   1.271 +/**
   1.272 + * The BoxModelHighlighter is the class that actually draws the the box model
   1.273 + * regions on top of a node.
   1.274 + * It is used by the HighlighterActor.
   1.275 + *
   1.276 + * Usage example:
   1.277 + *
   1.278 + * let h = new BoxModelHighlighter(browser);
   1.279 + * h.show(node);
   1.280 + * h.hide();
   1.281 + * h.destroy();
   1.282 + *
   1.283 + * Structure:
   1.284 + * <stack class="highlighter-container">
   1.285 + *   <svg class="box-model-root" hidden="true">
   1.286 + *     <g class="box-model-container">
   1.287 + *       <polygon class="box-model-margin" points="317,122 747,36 747,181 317,267" />
   1.288 + *       <polygon class="box-model-border" points="317,128 747,42 747,161 317,247" />
   1.289 + *       <polygon class="box-model-padding" points="323,127 747,42 747,161 323,246" />
   1.290 + *       <polygon class="box-model-content" points="335,137 735,57 735,152 335,232" />
   1.291 + *     </g>
   1.292 + *     <line class="box-model-guide-top" x1="0" y1="592" x2="99999" y2="592" />
   1.293 + *     <line class="box-model-guide-right" x1="735" y1="0" x2="735" y2="99999" />
   1.294 + *     <line class="box-model-guide-bottom" x1="0" y1="612" x2="99999" y2="612" />
   1.295 + *     <line class="box-model-guide-left" x1="334" y1="0" x2="334" y2="99999" />
   1.296 + *   </svg>
   1.297 + *   <box class="highlighter-nodeinfobar-container">
   1.298 + *     <box class="highlighter-nodeinfobar-positioner" position="top" />
   1.299 + *       <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top" />
   1.300 + *       <hbox class="highlighter-nodeinfobar">
   1.301 + *         <hbox class="highlighter-nodeinfobar-text" align="center" flex="1">
   1.302 + *           <span class="highlighter-nodeinfobar-tagname">Node name</span>
   1.303 + *           <span class="highlighter-nodeinfobar-id">Node id</span>
   1.304 + *           <span class="highlighter-nodeinfobar-classes">.someClass</span>
   1.305 + *           <span class="highlighter-nodeinfobar-pseudo-classes">:hover</span>
   1.306 + *         </hbox>
   1.307 + *       </hbox>
   1.308 + *       <box class="highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom"/>
   1.309 + *     </box>
   1.310 + *   </box>
   1.311 + * </stack>
   1.312 + */
   1.313 +function BoxModelHighlighter(tabActor, inspector) {
   1.314 +  this.browser = tabActor.browser;
   1.315 +  this.win = tabActor.window;
   1.316 +  this.chromeDoc = this.browser.ownerDocument;
   1.317 +  this.chromeWin = this.chromeDoc.defaultView;
   1.318 +  this._inspector = inspector;
   1.319 +
   1.320 +  this.layoutHelpers = new LayoutHelpers(this.win);
   1.321 +  this.chromeLayoutHelper = new LayoutHelpers(this.chromeWin);
   1.322 +
   1.323 +  this.transitionDisabler = null;
   1.324 +  this.pageEventsMuter = null;
   1.325 +  this._update = this._update.bind(this);
   1.326 +  this.handleEvent = this.handleEvent.bind(this);
   1.327 +  this.currentNode = null;
   1.328 +
   1.329 +  EventEmitter.decorate(this);
   1.330 +  this._initMarkup();
   1.331 +}
   1.332 +
   1.333 +BoxModelHighlighter.prototype = {
   1.334 +  get zoom() {
   1.335 +    return this.win.QueryInterface(Ci.nsIInterfaceRequestor)
   1.336 +               .getInterface(Ci.nsIDOMWindowUtils).fullZoom;
   1.337 +  },
   1.338 +
   1.339 +  _initMarkup: function() {
   1.340 +    let stack = this.browser.parentNode;
   1.341 +
   1.342 +    this._highlighterContainer = this.chromeDoc.createElement("stack");
   1.343 +    this._highlighterContainer.className = "highlighter-container";
   1.344 +
   1.345 +    this._svgRoot = this._createSVGNode("root", "svg", this._highlighterContainer);
   1.346 +
   1.347 +    // Set the SVG canvas height to 0 to stop content jumping around on small
   1.348 +    // screens.
   1.349 +    this._svgRoot.setAttribute("height", "0");
   1.350 +
   1.351 +    this._boxModelContainer = this._createSVGNode("container", "g", this._svgRoot);
   1.352 +
   1.353 +    this._boxModelNodes = {
   1.354 +      margin: this._createSVGNode("margin", "polygon", this._boxModelContainer),
   1.355 +      border: this._createSVGNode("border", "polygon", this._boxModelContainer),
   1.356 +      padding: this._createSVGNode("padding", "polygon", this._boxModelContainer),
   1.357 +      content: this._createSVGNode("content", "polygon", this._boxModelContainer)
   1.358 +    };
   1.359 +
   1.360 +    this._guideNodes = {
   1.361 +      top: this._createSVGNode("guide-top", "line", this._svgRoot),
   1.362 +      right: this._createSVGNode("guide-right", "line", this._svgRoot),
   1.363 +      bottom: this._createSVGNode("guide-bottom", "line", this._svgRoot),
   1.364 +      left: this._createSVGNode("guide-left", "line", this._svgRoot)
   1.365 +    };
   1.366 +
   1.367 +    this._guideNodes.top.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
   1.368 +    this._guideNodes.right.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
   1.369 +    this._guideNodes.bottom.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
   1.370 +    this._guideNodes.left.setAttribute("stroke-width", GUIDE_STROKE_WIDTH);
   1.371 +
   1.372 +    this._highlighterContainer.appendChild(this._svgRoot);
   1.373 +
   1.374 +    let infobarContainer = this.chromeDoc.createElement("box");
   1.375 +    infobarContainer.className = "highlighter-nodeinfobar-container";
   1.376 +    this._highlighterContainer.appendChild(infobarContainer);
   1.377 +
   1.378 +    // Insert the highlighter right after the browser
   1.379 +    stack.insertBefore(this._highlighterContainer, stack.childNodes[1]);
   1.380 +
   1.381 +    // Building the infobar
   1.382 +    let infobarPositioner = this.chromeDoc.createElement("box");
   1.383 +    infobarPositioner.className = "highlighter-nodeinfobar-positioner";
   1.384 +    infobarPositioner.setAttribute("position", "top");
   1.385 +    infobarPositioner.setAttribute("disabled", "true");
   1.386 +
   1.387 +    let nodeInfobar = this.chromeDoc.createElement("hbox");
   1.388 +    nodeInfobar.className = "highlighter-nodeinfobar";
   1.389 +
   1.390 +    let arrowBoxTop = this.chromeDoc.createElement("box");
   1.391 +    arrowBoxTop.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-top";
   1.392 +
   1.393 +    let arrowBoxBottom = this.chromeDoc.createElement("box");
   1.394 +    arrowBoxBottom.className = "highlighter-nodeinfobar-arrow highlighter-nodeinfobar-arrow-bottom";
   1.395 +
   1.396 +    let tagNameLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
   1.397 +    tagNameLabel.className = "highlighter-nodeinfobar-tagname";
   1.398 +
   1.399 +    let idLabel = this.chromeDoc.createElementNS(XHTML_NS, "span");
   1.400 +    idLabel.className = "highlighter-nodeinfobar-id";
   1.401 +
   1.402 +    let classesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
   1.403 +    classesBox.className = "highlighter-nodeinfobar-classes";
   1.404 +
   1.405 +    let pseudoClassesBox = this.chromeDoc.createElementNS(XHTML_NS, "span");
   1.406 +    pseudoClassesBox.className = "highlighter-nodeinfobar-pseudo-classes";
   1.407 +
   1.408 +    // Add some content to force a better boundingClientRect
   1.409 +    pseudoClassesBox.textContent = "&nbsp;";
   1.410 +
   1.411 +    // <hbox class="highlighter-nodeinfobar-text"/>
   1.412 +    let texthbox = this.chromeDoc.createElement("hbox");
   1.413 +    texthbox.className = "highlighter-nodeinfobar-text";
   1.414 +    texthbox.setAttribute("align", "center");
   1.415 +    texthbox.setAttribute("flex", "1");
   1.416 +
   1.417 +    texthbox.appendChild(tagNameLabel);
   1.418 +    texthbox.appendChild(idLabel);
   1.419 +    texthbox.appendChild(classesBox);
   1.420 +    texthbox.appendChild(pseudoClassesBox);
   1.421 +
   1.422 +    nodeInfobar.appendChild(texthbox);
   1.423 +
   1.424 +    infobarPositioner.appendChild(arrowBoxTop);
   1.425 +    infobarPositioner.appendChild(nodeInfobar);
   1.426 +    infobarPositioner.appendChild(arrowBoxBottom);
   1.427 +
   1.428 +    infobarContainer.appendChild(infobarPositioner);
   1.429 +
   1.430 +    let barHeight = infobarPositioner.getBoundingClientRect().height;
   1.431 +
   1.432 +    this.nodeInfo = {
   1.433 +      tagNameLabel: tagNameLabel,
   1.434 +      idLabel: idLabel,
   1.435 +      classesBox: classesBox,
   1.436 +      pseudoClassesBox: pseudoClassesBox,
   1.437 +      positioner: infobarPositioner,
   1.438 +      barHeight: barHeight,
   1.439 +    };
   1.440 +  },
   1.441 +
   1.442 +  _createSVGNode: function(classPostfix, nodeType, parent) {
   1.443 +    let node = this.chromeDoc.createElementNS(SVG_NS, nodeType);
   1.444 +    node.setAttribute("class", "box-model-" + classPostfix);
   1.445 +
   1.446 +    parent.appendChild(node);
   1.447 +
   1.448 +    return node;
   1.449 +  },
   1.450 +
   1.451 +  /**
   1.452 +   * Destroy the nodes. Remove listeners.
   1.453 +   */
   1.454 +  destroy: function() {
   1.455 +    this.hide();
   1.456 +
   1.457 +    this.chromeWin.clearTimeout(this.transitionDisabler);
   1.458 +    this.chromeWin.clearTimeout(this.pageEventsMuter);
   1.459 +
   1.460 +    this.nodeInfo = null;
   1.461 +
   1.462 +    this._highlighterContainer.remove();
   1.463 +    this._highlighterContainer = null;
   1.464 +
   1.465 +    this.rect = null;
   1.466 +    this.win = null;
   1.467 +    this.browser = null;
   1.468 +    this.chromeDoc = null;
   1.469 +    this.chromeWin = null;
   1.470 +    this.currentNode = null;
   1.471 +  },
   1.472 +
   1.473 +  /**
   1.474 +   * Show the highlighter on a given node
   1.475 +   *
   1.476 +   * @param {DOMNode} node
   1.477 +   * @param {Object} options
   1.478 +   *        Object used for passing options
   1.479 +   */
   1.480 +  show: function(node, options={}) {
   1.481 +    this.currentNode = node;
   1.482 +
   1.483 +    this._showInfobar();
   1.484 +    this._detachPageListeners();
   1.485 +    this._attachPageListeners();
   1.486 +    this._update();
   1.487 +    this._trackMutations();
   1.488 +  },
   1.489 +
   1.490 +  _trackMutations: function() {
   1.491 +    if (this.currentNode) {
   1.492 +      let win = this.currentNode.ownerDocument.defaultView;
   1.493 +      this.currentNodeObserver = new win.MutationObserver(() => {
   1.494 +        this._update();
   1.495 +      });
   1.496 +      this.currentNodeObserver.observe(this.currentNode, {attributes: true});
   1.497 +    }
   1.498 +  },
   1.499 +
   1.500 +  _untrackMutations: function() {
   1.501 +    if (this.currentNode) {
   1.502 +      if (this.currentNodeObserver) {
   1.503 +        // The following may fail with a "can't access dead object" exception
   1.504 +        // when the actor is being destroyed
   1.505 +        try {
   1.506 +          this.currentNodeObserver.disconnect();
   1.507 +        } catch (e) {}
   1.508 +        this.currentNodeObserver = null;
   1.509 +      }
   1.510 +    }
   1.511 +  },
   1.512 +
   1.513 +  /**
   1.514 +   * Update the highlighter on the current highlighted node (the one that was
   1.515 +   * passed as an argument to show(node)).
   1.516 +   * Should be called whenever node size or attributes change
   1.517 +   * @param {Object} options
   1.518 +   *        Object used for passing options. Valid options are:
   1.519 +   *          - box: "content", "padding", "border" or "margin." This specifies
   1.520 +   *            the box that the guides should outline. Default is content.
   1.521 +   */
   1.522 +  _update: function(options={}) {
   1.523 +    if (this.currentNode) {
   1.524 +      if (this._highlightBoxModel(options)) {
   1.525 +        this._showInfobar();
   1.526 +      } else {
   1.527 +        // Nothing to highlight (0px rectangle like a <script> tag for instance)
   1.528 +        this.hide();
   1.529 +      }
   1.530 +      this.emit("ready");
   1.531 +    }
   1.532 +  },
   1.533 +
   1.534 +  /**
   1.535 +   * Hide the highlighter, the outline and the infobar.
   1.536 +   */
   1.537 +  hide: function() {
   1.538 +    if (this.currentNode) {
   1.539 +      this._untrackMutations();
   1.540 +      this.currentNode = null;
   1.541 +      this._hideBoxModel();
   1.542 +      this._hideInfobar();
   1.543 +      this._detachPageListeners();
   1.544 +    }
   1.545 +    this.emit("hide");
   1.546 +  },
   1.547 +
   1.548 +  /**
   1.549 +   * Hide the infobar
   1.550 +   */
   1.551 +  _hideInfobar: function() {
   1.552 +    this.nodeInfo.positioner.setAttribute("hidden", "true");
   1.553 +  },
   1.554 +
   1.555 +  /**
   1.556 +   * Show the infobar
   1.557 +   */
   1.558 +  _showInfobar: function() {
   1.559 +    this.nodeInfo.positioner.removeAttribute("hidden");
   1.560 +    this._updateInfobar();
   1.561 +  },
   1.562 +
   1.563 +  /**
   1.564 +   * Hide the box model
   1.565 +   */
   1.566 +  _hideBoxModel: function() {
   1.567 +    this._svgRoot.setAttribute("hidden", "true");
   1.568 +  },
   1.569 +
   1.570 +  /**
   1.571 +   * Show the box model
   1.572 +   */
   1.573 +  _showBoxModel: function() {
   1.574 +    this._svgRoot.removeAttribute("hidden");
   1.575 +  },
   1.576 +
   1.577 +  /**
   1.578 +   * Highlight the box model.
   1.579 +   *
   1.580 +   * @param {Object} options
   1.581 +   *        Object used for passing options. Valid options are:
   1.582 +   *          - region: "content", "padding", "border" or "margin." This specifies
   1.583 +   *            the region that the guides should outline. Default is content.
   1.584 +   * @return {boolean}
   1.585 +   *         True if the rectangle was highlighted, false otherwise.
   1.586 +   */
   1.587 +  _highlightBoxModel: function(options) {
   1.588 +    let isShown = false;
   1.589 +
   1.590 +    options.region = options.region || "content";
   1.591 +
   1.592 +    // TODO: Remove this polyfill
   1.593 +    this.rect =
   1.594 +      this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, "margin");
   1.595 +
   1.596 +    if (!this.rect) {
   1.597 +      return null;
   1.598 +    }
   1.599 +
   1.600 +    if (this.rect.bounds.width > 0 && this.rect.bounds.height > 0) {
   1.601 +      for (let boxType in this._boxModelNodes) {
   1.602 +        // TODO: Remove this polyfill
   1.603 +        let {p1, p2, p3, p4} = boxType === "margin" ? this.rect :
   1.604 +          this.layoutHelpers.getAdjustedQuadsPolyfill(this.currentNode, boxType);
   1.605 +
   1.606 +        let boxNode = this._boxModelNodes[boxType];
   1.607 +        boxNode.setAttribute("points",
   1.608 +                             p1.x + "," + p1.y + " " +
   1.609 +                             p2.x + "," + p2.y + " " +
   1.610 +                             p3.x + "," + p3.y + " " +
   1.611 +                             p4.x + "," + p4.y);
   1.612 +
   1.613 +        if (boxType === options.region) {
   1.614 +          this._showGuides(p1, p2, p3, p4);
   1.615 +        }
   1.616 +      }
   1.617 +
   1.618 +      isShown = true;
   1.619 +      this._showBoxModel();
   1.620 +    } else {
   1.621 +      // Only return false if the element really is invisible.
   1.622 +      // A height of 0 and a non-0 width corresponds to a visible element that
   1.623 +      // is below the fold for instance
   1.624 +      if (this.rect.width > 0 || this.rect.height > 0) {
   1.625 +        isShown = true;
   1.626 +        this._hideBoxModel();
   1.627 +      }
   1.628 +    }
   1.629 +    return isShown;
   1.630 +  },
   1.631 +
   1.632 +  /**
   1.633 +   * We only want to show guides for horizontal and vertical edges as this helps
   1.634 +   * to line them up. This method finds these edges and displays a guide there.
   1.635 +   *
   1.636 +   * @param  {DOMPoint} p1
   1.637 +   *                    Point 1
   1.638 +   * @param  {DOMPoint} p2
   1.639 +   *                    Point 2
   1.640 +   * @param  {DOMPoint} p3 [description]
   1.641 +   *                    Point 3
   1.642 +   * @param  {DOMPoint} p4 [description]
   1.643 +   *                    Point 4
   1.644 +   */
   1.645 +  _showGuides: function(p1, p2, p3, p4) {
   1.646 +    let allX = [p1.x, p2.x, p3.x, p4.x].sort();
   1.647 +    let allY = [p1.y, p2.y, p3.y, p4.y].sort();
   1.648 +    let toShowX = [];
   1.649 +    let toShowY = [];
   1.650 +
   1.651 +    for (let arr of [allX, allY]) {
   1.652 +      for (let i = 0; i < arr.length; i++) {
   1.653 +        let val = arr[i];
   1.654 +
   1.655 +        if (i !== arr.lastIndexOf(val)) {
   1.656 +          if (arr === allX) {
   1.657 +            toShowX.push(val);
   1.658 +          } else {
   1.659 +            toShowY.push(val);
   1.660 +          }
   1.661 +          arr.splice(arr.lastIndexOf(val), 1);
   1.662 +        }
   1.663 +      }
   1.664 +    }
   1.665 +
   1.666 +    // Move guide into place or hide it if no valid co-ordinate was found.
   1.667 +    this._updateGuide(this._guideNodes.top, toShowY[0]);
   1.668 +    this._updateGuide(this._guideNodes.right, toShowX[1]);
   1.669 +    this._updateGuide(this._guideNodes.bottom, toShowY[1]);
   1.670 +    this._updateGuide(this._guideNodes.left, toShowX[0]);
   1.671 +  },
   1.672 +
   1.673 +  /**
   1.674 +   * Move a guide to the appropriate position and display it. If no point is
   1.675 +   * passed then the guide is hidden.
   1.676 +   *
   1.677 +   * @param  {SVGLine} guide
   1.678 +   *         The guide to update
   1.679 +   * @param  {Integer} point
   1.680 +   *         x or y co-ordinate. If this is undefined we hide the guide.
   1.681 +   */
   1.682 +  _updateGuide: function(guide, point=-1) {
   1.683 +    if (point > 0) {
   1.684 +      let offset = GUIDE_STROKE_WIDTH / 2;
   1.685 +
   1.686 +      if (guide === this._guideNodes.top || guide === this._guideNodes.left) {
   1.687 +        point -= offset;
   1.688 +      } else {
   1.689 +        point += offset;
   1.690 +      }
   1.691 +
   1.692 +      if (guide === this._guideNodes.top || guide === this._guideNodes.bottom) {
   1.693 +        guide.setAttribute("x1", 0);
   1.694 +        guide.setAttribute("y1", point);
   1.695 +        guide.setAttribute("x2", "100%");
   1.696 +        guide.setAttribute("y2", point);
   1.697 +      } else {
   1.698 +        guide.setAttribute("x1", point);
   1.699 +        guide.setAttribute("y1", 0);
   1.700 +        guide.setAttribute("x2", point);
   1.701 +        guide.setAttribute("y2", "100%");
   1.702 +      }
   1.703 +      guide.removeAttribute("hidden");
   1.704 +      return true;
   1.705 +    } else {
   1.706 +      guide.setAttribute("hidden", "true");
   1.707 +      return false;
   1.708 +    }
   1.709 +  },
   1.710 +
   1.711 +  /**
   1.712 +   * Update node information (tagName#id.class)
   1.713 +   */
   1.714 +  _updateInfobar: function() {
   1.715 +    if (this.currentNode) {
   1.716 +      // Tag name
   1.717 +      this.nodeInfo.tagNameLabel.textContent = this.currentNode.tagName;
   1.718 +
   1.719 +      // ID
   1.720 +      this.nodeInfo.idLabel.textContent = this.currentNode.id ? "#" + this.currentNode.id : "";
   1.721 +
   1.722 +      // Classes
   1.723 +      let classes = this.nodeInfo.classesBox;
   1.724 +
   1.725 +      classes.textContent = this.currentNode.classList.length ?
   1.726 +                              "." + Array.join(this.currentNode.classList, ".") : "";
   1.727 +
   1.728 +      // Pseudo-classes
   1.729 +      let pseudos = PSEUDO_CLASSES.filter(pseudo => {
   1.730 +        return DOMUtils.hasPseudoClassLock(this.currentNode, pseudo);
   1.731 +      }, this);
   1.732 +
   1.733 +      let pseudoBox = this.nodeInfo.pseudoClassesBox;
   1.734 +      pseudoBox.textContent = pseudos.join("");
   1.735 +
   1.736 +      this._moveInfobar();
   1.737 +    }
   1.738 +  },
   1.739 +
   1.740 +  /**
   1.741 +   * Move the Infobar to the right place in the highlighter.
   1.742 +   */
   1.743 +  _moveInfobar: function() {
   1.744 +    if (this.rect) {
   1.745 +      let bounds = this.rect.bounds;
   1.746 +      let winHeight = this.win.innerHeight * this.zoom;
   1.747 +      let winWidth = this.win.innerWidth * this.zoom;
   1.748 +
   1.749 +      this.nodeInfo.positioner.removeAttribute("disabled");
   1.750 +      // Can the bar be above the node?
   1.751 +      if (bounds.top < this.nodeInfo.barHeight) {
   1.752 +        // No. Can we move the toolbar under the node?
   1.753 +        if (bounds.bottom + this.nodeInfo.barHeight > winHeight) {
   1.754 +          // No. Let's move it inside.
   1.755 +          this.nodeInfo.positioner.style.top = bounds.top + "px";
   1.756 +          this.nodeInfo.positioner.setAttribute("position", "overlap");
   1.757 +        } else {
   1.758 +          // Yes. Let's move it under the node.
   1.759 +          this.nodeInfo.positioner.style.top = bounds.bottom - INFO_BAR_OFFSET + "px";
   1.760 +          this.nodeInfo.positioner.setAttribute("position", "bottom");
   1.761 +        }
   1.762 +      } else {
   1.763 +        // Yes. Let's move it on top of the node.
   1.764 +        this.nodeInfo.positioner.style.top =
   1.765 +          bounds.top + INFO_BAR_OFFSET - this.nodeInfo.barHeight + "px";
   1.766 +        this.nodeInfo.positioner.setAttribute("position", "top");
   1.767 +      }
   1.768 +
   1.769 +      let barWidth = this.nodeInfo.positioner.getBoundingClientRect().width;
   1.770 +      let left = bounds.right - bounds.width / 2 - barWidth / 2;
   1.771 +
   1.772 +      // Make sure the whole infobar is visible
   1.773 +      if (left < 0) {
   1.774 +        left = 0;
   1.775 +        this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
   1.776 +      } else {
   1.777 +        if (left + barWidth > winWidth) {
   1.778 +          left = winWidth - barWidth;
   1.779 +          this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
   1.780 +        } else {
   1.781 +          this.nodeInfo.positioner.removeAttribute("hide-arrow");
   1.782 +        }
   1.783 +      }
   1.784 +      this.nodeInfo.positioner.style.left = left + "px";
   1.785 +    } else {
   1.786 +      this.nodeInfo.positioner.style.left = "0";
   1.787 +      this.nodeInfo.positioner.style.top = "0";
   1.788 +      this.nodeInfo.positioner.setAttribute("position", "top");
   1.789 +      this.nodeInfo.positioner.setAttribute("hide-arrow", "true");
   1.790 +    }
   1.791 +  },
   1.792 +
   1.793 +  _attachPageListeners: function() {
   1.794 +    if (this.currentNode) {
   1.795 +      let win = this.currentNode.ownerGlobal;
   1.796 +
   1.797 +      win.addEventListener("scroll", this, false);
   1.798 +      win.addEventListener("resize", this, false);
   1.799 +      win.addEventListener("MozAfterPaint", this, false);
   1.800 +    }
   1.801 +  },
   1.802 +
   1.803 +  _detachPageListeners: function() {
   1.804 +    if (this.currentNode) {
   1.805 +      let win = this.currentNode.ownerGlobal;
   1.806 +
   1.807 +      win.removeEventListener("scroll", this, false);
   1.808 +      win.removeEventListener("resize", this, false);
   1.809 +      win.removeEventListener("MozAfterPaint", this, false);
   1.810 +    }
   1.811 +  },
   1.812 +
   1.813 +  /**
   1.814 +   * Generic event handler.
   1.815 +   *
   1.816 +   * @param nsIDOMEvent aEvent
   1.817 +   *        The DOM event object.
   1.818 +   */
   1.819 +  handleEvent: function(event) {
   1.820 +    switch (event.type) {
   1.821 +      case "resize":
   1.822 +      case "MozAfterPaint":
   1.823 +      case "scroll":
   1.824 +        this._update();
   1.825 +        break;
   1.826 +    }
   1.827 +  },
   1.828 +};
   1.829 +
   1.830 +/**
   1.831 + * The SimpleOutlineHighlighter is a class that has the same API than the
   1.832 + * BoxModelHighlighter, but adds a pseudo-class on the target element itself
   1.833 + * to draw a simple outline.
   1.834 + * It is used by the HighlighterActor too, but in case the more complex
   1.835 + * BoxModelHighlighter can't be attached (which is the case for FirefoxOS and
   1.836 + * Fennec targets for instance).
   1.837 + */
   1.838 +function SimpleOutlineHighlighter(tabActor) {
   1.839 +  this.chromeDoc = tabActor.window.document;
   1.840 +}
   1.841 +
   1.842 +SimpleOutlineHighlighter.prototype = {
   1.843 +  /**
   1.844 +   * Destroy the nodes. Remove listeners.
   1.845 +   */
   1.846 +  destroy: function() {
   1.847 +    this.hide();
   1.848 +    if (this.installedHelpers) {
   1.849 +      this.installedHelpers.clear();
   1.850 +    }
   1.851 +    this.chromeDoc = null;
   1.852 +  },
   1.853 +
   1.854 +  _installHelperSheet: function(node) {
   1.855 +    if (!this.installedHelpers) {
   1.856 +      this.installedHelpers = new WeakMap;
   1.857 +    }
   1.858 +    let win = node.ownerDocument.defaultView;
   1.859 +    if (!this.installedHelpers.has(win)) {
   1.860 +      let {Style} = require("sdk/stylesheet/style");
   1.861 +      let {attach} = require("sdk/content/mod");
   1.862 +      let style = Style({source: HELPER_SHEET, type: "agent"});
   1.863 +      attach(style, win);
   1.864 +      this.installedHelpers.set(win, style);
   1.865 +    }
   1.866 +  },
   1.867 +
   1.868 +  /**
   1.869 +   * Show the highlighter on a given node
   1.870 +   * @param {DOMNode} node
   1.871 +   */
   1.872 +  show: function(node) {
   1.873 +    if (!this.currentNode || node !== this.currentNode) {
   1.874 +      this.hide();
   1.875 +      this.currentNode = node;
   1.876 +      this._installHelperSheet(node);
   1.877 +      DOMUtils.addPseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
   1.878 +    }
   1.879 +  },
   1.880 +
   1.881 +  /**
   1.882 +   * Hide the highlighter, the outline and the infobar.
   1.883 +   */
   1.884 +  hide: function() {
   1.885 +    if (this.currentNode) {
   1.886 +      DOMUtils.removePseudoClassLock(this.currentNode, HIGHLIGHTED_PSEUDO_CLASS);
   1.887 +      this.currentNode = null;
   1.888 +    }
   1.889 +  }
   1.890 +};
   1.891 +
   1.892 +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   1.893 +  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
   1.894 +});

mercurial