Bug 703031 - [highlighter] Refactor the highlighter code. Create highlighter.jsm Patch A; r=rcampbell
authorPaul Rouget <paul@mozilla.com>
Fri, 13 Jan 2012 19:52:37 +0100
changeset 86005 15d0a2e0c3c83085f3c78b92ee497cfc3d7a24db
parent 85836 0b4c58200e3a4ad1966d7f1cd244ab9bb73fa69a
child 86006 3e736f012ce8688295dc1a0f21cc69bf673879dc
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs703031
milestone12.0a1
Bug 703031 - [highlighter] Refactor the highlighter code. Create highlighter.jsm Patch A; r=rcampbell
browser/devtools/highlighter/Makefile.in
browser/devtools/highlighter/highlighter.jsm
browser/devtools/highlighter/inspector.jsm
--- a/browser/devtools/highlighter/Makefile.in
+++ b/browser/devtools/highlighter/Makefile.in
@@ -44,15 +44,16 @@ VPATH		= @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
 	inspector.jsm \
 	domplate.jsm \
 	InsideOutBox.jsm \
 	TreePanel.jsm \
+	highlighter.jsm \
 	$(NULL)
 
 ifdef ENABLE_TESTS
  	DIRS += test
 endif
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/highlighter.jsm
@@ -0,0 +1,678 @@
+//// Highlighter
+
+/**
+ * A highlighter mechanism.
+ *
+ * The highlighter is built dynamically once the Inspector is invoked:
+ * <stack id="highlighter-container">
+ *   <vbox id="highlighter-veil-container">...</vbox>
+ *   <box id="highlighter-controls>...</vbox>
+ * </stack>
+ *
+ * @param object aInspector
+ *        The InspectorUI instance.
+ */
+function Highlighter(aInspector)
+{
+  this.IUI = aInspector;
+  this._init();
+}
+
+Highlighter.prototype = {
+  _init: function Highlighter__init()
+  {
+    this.browser = this.IUI.browser;
+    this.chromeDoc = this.IUI.chromeDoc;
+
+    let stack = this.browser.parentNode;
+    this.win = this.browser.contentWindow;
+    this._highlighting = false;
+
+    this.highlighterContainer = this.chromeDoc.createElement("stack");
+    this.highlighterContainer.id = "highlighter-container";
+
+    this.veilContainer = this.chromeDoc.createElement("vbox");
+    this.veilContainer.id = "highlighter-veil-container";
+
+    // The controlsBox will host the different interactive
+    // elements of the highlighter (buttons, toolbars, ...).
+    let controlsBox = this.chromeDoc.createElement("box");
+    controlsBox.id = "highlighter-controls";
+    this.highlighterContainer.appendChild(this.veilContainer);
+    this.highlighterContainer.appendChild(controlsBox);
+
+    stack.appendChild(this.highlighterContainer);
+
+    // The veil will make the whole page darker except
+    // for the region of the selected box.
+    this.buildVeil(this.veilContainer);
+
+    this.buildInfobar(controlsBox);
+
+    if (!this.IUI.store.getValue(this.winID, "inspecting")) {
+      this.veilContainer.setAttribute("locked", true);
+      this.nodeInfo.container.setAttribute("locked", true);
+    }
+
+    this.browser.addEventListener("resize", this, true);
+    this.browser.addEventListener("scroll", this, true);
+
+    this.transitionDisabler = null;
+
+    this.computeZoomFactor();
+    this.handleResize();
+  },
+
+  /**
+   * Build the veil:
+   *
+   * <vbox id="highlighter-veil-container">
+   *   <box id="highlighter-veil-topbox" class="highlighter-veil"/>
+   *   <hbox id="highlighter-veil-middlebox">
+   *     <box id="highlighter-veil-leftbox" class="highlighter-veil"/>
+   *     <box id="highlighter-veil-transparentbox"/>
+   *     <box id="highlighter-veil-rightbox" class="highlighter-veil"/>
+   *   </hbox>
+   *   <box id="highlighter-veil-bottombox" class="highlighter-veil"/>
+   * </vbox>
+   *
+   * @param nsIDOMElement aParent
+   *        The container of the veil boxes.
+   */
+  buildVeil: function Highlighter_buildVeil(aParent)
+  {
+    // We will need to resize these boxes to surround a node.
+    // See highlightRectangle().
+
+    this.veilTopBox = this.chromeDoc.createElement("box");
+    this.veilTopBox.id = "highlighter-veil-topbox";
+    this.veilTopBox.className = "highlighter-veil";
+
+    this.veilMiddleBox = this.chromeDoc.createElement("hbox");
+    this.veilMiddleBox.id = "highlighter-veil-middlebox";
+
+    this.veilLeftBox = this.chromeDoc.createElement("box");
+    this.veilLeftBox.id = "highlighter-veil-leftbox";
+    this.veilLeftBox.className = "highlighter-veil";
+
+    this.veilTransparentBox = this.chromeDoc.createElement("box");
+    this.veilTransparentBox.id = "highlighter-veil-transparentbox";
+
+    // We don't need any references to veilRightBox and veilBottomBox.
+    // These boxes are automatically resized (flex=1)
+
+    let veilRightBox = this.chromeDoc.createElement("box");
+    veilRightBox.id = "highlighter-veil-rightbox";
+    veilRightBox.className = "highlighter-veil";
+
+    let veilBottomBox = this.chromeDoc.createElement("box");
+    veilBottomBox.id = "highlighter-veil-bottombox";
+    veilBottomBox.className = "highlighter-veil";
+
+    this.veilMiddleBox.appendChild(this.veilLeftBox);
+    this.veilMiddleBox.appendChild(this.veilTransparentBox);
+    this.veilMiddleBox.appendChild(veilRightBox);
+
+    aParent.appendChild(this.veilTopBox);
+    aParent.appendChild(this.veilMiddleBox);
+    aParent.appendChild(veilBottomBox);
+  },
+
+  /**
+   * Build the node Infobar.
+   *
+   * <box id="highlighter-nodeinfobar-container">
+   *   <box id="Highlighter-nodeinfobar-arrow-top"/>
+   *   <vbox id="highlighter-nodeinfobar">
+   *     <label id="highlighter-nodeinfobar-tagname"/>
+   *     <label id="highlighter-nodeinfobar-id"/>
+   *     <vbox id="highlighter-nodeinfobar-classes"/>
+   *   </vbox>
+   *   <box id="Highlighter-nodeinfobar-arrow-bottom"/>
+   * </box>
+   *
+   * @param nsIDOMElement aParent
+   *        The container of the infobar.
+   */
+  buildInfobar: function Highlighter_buildInfobar(aParent)
+  {
+    let container = this.chromeDoc.createElement("box");
+    container.id = "highlighter-nodeinfobar-container";
+    container.setAttribute("position", "top");
+    container.setAttribute("disabled", "true");
+
+    let nodeInfobar = this.chromeDoc.createElement("hbox");
+    nodeInfobar.id = "highlighter-nodeinfobar";
+
+    let arrowBoxTop = this.chromeDoc.createElement("box");
+    arrowBoxTop.className = "highlighter-nodeinfobar-arrow";
+    arrowBoxTop.id = "highlighter-nodeinfobar-arrow-top";
+
+    let arrowBoxBottom = this.chromeDoc.createElement("box");
+    arrowBoxBottom.className = "highlighter-nodeinfobar-arrow";
+    arrowBoxBottom.id = "highlighter-nodeinfobar-arrow-bottom";
+
+    let tagNameLabel = this.chromeDoc.createElement("label");
+    tagNameLabel.id = "highlighter-nodeinfobar-tagname";
+    tagNameLabel.className = "plain";
+
+    let idLabel = this.chromeDoc.createElement("label");
+    idLabel.id = "highlighter-nodeinfobar-id";
+    idLabel.className = "plain";
+
+    let classesBox = this.chromeDoc.createElement("hbox");
+    classesBox.id = "highlighter-nodeinfobar-classes";
+
+    nodeInfobar.appendChild(tagNameLabel);
+    nodeInfobar.appendChild(idLabel);
+    nodeInfobar.appendChild(classesBox);
+    container.appendChild(arrowBoxTop);
+    container.appendChild(nodeInfobar);
+    container.appendChild(arrowBoxBottom);
+
+    aParent.appendChild(container);
+
+    let barHeight = container.getBoundingClientRect().height;
+
+    this.nodeInfo = {
+      tagNameLabel: tagNameLabel,
+      idLabel: idLabel,
+      classesBox: classesBox,
+      container: container,
+      barHeight: barHeight,
+    };
+  },
+
+  /**
+   * Destroy the nodes.
+   */
+  destroy: function Highlighter_destroy()
+  {
+    this.IUI.win.clearTimeout(this.transitionDisabler);
+    this.browser.removeEventListener("scroll", this, true);
+    this.browser.removeEventListener("resize", this, true);
+    this.boundCloseEventHandler = null;
+    this._contentRect = null;
+    this._highlightRect = null;
+    this._highlighting = false;
+    this.veilTopBox = null;
+    this.veilLeftBox = null;
+    this.veilMiddleBox = null;
+    this.veilTransparentBox = null;
+    this.veilContainer = null;
+    this.node = null;
+    this.nodeInfo = null;
+    this.highlighterContainer.parentNode.removeChild(this.highlighterContainer);
+    this.highlighterContainer = null;
+    this.win = null
+    this.browser = null;
+    this.chromeDoc = null;
+    this.IUI = null;
+  },
+
+  /**
+   * Is the highlighter highlighting? Public method for querying the state
+   * of the highlighter.
+   */
+  get isHighlighting() {
+    return this._highlighting;
+  },
+
+  /**
+   * Highlight this.node, unhilighting first if necessary.
+   *
+   * @param boolean aScroll
+   *        Boolean determining whether to scroll or not.
+   */
+  highlight: function Highlighter_highlight(aScroll)
+  {
+    let rect = null;
+
+    if (this.node && this.isNodeHighlightable(this.node)) {
+
+      if (aScroll) {
+        this.node.scrollIntoView();
+      }
+
+      let clientRect = this.node.getBoundingClientRect();
+
+      // Go up in the tree of frames to determine the correct rectangle.
+      // clientRect is read-only, we need to be able to change properties.
+      rect = {top: clientRect.top,
+              left: clientRect.left,
+              width: clientRect.width,
+              height: clientRect.height};
+
+      let frameWin = this.node.ownerDocument.defaultView;
+
+      // We iterate through all the parent windows.
+      while (true) {
+
+        // Does the selection overflow on the right of its window?
+        let diffx = frameWin.innerWidth - (rect.left + rect.width);
+        if (diffx < 0) {
+          rect.width += diffx;
+        }
+
+        // Does the selection overflow on the bottom of its window?
+        let diffy = frameWin.innerHeight - (rect.top + rect.height);
+        if (diffy < 0) {
+          rect.height += diffy;
+        }
+
+        // Does the selection overflow on the left of its window?
+        if (rect.left < 0) {
+          rect.width += rect.left;
+          rect.left = 0;
+        }
+
+        // Does the selection overflow on the top of its window?
+        if (rect.top < 0) {
+          rect.height += rect.top;
+          rect.top = 0;
+        }
+
+        // Selection has been clipped to fit in its own window.
+
+        // Are we in the top-level window?
+        if (frameWin.parent === frameWin || !frameWin.frameElement) {
+          break;
+        }
+
+        // We are in an iframe.
+        // We take into account the parent iframe position and its
+        // offset (borders and padding).
+        let frameRect = frameWin.frameElement.getBoundingClientRect();
+
+        let [offsetTop, offsetLeft] =
+          this.IUI.getIframeContentOffset(frameWin.frameElement);
+
+        rect.top += frameRect.top + offsetTop;
+        rect.left += frameRect.left + offsetLeft;
+
+        frameWin = frameWin.parent;
+      }
+    }
+
+    this.highlightRectangle(rect);
+
+    this.moveInfobar();
+
+    if (this._highlighting) {
+      Services.obs.notifyObservers(null,
+        INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, null);
+    }
+  },
+
+  /**
+   * Highlight the given node.
+   *
+   * @param nsIDOMNode aNode
+   *        a DOM element to be highlighted
+   * @param object aParams
+   *        extra parameters object
+   */
+  highlightNode: function Highlighter_highlightNode(aNode, aParams)
+  {
+    this.node = aNode;
+    this.updateInfobar();
+    this.highlight(aParams && aParams.scroll);
+  },
+
+  /**
+   * Highlight a rectangular region.
+   *
+   * @param object aRect
+   *        The rectangle region to highlight.
+   * @returns boolean
+   *          True if the rectangle was highlighted, false otherwise.
+   */
+  highlightRectangle: function Highlighter_highlightRectangle(aRect)
+  {
+    if (!aRect) {
+      this.unhighlight();
+      return;
+    }
+
+    let oldRect = this._contentRect;
+
+    if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
+        aRect.width == oldRect.width && aRect.height == oldRect.height) {
+      return this._highlighting; // same rectangle
+    }
+
+    // adjust rect for zoom scaling
+    let aRectScaled = {};
+    for (let prop in aRect) {
+      aRectScaled[prop] = aRect[prop] * this.zoom;
+    }
+
+    if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
+        aRectScaled.width > 0 && aRectScaled.height > 0) {
+
+      this.veilTransparentBox.style.visibility = "visible";
+
+      // The bottom div and the right div are flexibles (flex=1).
+      // We don't need to resize them.
+      this.veilTopBox.style.height = aRectScaled.top + "px";
+      this.veilLeftBox.style.width = aRectScaled.left + "px";
+      this.veilMiddleBox.style.height = aRectScaled.height + "px";
+      this.veilTransparentBox.style.width = aRectScaled.width + "px";
+
+      this._highlighting = true;
+    } else {
+      this.unhighlight();
+    }
+
+    this._contentRect = aRect; // save orig (non-scaled) rect
+    this._highlightRect = aRectScaled; // and save the scaled rect.
+
+    return this._highlighting;
+  },
+
+  /**
+   * Clear the highlighter surface.
+   */
+  unhighlight: function Highlighter_unhighlight()
+  {
+    this._highlighting = false;
+    this.veilMiddleBox.style.height = 0;
+    this.veilTransparentBox.style.width = 0;
+    this.veilTransparentBox.style.visibility = "hidden";
+    Services.obs.notifyObservers(null,
+      INSPECTOR_NOTIFICATIONS.UNHIGHLIGHTING, null);
+  },
+
+  /**
+   * Update node information (tagName#id.class) 
+   */
+  updateInfobar: function Highlighter_updateInfobar()
+  {
+    // Tag name
+    this.nodeInfo.tagNameLabel.textContent = this.node.tagName;
+
+    // ID
+    this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : "";
+
+    // Classes
+    let classes = this.nodeInfo.classesBox;
+    while (classes.hasChildNodes()) {
+      classes.removeChild(classes.firstChild);
+    }
+
+    if (this.node.className) {
+      let fragment = this.chromeDoc.createDocumentFragment();
+      for (let i = 0; i < this.node.classList.length; i++) {
+        let classLabel = this.chromeDoc.createElement("label");
+        classLabel.className = "highlighter-nodeinfobar-class plain";
+        classLabel.textContent = "." + this.node.classList[i];
+        fragment.appendChild(classLabel);
+      }
+      classes.appendChild(fragment);
+    }
+  },
+
+  /**
+   * Move the Infobar to the right place in the highlighter.
+   */
+  moveInfobar: function Highlighter_moveInfobar()
+  {
+    if (this._highlightRect) {
+      let winHeight = this.win.innerHeight * this.zoom;
+      let winWidth = this.win.innerWidth * this.zoom;
+
+      let rect = {top: this._highlightRect.top,
+                  left: this._highlightRect.left,
+                  width: this._highlightRect.width,
+                  height: this._highlightRect.height};
+
+      rect.top = Math.max(rect.top, 0);
+      rect.left = Math.max(rect.left, 0);
+      rect.width = Math.max(rect.width, 0);
+      rect.height = Math.max(rect.height, 0);
+
+      rect.top = Math.min(rect.top, winHeight);
+      rect.left = Math.min(rect.left, winWidth);
+
+      this.nodeInfo.container.removeAttribute("disabled");
+      // Can the bar be above the node?
+      if (rect.top < this.nodeInfo.barHeight) {
+        // No. Can we move the toolbar under the node?
+        if (rect.top + rect.height +
+            this.nodeInfo.barHeight > winHeight) {
+          // No. Let's move it inside.
+          this.nodeInfo.container.style.top = rect.top + "px";
+          this.nodeInfo.container.setAttribute("position", "overlap");
+        } else {
+          // Yes. Let's move it under the node.
+          this.nodeInfo.container.style.top = rect.top + rect.height + "px";
+          this.nodeInfo.container.setAttribute("position", "bottom");
+        }
+      } else {
+        // Yes. Let's move it on top of the node.
+        this.nodeInfo.container.style.top =
+          rect.top - this.nodeInfo.barHeight + "px";
+        this.nodeInfo.container.setAttribute("position", "top");
+      }
+
+      let barWidth = this.nodeInfo.container.getBoundingClientRect().width;
+      let left = rect.left + rect.width / 2 - barWidth / 2;
+
+      // Make sure the whole infobar is visible
+      if (left < 0) {
+        left = 0;
+        this.nodeInfo.container.setAttribute("hide-arrow", "true");
+      } else {
+        if (left + barWidth > winWidth) {
+          left = winWidth - barWidth;
+          this.nodeInfo.container.setAttribute("hide-arrow", "true");
+        } else {
+          this.nodeInfo.container.removeAttribute("hide-arrow");
+        }
+      }
+      this.nodeInfo.container.style.left = left + "px";
+    } else {
+      this.nodeInfo.container.style.left = "0";
+      this.nodeInfo.container.style.top = "0";
+      this.nodeInfo.container.setAttribute("position", "top");
+      this.nodeInfo.container.setAttribute("hide-arrow", "true");
+    }
+  },
+
+  /**
+   * Return the midpoint of a line from pointA to pointB.
+   *
+   * @param object aPointA
+   *        An object with x and y properties.
+   * @param object aPointB
+   *        An object with x and y properties.
+   * @returns object
+   *          An object with x and y properties.
+   */
+  midPoint: function Highlighter_midPoint(aPointA, aPointB)
+  {
+    let pointC = { };
+    pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
+    pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
+    return pointC;
+  },
+
+  /**
+   * Return the node under the highlighter rectangle. Useful for testing.
+   * Calculation based on midpoint of diagonal from top left to bottom right
+   * of panel.
+   *
+   * @returns nsIDOMNode|null
+   *          Returns the node under the current highlighter rectangle. Null is
+   *          returned if there is no node highlighted.
+   */
+  get highlitNode()
+  {
+    // Not highlighting? Bail.
+    if (!this._highlighting || !this._contentRect) {
+      return null;
+    }
+
+    let a = {
+      x: this._contentRect.left,
+      y: this._contentRect.top
+    };
+
+    let b = {
+      x: a.x + this._contentRect.width,
+      y: a.y + this._contentRect.height
+    };
+
+    // Get midpoint of diagonal line.
+    let midpoint = this.midPoint(a, b);
+
+    return this.IUI.elementFromPoint(this.win.document, midpoint.x,
+      midpoint.y);
+  },
+
+  /**
+   * Is the specified node highlightable?
+   *
+   * @param nsIDOMNode aNode
+   *        the DOM element in question
+   * @returns boolean
+   *          True if the node is highlightable or false otherwise.
+   */
+  isNodeHighlightable: function Highlighter_isNodeHighlightable(aNode)
+  {
+    if (aNode.nodeType != aNode.ELEMENT_NODE) {
+      return false;
+    }
+    let nodeName = aNode.nodeName.toLowerCase();
+    return !INSPECTOR_INVISIBLE_ELEMENTS[nodeName];
+  },
+
+  /**
+   * Store page zoom factor.
+   */
+  computeZoomFactor: function Highlighter_computeZoomFactor() {
+    this.zoom =
+      this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+      .getInterface(Components.interfaces.nsIDOMWindowUtils)
+      .screenPixelsPerCSSPixel;
+  },
+
+  /////////////////////////////////////////////////////////////////////////
+  //// Event Handling
+
+  attachInspectListeners: function Highlighter_attachInspectListeners()
+  {
+    this.browser.addEventListener("mousemove", this, true);
+    this.browser.addEventListener("click", this, true);
+    this.browser.addEventListener("dblclick", this, true);
+    this.browser.addEventListener("mousedown", this, true);
+    this.browser.addEventListener("mouseup", this, true);
+  },
+
+  detachInspectListeners: function Highlighter_detachInspectListeners()
+  {
+    this.browser.removeEventListener("mousemove", this, true);
+    this.browser.removeEventListener("click", this, true);
+    this.browser.removeEventListener("dblclick", this, true);
+    this.browser.removeEventListener("mousedown", this, true);
+    this.browser.removeEventListener("mouseup", this, true);
+  },
+
+
+  /**
+   * Generic event handler.
+   *
+   * @param nsIDOMEvent aEvent
+   *        The DOM event object.
+   */
+  handleEvent: function Highlighter_handleEvent(aEvent)
+  {
+    switch (aEvent.type) {
+      case "click":
+        this.handleClick(aEvent);
+        break;
+      case "mousemove":
+        this.handleMouseMove(aEvent);
+        break;
+      case "resize":
+        this.computeZoomFactor();
+        this.brieflyDisableTransitions();
+        this.handleResize(aEvent);
+        break;
+      case "dblclick":
+      case "mousedown":
+      case "mouseup":
+        aEvent.stopPropagation();
+        aEvent.preventDefault();
+        break;
+      case "scroll":
+        this.brieflyDisableTransitions();
+        this.highlight();
+        break;
+    }
+  },
+
+  /**
+   * Disable the CSS transitions for a short time to avoid laggy animations
+   * during scrolling or resizing.
+   */
+  brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
+  {
+   if (this.transitionDisabler) {
+     this.IUI.win.clearTimeout(this.transitionDisabler);
+   } else {
+     this.veilContainer.setAttribute("disable-transitions", "true");
+     this.nodeInfo.container.setAttribute("disable-transitions", "true");
+   }
+   this.transitionDisabler =
+     this.IUI.win.setTimeout(function() {
+       this.veilContainer.removeAttribute("disable-transitions");
+       this.nodeInfo.container.removeAttribute("disable-transitions");
+       this.transitionDisabler = null;
+     }.bind(this), 500);
+  },
+
+  /**
+   * Handle clicks.
+   *
+   * @param nsIDOMEvent aEvent
+   *        The DOM event.
+   */
+  handleClick: function Highlighter_handleClick(aEvent)
+  {
+    // Stop inspection when the user clicks on a node.
+    if (aEvent.button == 0) {
+      let win = aEvent.target.ownerDocument.defaultView;
+      this.IUI.stopInspecting();
+      win.focus();
+    }
+    aEvent.preventDefault();
+    aEvent.stopPropagation();
+  },
+
+  /**
+   * Handle mousemoves in panel when InspectorUI.inspecting is true.
+   *
+   * @param nsiDOMEvent aEvent
+   *        The MouseEvent triggering the method.
+   */
+  handleMouseMove: function Highlighter_handleMouseMove(aEvent)
+  {
+    let element = this.IUI.elementFromPoint(aEvent.target.ownerDocument,
+      aEvent.clientX, aEvent.clientY);
+    if (element && element != this.node) {
+      this.IUI.inspectNode(element);
+    }
+  },
+
+  /**
+   * Handle window resize events.
+   */
+  handleResize: function Highlighter_handleResize()
+  {
+    this.highlight();
+  },
+};
+
+///////////////////////////////////////////////////////////////////////////
+
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -94,693 +94,16 @@ const INSPECTOR_NOTIFICATIONS = {
 
   // Event notifications for the attribute-value editor
   EDITOR_OPENED: "inspector-editor-opened",
   EDITOR_CLOSED: "inspector-editor-closed",
   EDITOR_SAVED: "inspector-editor-saved",
 };
 
 ///////////////////////////////////////////////////////////////////////////
-//// Highlighter
-
-/**
- * A highlighter mechanism.
- *
- * The highlighter is built dynamically once the Inspector is invoked:
- * <stack id="highlighter-container">
- *   <vbox id="highlighter-veil-container">...</vbox>
- *   <box id="highlighter-controls>...</vbox>
- * </stack>
- *
- * @param object aInspector
- *        The InspectorUI instance.
- */
-function Highlighter(aInspector)
-{
-  this.IUI = aInspector;
-  this._init();
-}
-
-Highlighter.prototype = {
-  _init: function Highlighter__init()
-  {
-    this.browser = this.IUI.browser;
-    this.chromeDoc = this.IUI.chromeDoc;
-
-    let stack = this.browser.parentNode;
-    this.win = this.browser.contentWindow;
-    this._highlighting = false;
-
-    this.highlighterContainer = this.chromeDoc.createElement("stack");
-    this.highlighterContainer.id = "highlighter-container";
-
-    this.veilContainer = this.chromeDoc.createElement("vbox");
-    this.veilContainer.id = "highlighter-veil-container";
-
-    // The controlsBox will host the different interactive
-    // elements of the highlighter (buttons, toolbars, ...).
-    let controlsBox = this.chromeDoc.createElement("box");
-    controlsBox.id = "highlighter-controls";
-    this.highlighterContainer.appendChild(this.veilContainer);
-    this.highlighterContainer.appendChild(controlsBox);
-
-    stack.appendChild(this.highlighterContainer);
-
-    // The veil will make the whole page darker except
-    // for the region of the selected box.
-    this.buildVeil(this.veilContainer);
-
-    this.buildInfobar(controlsBox);
-
-    if (!this.IUI.store.getValue(this.winID, "inspecting")) {
-      this.veilContainer.setAttribute("locked", true);
-      this.nodeInfo.container.setAttribute("locked", true);
-    }
-
-    this.browser.addEventListener("resize", this, true);
-    this.browser.addEventListener("scroll", this, true);
-
-    this.transitionDisabler = null;
-
-    this.computeZoomFactor();
-    this.handleResize();
-  },
-
-  /**
-   * Build the veil:
-   *
-   * <vbox id="highlighter-veil-container">
-   *   <box id="highlighter-veil-topbox" class="highlighter-veil"/>
-   *   <hbox id="highlighter-veil-middlebox">
-   *     <box id="highlighter-veil-leftbox" class="highlighter-veil"/>
-   *     <box id="highlighter-veil-transparentbox"/>
-   *     <box id="highlighter-veil-rightbox" class="highlighter-veil"/>
-   *   </hbox>
-   *   <box id="highlighter-veil-bottombox" class="highlighter-veil"/>
-   * </vbox>
-   *
-   * @param nsIDOMElement aParent
-   *        The container of the veil boxes.
-   */
-  buildVeil: function Highlighter_buildVeil(aParent)
-  {
-    // We will need to resize these boxes to surround a node.
-    // See highlightRectangle().
-
-    this.veilTopBox = this.chromeDoc.createElement("box");
-    this.veilTopBox.id = "highlighter-veil-topbox";
-    this.veilTopBox.className = "highlighter-veil";
-
-    this.veilMiddleBox = this.chromeDoc.createElement("hbox");
-    this.veilMiddleBox.id = "highlighter-veil-middlebox";
-
-    this.veilLeftBox = this.chromeDoc.createElement("box");
-    this.veilLeftBox.id = "highlighter-veil-leftbox";
-    this.veilLeftBox.className = "highlighter-veil";
-
-    this.veilTransparentBox = this.chromeDoc.createElement("box");
-    this.veilTransparentBox.id = "highlighter-veil-transparentbox";
-
-    // We don't need any references to veilRightBox and veilBottomBox.
-    // These boxes are automatically resized (flex=1)
-
-    let veilRightBox = this.chromeDoc.createElement("box");
-    veilRightBox.id = "highlighter-veil-rightbox";
-    veilRightBox.className = "highlighter-veil";
-
-    let veilBottomBox = this.chromeDoc.createElement("box");
-    veilBottomBox.id = "highlighter-veil-bottombox";
-    veilBottomBox.className = "highlighter-veil";
-
-    this.veilMiddleBox.appendChild(this.veilLeftBox);
-    this.veilMiddleBox.appendChild(this.veilTransparentBox);
-    this.veilMiddleBox.appendChild(veilRightBox);
-
-    aParent.appendChild(this.veilTopBox);
-    aParent.appendChild(this.veilMiddleBox);
-    aParent.appendChild(veilBottomBox);
-  },
-
-  /**
-   * Build the node Infobar.
-   *
-   * <box id="highlighter-nodeinfobar-container">
-   *   <box id="Highlighter-nodeinfobar-arrow-top"/>
-   *   <vbox id="highlighter-nodeinfobar">
-   *     <label id="highlighter-nodeinfobar-tagname"/>
-   *     <label id="highlighter-nodeinfobar-id"/>
-   *     <vbox id="highlighter-nodeinfobar-classes"/>
-   *   </vbox>
-   *   <box id="Highlighter-nodeinfobar-arrow-bottom"/>
-   * </box>
-   *
-   * @param nsIDOMElement aParent
-   *        The container of the infobar.
-   */
-  buildInfobar: function Highlighter_buildInfobar(aParent)
-  {
-    let container = this.chromeDoc.createElement("box");
-    container.id = "highlighter-nodeinfobar-container";
-    container.setAttribute("position", "top");
-    container.setAttribute("disabled", "true");
-
-    let nodeInfobar = this.chromeDoc.createElement("hbox");
-    nodeInfobar.id = "highlighter-nodeinfobar";
-
-    let arrowBoxTop = this.chromeDoc.createElement("box");
-    arrowBoxTop.className = "highlighter-nodeinfobar-arrow";
-    arrowBoxTop.id = "highlighter-nodeinfobar-arrow-top";
-
-    let arrowBoxBottom = this.chromeDoc.createElement("box");
-    arrowBoxBottom.className = "highlighter-nodeinfobar-arrow";
-    arrowBoxBottom.id = "highlighter-nodeinfobar-arrow-bottom";
-
-    let tagNameLabel = this.chromeDoc.createElement("label");
-    tagNameLabel.id = "highlighter-nodeinfobar-tagname";
-    tagNameLabel.className = "plain";
-
-    let idLabel = this.chromeDoc.createElement("label");
-    idLabel.id = "highlighter-nodeinfobar-id";
-    idLabel.className = "plain";
-
-    let classesBox = this.chromeDoc.createElement("hbox");
-    classesBox.id = "highlighter-nodeinfobar-classes";
-
-    nodeInfobar.appendChild(tagNameLabel);
-    nodeInfobar.appendChild(idLabel);
-    nodeInfobar.appendChild(classesBox);
-    container.appendChild(arrowBoxTop);
-    container.appendChild(nodeInfobar);
-    container.appendChild(arrowBoxBottom);
-
-    aParent.appendChild(container);
-
-    let barHeight = container.getBoundingClientRect().height;
-
-    this.nodeInfo = {
-      tagNameLabel: tagNameLabel,
-      idLabel: idLabel,
-      classesBox: classesBox,
-      container: container,
-      barHeight: barHeight,
-    };
-  },
-
-  /**
-   * Destroy the nodes.
-   */
-  destroy: function Highlighter_destroy()
-  {
-    this.IUI.win.clearTimeout(this.transitionDisabler);
-    this.browser.removeEventListener("scroll", this, true);
-    this.browser.removeEventListener("resize", this, true);
-    this.boundCloseEventHandler = null;
-    this._contentRect = null;
-    this._highlightRect = null;
-    this._highlighting = false;
-    this.veilTopBox = null;
-    this.veilLeftBox = null;
-    this.veilMiddleBox = null;
-    this.veilTransparentBox = null;
-    this.veilContainer = null;
-    this.node = null;
-    this.nodeInfo = null;
-    this.highlighterContainer.parentNode.removeChild(this.highlighterContainer);
-    this.highlighterContainer = null;
-    this.win = null
-    this.browser = null;
-    this.chromeDoc = null;
-    this.IUI = null;
-  },
-
-  /**
-   * Is the highlighter highlighting? Public method for querying the state
-   * of the highlighter.
-   */
-  get isHighlighting() {
-    return this._highlighting;
-  },
-
-  /**
-   * Highlight this.node, unhilighting first if necessary.
-   *
-   * @param boolean aScroll
-   *        Boolean determining whether to scroll or not.
-   */
-  highlight: function Highlighter_highlight(aScroll)
-  {
-    let rect = null;
-
-    if (this.node && this.isNodeHighlightable(this.node)) {
-
-      if (aScroll) {
-        this.node.scrollIntoView();
-      }
-
-      let clientRect = this.node.getBoundingClientRect();
-
-      // Go up in the tree of frames to determine the correct rectangle.
-      // clientRect is read-only, we need to be able to change properties.
-      rect = {top: clientRect.top,
-              left: clientRect.left,
-              width: clientRect.width,
-              height: clientRect.height};
-
-      let frameWin = this.node.ownerDocument.defaultView;
-
-      // We iterate through all the parent windows.
-      while (true) {
-
-        // Does the selection overflow on the right of its window?
-        let diffx = frameWin.innerWidth - (rect.left + rect.width);
-        if (diffx < 0) {
-          rect.width += diffx;
-        }
-
-        // Does the selection overflow on the bottom of its window?
-        let diffy = frameWin.innerHeight - (rect.top + rect.height);
-        if (diffy < 0) {
-          rect.height += diffy;
-        }
-
-        // Does the selection overflow on the left of its window?
-        if (rect.left < 0) {
-          rect.width += rect.left;
-          rect.left = 0;
-        }
-
-        // Does the selection overflow on the top of its window?
-        if (rect.top < 0) {
-          rect.height += rect.top;
-          rect.top = 0;
-        }
-
-        // Selection has been clipped to fit in its own window.
-
-        // Are we in the top-level window?
-        if (frameWin.parent === frameWin || !frameWin.frameElement) {
-          break;
-        }
-
-        // We are in an iframe.
-        // We take into account the parent iframe position and its
-        // offset (borders and padding).
-        let frameRect = frameWin.frameElement.getBoundingClientRect();
-
-        let [offsetTop, offsetLeft] =
-          this.IUI.getIframeContentOffset(frameWin.frameElement);
-
-        rect.top += frameRect.top + offsetTop;
-        rect.left += frameRect.left + offsetLeft;
-
-        frameWin = frameWin.parent;
-      }
-    }
-
-    this.highlightRectangle(rect);
-
-    this.moveInfobar();
-
-    if (this._highlighting) {
-      Services.obs.notifyObservers(null,
-        INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, null);
-    }
-  },
-
-  /**
-   * Highlight the given node.
-   *
-   * @param nsIDOMNode aNode
-   *        a DOM element to be highlighted
-   * @param object aParams
-   *        extra parameters object
-   */
-  highlightNode: function Highlighter_highlightNode(aNode, aParams)
-  {
-    this.node = aNode;
-    this.updateInfobar();
-    this.highlight(aParams && aParams.scroll);
-  },
-
-  /**
-   * Highlight a rectangular region.
-   *
-   * @param object aRect
-   *        The rectangle region to highlight.
-   * @returns boolean
-   *          True if the rectangle was highlighted, false otherwise.
-   */
-  highlightRectangle: function Highlighter_highlightRectangle(aRect)
-  {
-    if (!aRect) {
-      this.unhighlight();
-      return;
-    }
-
-    let oldRect = this._contentRect;
-
-    if (oldRect && aRect.top == oldRect.top && aRect.left == oldRect.left &&
-        aRect.width == oldRect.width && aRect.height == oldRect.height) {
-      return this._highlighting; // same rectangle
-    }
-
-    // adjust rect for zoom scaling
-    let aRectScaled = {};
-    for (let prop in aRect) {
-      aRectScaled[prop] = aRect[prop] * this.zoom;
-    }
-
-    if (aRectScaled.left >= 0 && aRectScaled.top >= 0 &&
-        aRectScaled.width > 0 && aRectScaled.height > 0) {
-
-      this.veilTransparentBox.style.visibility = "visible";
-
-      // The bottom div and the right div are flexibles (flex=1).
-      // We don't need to resize them.
-      this.veilTopBox.style.height = aRectScaled.top + "px";
-      this.veilLeftBox.style.width = aRectScaled.left + "px";
-      this.veilMiddleBox.style.height = aRectScaled.height + "px";
-      this.veilTransparentBox.style.width = aRectScaled.width + "px";
-
-      this._highlighting = true;
-    } else {
-      this.unhighlight();
-    }
-
-    this._contentRect = aRect; // save orig (non-scaled) rect
-    this._highlightRect = aRectScaled; // and save the scaled rect.
-
-    return this._highlighting;
-  },
-
-  /**
-   * Clear the highlighter surface.
-   */
-  unhighlight: function Highlighter_unhighlight()
-  {
-    this._highlighting = false;
-    this.veilMiddleBox.style.height = 0;
-    this.veilTransparentBox.style.width = 0;
-    this.veilTransparentBox.style.visibility = "hidden";
-    Services.obs.notifyObservers(null,
-      INSPECTOR_NOTIFICATIONS.UNHIGHLIGHTING, null);
-  },
-
-  /**
-   * Update node information (tagName#id.class) 
-   */
-  updateInfobar: function Highlighter_updateInfobar()
-  {
-    // Tag name
-    this.nodeInfo.tagNameLabel.textContent = this.node.tagName;
-
-    // ID
-    this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : "";
-
-    // Classes
-    let classes = this.nodeInfo.classesBox;
-    while (classes.hasChildNodes()) {
-      classes.removeChild(classes.firstChild);
-    }
-
-    if (this.node.className) {
-      let fragment = this.chromeDoc.createDocumentFragment();
-      for (let i = 0; i < this.node.classList.length; i++) {
-        let classLabel = this.chromeDoc.createElement("label");
-        classLabel.className = "highlighter-nodeinfobar-class plain";
-        classLabel.textContent = "." + this.node.classList[i];
-        fragment.appendChild(classLabel);
-      }
-      classes.appendChild(fragment);
-    }
-  },
-
-  /**
-   * Move the Infobar to the right place in the highlighter.
-   */
-  moveInfobar: function Highlighter_moveInfobar()
-  {
-    if (this._highlightRect) {
-      let winHeight = this.win.innerHeight * this.zoom;
-      let winWidth = this.win.innerWidth * this.zoom;
-
-      let rect = {top: this._highlightRect.top,
-                  left: this._highlightRect.left,
-                  width: this._highlightRect.width,
-                  height: this._highlightRect.height};
-
-      rect.top = Math.max(rect.top, 0);
-      rect.left = Math.max(rect.left, 0);
-      rect.width = Math.max(rect.width, 0);
-      rect.height = Math.max(rect.height, 0);
-
-      rect.top = Math.min(rect.top, winHeight);
-      rect.left = Math.min(rect.left, winWidth);
-
-      this.nodeInfo.container.removeAttribute("disabled");
-      // Can the bar be above the node?
-      if (rect.top < this.nodeInfo.barHeight) {
-        // No. Can we move the toolbar under the node?
-        if (rect.top + rect.height +
-            this.nodeInfo.barHeight > winHeight) {
-          // No. Let's move it inside.
-          this.nodeInfo.container.style.top = rect.top + "px";
-          this.nodeInfo.container.setAttribute("position", "overlap");
-        } else {
-          // Yes. Let's move it under the node.
-          this.nodeInfo.container.style.top = rect.top + rect.height + "px";
-          this.nodeInfo.container.setAttribute("position", "bottom");
-        }
-      } else {
-        // Yes. Let's move it on top of the node.
-        this.nodeInfo.container.style.top =
-          rect.top - this.nodeInfo.barHeight + "px";
-        this.nodeInfo.container.setAttribute("position", "top");
-      }
-
-      let barWidth = this.nodeInfo.container.getBoundingClientRect().width;
-      let left = rect.left + rect.width / 2 - barWidth / 2;
-
-      // Make sure the whole infobar is visible
-      if (left < 0) {
-        left = 0;
-        this.nodeInfo.container.setAttribute("hide-arrow", "true");
-      } else {
-        if (left + barWidth > winWidth) {
-          left = winWidth - barWidth;
-          this.nodeInfo.container.setAttribute("hide-arrow", "true");
-        } else {
-          this.nodeInfo.container.removeAttribute("hide-arrow");
-        }
-      }
-      this.nodeInfo.container.style.left = left + "px";
-    } else {
-      this.nodeInfo.container.style.left = "0";
-      this.nodeInfo.container.style.top = "0";
-      this.nodeInfo.container.setAttribute("position", "top");
-      this.nodeInfo.container.setAttribute("hide-arrow", "true");
-    }
-  },
-
-  /**
-   * Return the midpoint of a line from pointA to pointB.
-   *
-   * @param object aPointA
-   *        An object with x and y properties.
-   * @param object aPointB
-   *        An object with x and y properties.
-   * @returns object
-   *          An object with x and y properties.
-   */
-  midPoint: function Highlighter_midPoint(aPointA, aPointB)
-  {
-    let pointC = { };
-    pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
-    pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
-    return pointC;
-  },
-
-  /**
-   * Return the node under the highlighter rectangle. Useful for testing.
-   * Calculation based on midpoint of diagonal from top left to bottom right
-   * of panel.
-   *
-   * @returns nsIDOMNode|null
-   *          Returns the node under the current highlighter rectangle. Null is
-   *          returned if there is no node highlighted.
-   */
-  get highlitNode()
-  {
-    // Not highlighting? Bail.
-    if (!this._highlighting || !this._contentRect) {
-      return null;
-    }
-
-    let a = {
-      x: this._contentRect.left,
-      y: this._contentRect.top
-    };
-
-    let b = {
-      x: a.x + this._contentRect.width,
-      y: a.y + this._contentRect.height
-    };
-
-    // Get midpoint of diagonal line.
-    let midpoint = this.midPoint(a, b);
-
-    return this.IUI.elementFromPoint(this.win.document, midpoint.x,
-      midpoint.y);
-  },
-
-  /**
-   * Is the specified node highlightable?
-   *
-   * @param nsIDOMNode aNode
-   *        the DOM element in question
-   * @returns boolean
-   *          True if the node is highlightable or false otherwise.
-   */
-  isNodeHighlightable: function Highlighter_isNodeHighlightable(aNode)
-  {
-    if (aNode.nodeType != aNode.ELEMENT_NODE) {
-      return false;
-    }
-    let nodeName = aNode.nodeName.toLowerCase();
-    return !INSPECTOR_INVISIBLE_ELEMENTS[nodeName];
-  },
-
-  /**
-   * Store page zoom factor.
-   */
-  computeZoomFactor: function Highlighter_computeZoomFactor() {
-    this.zoom =
-      this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-      .getInterface(Components.interfaces.nsIDOMWindowUtils)
-      .screenPixelsPerCSSPixel;
-  },
-
-  /////////////////////////////////////////////////////////////////////////
-  //// Event Handling
-
-  attachInspectListeners: function Highlighter_attachInspectListeners()
-  {
-    this.browser.addEventListener("mousemove", this, true);
-    this.browser.addEventListener("click", this, true);
-    this.browser.addEventListener("dblclick", this, true);
-    this.browser.addEventListener("mousedown", this, true);
-    this.browser.addEventListener("mouseup", this, true);
-  },
-
-  detachInspectListeners: function Highlighter_detachInspectListeners()
-  {
-    this.browser.removeEventListener("mousemove", this, true);
-    this.browser.removeEventListener("click", this, true);
-    this.browser.removeEventListener("dblclick", this, true);
-    this.browser.removeEventListener("mousedown", this, true);
-    this.browser.removeEventListener("mouseup", this, true);
-  },
-
-
-  /**
-   * Generic event handler.
-   *
-   * @param nsIDOMEvent aEvent
-   *        The DOM event object.
-   */
-  handleEvent: function Highlighter_handleEvent(aEvent)
-  {
-    switch (aEvent.type) {
-      case "click":
-        this.handleClick(aEvent);
-        break;
-      case "mousemove":
-        this.handleMouseMove(aEvent);
-        break;
-      case "resize":
-        this.computeZoomFactor();
-        this.brieflyDisableTransitions();
-        this.handleResize(aEvent);
-        break;
-      case "dblclick":
-      case "mousedown":
-      case "mouseup":
-        aEvent.stopPropagation();
-        aEvent.preventDefault();
-        break;
-      case "scroll":
-        this.brieflyDisableTransitions();
-        this.highlight();
-        break;
-    }
-  },
-
-  /**
-   * Disable the CSS transitions for a short time to avoid laggy animations
-   * during scrolling or resizing.
-   */
-  brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
-  {
-   if (this.transitionDisabler) {
-     this.IUI.win.clearTimeout(this.transitionDisabler);
-   } else {
-     this.veilContainer.setAttribute("disable-transitions", "true");
-     this.nodeInfo.container.setAttribute("disable-transitions", "true");
-   }
-   this.transitionDisabler =
-     this.IUI.win.setTimeout(function() {
-       this.veilContainer.removeAttribute("disable-transitions");
-       this.nodeInfo.container.removeAttribute("disable-transitions");
-       this.transitionDisabler = null;
-     }.bind(this), 500);
-  },
-
-  /**
-   * Handle clicks.
-   *
-   * @param nsIDOMEvent aEvent
-   *        The DOM event.
-   */
-  handleClick: function Highlighter_handleClick(aEvent)
-  {
-    // Stop inspection when the user clicks on a node.
-    if (aEvent.button == 0) {
-      let win = aEvent.target.ownerDocument.defaultView;
-      this.IUI.stopInspecting();
-      win.focus();
-    }
-    aEvent.preventDefault();
-    aEvent.stopPropagation();
-  },
-
-  /**
-   * Handle mousemoves in panel when InspectorUI.inspecting is true.
-   *
-   * @param nsiDOMEvent aEvent
-   *        The MouseEvent triggering the method.
-   */
-  handleMouseMove: function Highlighter_handleMouseMove(aEvent)
-  {
-    let element = this.IUI.elementFromPoint(aEvent.target.ownerDocument,
-      aEvent.clientX, aEvent.clientY);
-    if (element && element != this.node) {
-      this.IUI.inspectNode(element);
-    }
-  },
-
-  /**
-   * Handle window resize events.
-   */
-  handleResize: function Highlighter_handleResize()
-  {
-    this.highlight();
-  },
-};
-
-///////////////////////////////////////////////////////////////////////////
 //// InspectorUI
 
 /**
  * Main controller class for the Inspector.
  *
  * @constructor
  * @param nsIDOMWindow aWindow
  *        The chrome window for which the Inspector instance is created.