Bug 703031 - [highlighter] Refactor the highlighter code. Create highlighter.jsm - Patch C; r=rcampbell
authorPaul Rouget <paul@mozilla.com>
Wed, 07 Dec 2011 21:13:02 +0100
changeset 86007 a62e69926f832a2c4ff84eea30ab0f65aa75dbed
parent 86006 3e736f012ce8688295dc1a0f21cc69bf673879dc
child 86008 3446a54199ab3f3e145bcaa1fbb9553d2b273512
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 C; r=rcampbell
browser/devtools/highlighter/TreePanel.jsm
browser/devtools/highlighter/highlighter.jsm
browser/devtools/highlighter/inspector.jsm
browser/devtools/shared/LayoutHelpers.jsm
browser/devtools/shared/Makefile.in
--- a/browser/devtools/highlighter/TreePanel.jsm
+++ b/browser/devtools/highlighter/TreePanel.jsm
@@ -356,17 +356,17 @@ TreePanel.prototype = {
     if (node) {
       if (hitTwisty) {
         this.ioBox.toggleObject(node);
       } else {
         if (this.IUI.inspecting) {
           this.IUI.stopInspecting(true);
         } else {
           this.IUI.select(node, true, false);
-          this.IUI.highlighter.highlightNode(node);
+          this.IUI.highlighter.highlight(node);
         }
       }
     }
   },
 
   /**
    * Handle double-click events in the html tree panel.
    * (double-clicking an attribute value allows it to be edited)
--- a/browser/devtools/highlighter/highlighter.jsm
+++ b/browser/devtools/highlighter/highlighter.jsm
@@ -1,34 +1,149 @@
-//// Highlighter
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Mozilla Highlighter Module.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Rob Campbell <rcampbell@mozilla.com> (original author)
+ *   Mihai Șucan <mihai.sucan@gmail.com>
+ *   Julian Viereck <jviereck@mozilla.com>
+ *   Paul Rouget <paul@mozilla.com>
+ *   Kyle Simpson <ksimpson@mozilla.com>
+ *   Johan Charlez <johan.charlez@gmail.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+
+var EXPORTED_SYMBOLS = ["Highlighter"];
+
+const INSPECTOR_INVISIBLE_ELEMENTS = {
+  "head": true,
+  "base": true,
+  "basefont": true,
+  "isindex": true,
+  "link": true,
+  "meta": true,
+  "script": true,
+  "style": true,
+  "title": true,
+};
 
 /**
  * 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>
+ * The highlighter is built dynamically into the browser element.
+ * The caller is in charge of destroying the highlighter (ie, the highlighter
+ * won't be destroyed if a new tab is selected for example).
+ *
+ * API:
+ *
+ *   // Constructor and destructor.
+ *   // @param aWindow - browser.xul window.
+ *   Highlighter(aWindow); 
+ *   void destroy();
+ *
+ *   // Highlight a node.
+ *   // @param aNode - node to highlight
+ *   // @param aScroll - scroll to ensure the node is visible
+ *   void highlight(aNode, aScroll);
+ *
+ *   // Get the selected node.
+ *   DOMNode getNode();
+ *
+ *   // Lock and unlock the select node.
+ *   void lock();
+ *   void unlock();
+ *
+ *   // Show and hide the highlighter
+ *   void show();
+ *   void hide();
+ *   boolean isHidden();
+ *
+ *   // Redraw the highlighter if the visible portion of the node has changed.
+ *   void invalidateSize(aScroll);
+ *
+ *   // Is a node highlightable.
+ *   boolean isNodeHighlightable(aNode);
  *
- * @param object aInspector
- *        The InspectorUI instance.
+ *   // Add/Remove lsiteners
+ *   // @param aEvent - event name
+ *   // @param aListener - function callback
+ *   void addListener(aEvent, aListener);
+ *   void removeListener(aEvent, aListener);
+ *
+ * Events:
+ *
+ *   "closed" - Highlighter is closing
+ *   "nodeselected" - A new node has been selected
+ *   "highlighting" - Highlighter is highlighting
+ *   "locked" - The selected node has been locked
+ *   "unlocked" - The selected ndoe has been unlocked
+ *
+ * Structure:
+ *
+ *   <stack id="highlighter-container">
+ *     <vbox id="highlighter-veil-container">...</vbox>
+ *     <box id="highlighter-controls>...</vbox>
+ *   </stack>
+ *
  */
-function Highlighter(aInspector)
+
+
+/**
+ * Constructor.
+ *
+ * @param object aWindow
+ */
+function Highlighter(aWindow)
 {
-  this.IUI = aInspector;
+  this.chromeWin = aWindow;
+  this.tabbrowser = aWindow.gBrowser;
+  this.chromeDoc = aWindow.document;
+  this.browser = aWindow.gBrowser.selectedBrowser;
+  this.events = {};
+
   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");
@@ -44,144 +159,185 @@ Highlighter.prototype = {
     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();
+    this.unlock();
+    this.hide();
   },
 
   /**
-   * Destroy the nodes.
+   * Destroy the nodes. Remove listeners.
    */
   destroy: function Highlighter_destroy()
   {
-    this.IUI.win.clearTimeout(this.transitionDisabler);
-    this.browser.removeEventListener("scroll", this, true);
-    this.browser.removeEventListener("resize", this, true);
+    this.detachKeysListeners();
+    this.detachMouseListeners();
+    this.detachPageListeners();
+
+    this.chromeWin.clearTimeout(this.transitionDisabler);
     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;
+    this.chromeWin = null;
+    this.tabbrowser = null;
+
+    this.emitEvent("closed");
+    this.removeAllListeners();
   },
 
   /**
-   * Highlight this.node, unhilighting first if necessary.
+   * Show the veil, and select a node.
+   * If no node is specified, the previous selected node is highlighted if any.
+   * If no node was selected, the root element is selected.
    *
-   * @param boolean aScroll
-   *        Boolean determining whether to scroll or not.
+   * @param aNode [optional] - The node to be selected.
+   * @param aScroll [optional] boolean
+   *        Should we scroll to ensure that the selected node is visible.
    */
-  highlight: function Highlighter_highlight(aScroll)
+  highlight: function Highlighter_highlight(aNode, aScroll)
+  {
+    if (this.hidden)
+      this.show();
+
+    let oldNode = this.node;
+
+    if (!aNode) {
+      if (!this.node)
+        this.node = this.win.document.documentElement;
+    } else {
+      this.node = aNode;
+    }
+
+    if (oldNode !== this.node) {
+      this.updateInfobar();
+    }
+
+    this.invalidateSize(!!aScroll);
+
+    if (oldNode !== this.node) {
+      this.emitEvent("nodeselected");
+    }
+  },
+
+  /**
+   * Update the highlighter size and position.
+   */
+  invalidateSize: function Highlighter_invalidateSize(aScroll)
   {
     let rect = null;
 
     if (this.node && this.isNodeHighlightable(this.node)) {
 
-      if (aScroll) {
+      if (aScroll &&
+          this.node.scrollIntoView) { // XUL elements don't have such method
         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;
-      }
+      rect = LayoutHelpers.getDirtyRect(this.node);
     }
 
     this.highlightRectangle(rect);
 
     this.moveInfobar();
 
     if (this._highlighting) {
-      Services.obs.notifyObservers(null,
-        INSPECTOR_NOTIFICATIONS.HIGHLIGHTING, null);
+      this.emitEvent("highlighting");
     }
   },
 
   /**
+   * Returns the selected node.
+   *
+   * @returns node
+   */
+  getNode: function() {
+    return this.node;
+  },
+
+  /**
+   * Show the highlighter if it has been hidden.
+   */
+  show: function() {
+    if (!this.hidden) return;
+    this.veilContainer.removeAttribute("hidden");
+    this.nodeInfo.container.removeAttribute("hidden");
+    this.attachKeysListeners();
+    this.attachPageListeners();
+    this.invalidateSize();
+    this.hidden = false;
+  },
+
+  /**
+   * Hide the highlighter, the veil and the infobar.
+   */
+  hide: function() {
+    if (this.hidden) return;
+    this.veilContainer.setAttribute("hidden", "true");
+    this.nodeInfo.container.setAttribute("hidden", "true");
+    this.detachKeysListeners();
+    this.detachPageListeners();
+    this.hidden = true;
+  },
+
+  /**
+   * Is the highlighter visible?
+   *
+   * @return boolean
+   */
+  isHidden: function() {
+    return this.hidden;
+  },
+
+  /**
+   * Lock a node. Stops the inspection.
+   */
+  lock: function() {
+    if (this.locked === true) return;
+    this.veilContainer.setAttribute("locked", "true");
+    this.nodeInfo.container.setAttribute("locked", "true");
+    this.detachMouseListeners();
+    this.locked = true;
+    this.emitEvent("locked");
+  },
+
+  /**
+   * Start inspecting.
+   * Unlock the current node (if any), and select any node being hovered.
+   */
+  unlock: function() {
+    if (this.locked === false) return;
+    this.veilContainer.removeAttribute("locked");
+    this.nodeInfo.container.removeAttribute("locked");
+    this.attachMouseListeners();
+    this.locked = false;
+    this.emitEvent("unlocked");
+  },
+
+  /**
    * 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)
@@ -309,39 +465,16 @@ Highlighter.prototype = {
       idLabel: idLabel,
       classesBox: classesBox,
       container: container,
       barHeight: barHeight,
     };
   },
 
   /**
-   * Is the highlighter highlighting? Public method for querying the state
-   * of the highlighter.
-   */
-  get isHighlighting() {
-    return this._highlighting;
-  },
-
-  /**
-   * 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)
@@ -350,24 +483,20 @@ Highlighter.prototype = {
       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
+      return; // same rectangle
     }
 
-    // adjust rect for zoom scaling
-    let aRectScaled = {};
-    for (let prop in aRect) {
-      aRectScaled[prop] = aRect[prop] * this.zoom;
-    }
+    let aRectScaled = LayoutHelpers.getZoomedRect(this.win, aRect);
 
     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.
@@ -379,30 +508,28 @@ Highlighter.prototype = {
       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;
+    return;
   },
 
   /**
    * 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
@@ -538,51 +665,114 @@ Highlighter.prototype = {
     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,
+    return LayoutHelpers.getElementFromPoint(this.win.document, midpoint.x,
       midpoint.y);
   },
 
   /**
    * Store page zoom factor.
    */
   computeZoomFactor: function Highlighter_computeZoomFactor() {
     this.zoom =
       this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
       .getInterface(Components.interfaces.nsIDOMWindowUtils)
       .screenPixelsPerCSSPixel;
   },
 
   /////////////////////////////////////////////////////////////////////////
+  //// Event Emitter Mechanism
+
+  addListener: function Highlighter_addListener(aEvent, aListener)
+  {
+    if (!(aEvent in this.events))
+      this.events[aEvent] = [];
+    this.events[aEvent].push(aListener);
+  },
+
+  removeListener: function Highlighter_removeListener(aEvent, aListener)
+  {
+    if (!(aEvent in this.events))
+      return;
+    let idx = this.events[aEvent].indexOf(aListener);
+    if (idx > -1)
+      this.events[aEvent].splice(idx, 1);
+  },
+
+  emitEvent: function Highlighter_emitEvent(aEvent, aArgv)
+  {
+    if (!(aEvent in this.events))
+      return;
+
+    let listeners = this.events[aEvent];
+    let highlighter = this;
+    listeners.forEach(function(aListener) {
+      try {
+        aListener.apply(highlighter, aArgv);
+      } catch(e) {}
+    });
+  },
+
+  removeAllListeners: function Highlighter_removeAllIsteners()
+  {
+    for (let event in this.events) {
+      delete this.events[event];
+    }
+  },
+
+  /////////////////////////////////////////////////////////////////////////
   //// Event Handling
 
-  attachInspectListeners: function Highlighter_attachInspectListeners()
+  attachMouseListeners: function Highlighter_attachMouseListeners()
   {
     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()
+  detachMouseListeners: function Highlighter_detachMouseListeners()
   {
     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);
   },
 
+  attachPageListeners: function Highlighter_attachPageListeners()
+  {
+    this.browser.addEventListener("resize", this, true);
+    this.browser.addEventListener("scroll", this, true);
+  },
+
+  detachPageListeners: function Highlighter_detachPageListeners()
+  {
+    this.browser.removeEventListener("resize", this, true);
+    this.browser.removeEventListener("scroll", this, true);
+  },
+
+  attachKeysListeners: function Highlighter_attachKeysListeners()
+  {
+    this.browser.addEventListener("keypress", this, true);
+    this.highlighterContainer.addEventListener("keypress", this, true);
+  },
+
+  detachKeysListeners: function Highlighter_detachKeysListeners()
+  {
+    this.browser.removeEventListener("keypress", this, true);
+    this.highlighterContainer.removeEventListener("keypress", this, true);
+  },
 
   /**
    * Generic event handler.
    *
    * @param nsIDOMEvent aEvent
    *        The DOM event object.
    */
   handleEvent: function Highlighter_handleEvent(aEvent)
@@ -590,47 +780,116 @@ Highlighter.prototype = {
     switch (aEvent.type) {
       case "click":
         this.handleClick(aEvent);
         break;
       case "mousemove":
         this.handleMouseMove(aEvent);
         break;
       case "resize":
+      case "scroll":
         this.computeZoomFactor();
         this.brieflyDisableTransitions();
-        this.handleResize(aEvent);
+        this.invalidateSize();
         break;
       case "dblclick":
       case "mousedown":
       case "mouseup":
         aEvent.stopPropagation();
         aEvent.preventDefault();
         break;
-      case "scroll":
-        this.brieflyDisableTransitions();
-        this.highlight();
         break;
+      case "keypress":
+        switch (aEvent.keyCode) {
+          case this.chromeWin.KeyEvent.DOM_VK_RETURN:
+            this.locked ? this.unlock() : this.lock();
+            aEvent.preventDefault();
+            aEvent.stopPropagation();
+            break;
+          case this.chromeWin.KeyEvent.DOM_VK_LEFT:
+            let node;
+            if (this.node) {
+              node = this.node.parentNode;
+            } else {
+              node = this.defaultSelection;
+            }
+            if (node && this.isNodeHighlightable(node)) {
+              this.highlight(node);
+            }
+            aEvent.preventDefault();
+            aEvent.stopPropagation();
+            break;
+          case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
+            if (this.node) {
+              // Find the first child that is highlightable.
+              for (let i = 0; i < this.node.childNodes.length; i++) {
+                node = this.node.childNodes[i];
+                if (node && this.isNodeHighlightable(node)) {
+                  break;
+                }
+              }
+            } else {
+              node = this.defaultSelection;
+            }
+            if (node && this.isNodeHighlightable(node)) {
+              this.highlight(node, true);
+            }
+            aEvent.preventDefault();
+            aEvent.stopPropagation();
+            break;
+          case this.chromeWin.KeyEvent.DOM_VK_UP:
+            if (this.node) {
+              // Find a previous sibling that is highlightable.
+              node = this.node.previousSibling;
+              while (node && !this.isNodeHighlightable(node)) {
+                node = node.previousSibling;
+              }
+            } else {
+              node = this.defaultSelection;
+            }
+            if (node && this.isNodeHighlightable(node)) {
+              this.highlight(node, true);
+            }
+            aEvent.preventDefault();
+            aEvent.stopPropagation();
+            break;
+          case this.chromeWin.KeyEvent.DOM_VK_DOWN:
+            if (this.node) {
+              // Find a next sibling that is highlightable.
+              node = this.node.nextSibling;
+              while (node && !this.isNodeHighlightable(node)) {
+                node = node.nextSibling;
+              }
+            } else {
+              node = this.defaultSelection;
+            }
+            if (node && this.isNodeHighlightable(node)) {
+              this.highlight(node, true);
+            }
+            aEvent.preventDefault();
+            aEvent.stopPropagation();
+            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);
+     this.chromeWin.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.chromeWin.setTimeout(function() {
        this.veilContainer.removeAttribute("disable-transitions");
        this.nodeInfo.container.removeAttribute("disable-transitions");
        this.transitionDisabler = null;
      }.bind(this), 500);
   },
 
   /**
    * Handle clicks.
@@ -638,41 +897,33 @@ Highlighter.prototype = {
    * @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();
+      this.lock();
       win.focus();
     }
     aEvent.preventDefault();
     aEvent.stopPropagation();
   },
 
   /**
-   * Handle mousemoves in panel when InspectorUI.inspecting is true.
+   * Handle mousemoves in panel.
    *
    * @param nsiDOMEvent aEvent
    *        The MouseEvent triggering the method.
    */
   handleMouseMove: function Highlighter_handleMouseMove(aEvent)
   {
-    let element = this.IUI.elementFromPoint(aEvent.target.ownerDocument,
+    let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
       aEvent.clientX, aEvent.clientY);
     if (element && element != this.node) {
-      this.IUI.inspectNode(element);
+      this.highlight(element);
     }
   },
-
-  /**
-   * Handle window resize events.
-   */
-  handleResize: function Highlighter_handleResize()
-  {
-    this.highlight();
-  },
 };
 
 ///////////////////////////////////////////////////////////////////////////
 
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -47,37 +47,21 @@ const Ci = Components.interfaces;
 const Cr = Components.results;
 
 var EXPORTED_SYMBOLS = ["InspectorUI"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/TreePanel.jsm");
 Cu.import("resource:///modules/devtools/CssRuleView.jsm");
-
-const INSPECTOR_INVISIBLE_ELEMENTS = {
-  "head": true,
-  "base": true,
-  "basefont": true,
-  "isindex": true,
-  "link": true,
-  "meta": true,
-  "script": true,
-  "style": true,
-  "title": true,
-};
+Cu.import("resource:///modules/highlighter.jsm");
+Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 
 // Inspector notifications dispatched through the nsIObserverService.
 const INSPECTOR_NOTIFICATIONS = {
-  // Fires once the Inspector highlights an element in the page.
-  HIGHLIGHTING: "inspector-highlighting",
-
-  // Fires once the Inspector stops highlighting any element.
-  UNHIGHLIGHTING: "inspector-unhighlighting",
-
   // Fires once the Inspector completes the initialization and opens up on
   // screen.
   OPENED: "inspector-opened",
 
   // Fires once the Inspector is closed.
   CLOSED: "inspector-closed",
 
   // Fires once the Inspector is destroyed. Not fired on tab switch.
@@ -293,18 +277,21 @@ InspectorUI.prototype = {
 
     // initialize the HTML Breadcrumbs
     this.breadcrumbs = new HTMLBreadcrumbs(this);
 
     this.isDirty = false;
 
     this.progressListener = new InspectorProgressListener(this);
 
+    this.chromeWin.addEventListener("keypress", this, false);
+
     // initialize the highlighter
-    this.initializeHighlighter();
+    this.highlighter = new Highlighter(this.chromeWin);
+    this.highlighterReady();
   },
 
   /**
    * Register the Rule View in the Sidebar.
    */
   registerRuleView: function IUI_registerRuleView()
   {
     let isOpen = this.isRuleViewOpen.bind(this);
@@ -331,27 +318,16 @@ InspectorUI.prototype = {
    * Register and initialize any included tools.
    */
   initTools: function IUI_initTools()
   {
     // Extras go here.
   },
 
   /**
-   * Initialize highlighter.
-   */
-  initializeHighlighter: function IUI_initializeHighlighter()
-  {
-    this.highlighter = new Highlighter(this);
-    this.browser.addEventListener("keypress", this, true);
-    this.highlighter.highlighterContainer.addEventListener("keypress", this, true);
-    this.highlighterReady();
-  },
-
-  /**
    * Initialize the InspectorStore.
    */
   initializeStore: function IUI_initializeStore()
   {
     // First time opened, add the TabSelect listener
     if (this.store.isEmpty()) {
       this.tabbrowser.tabContainer.addEventListener("TabSelect", this, false);
     }
@@ -414,31 +390,29 @@ InspectorUI.prototype = {
       this.store.setValue(this.winID, "inspecting", this.inspecting);
       this.store.setValue(this.winID, "isDirty", this.isDirty);
     }
 
     if (this.store.isEmpty()) {
       this.tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
     }
 
+    this.chromeWin.removeEventListener("keypress", this, false);
+
     this.stopInspecting();
-    this.browser.removeEventListener("keypress", this, true);
 
     this.saveToolState(this.winID);
     this.toolsDo(function IUI_toolsHide(aTool) {
       this.unregisterTool(aTool);
     }.bind(this));
 
     // close the sidebar
     this.hideSidebar();
 
     if (this.highlighter) {
-      this.highlighter.highlighterContainer.removeEventListener("keypress",
-                                                                this,
-                                                                true);
       this.highlighter.destroy();
       this.highlighter = null;
     }
 
     if (this.breadcrumbs) {
       this.breadcrumbs.destroy();
       this.breadcrumbs = null;
     }
@@ -465,52 +439,44 @@ InspectorUI.prototype = {
   startInspecting: function IUI_startInspecting()
   {
     // if currently editing an attribute value, starting
     // "live inspection" mode closes the editor
     if (this.treePanel && this.treePanel.editingContext)
       this.treePanel.closeEditor();
 
     this.inspectToolbutton.checked = true;
-    this.highlighter.attachInspectListeners();
 
     this.inspecting = true;
     this.toolsDim(true);
-    this.highlighter.veilContainer.removeAttribute("locked");
-    this.highlighter.nodeInfo.container.removeAttribute("locked");
+    this.highlighter.unlock();
   },
 
   /**
    * Stop inspecting webpage, detach page listeners, disable highlighter
    * event listeners.
    * @param aPreventScroll
    *        Prevent scroll in the HTML tree?
    */
   stopInspecting: function IUI_stopInspecting(aPreventScroll)
   {
     if (!this.inspecting) {
       return;
     }
 
     this.inspectToolbutton.checked = false;
-    // Detach event listeners from content window and child windows to disable
-    // highlighting. We still want to be notified if the user presses "ESCAPE"
-    // to close the inspector, or "RETURN" to unlock the node, so we don't 
-    // remove the "keypress" event until the highlighter is removed.
-    this.highlighter.detachInspectListeners();
 
     this.inspecting = false;
     this.toolsDim(false);
-    if (this.highlighter.node) {
-      this.select(this.highlighter.node, true, true, !aPreventScroll);
+    if (this.highlighter.getNode()) {
+      this.select(this.highlighter.getNode(), true, true, !aPreventScroll);
     } else {
       this.select(null, true, true);
     }
-    this.highlighter.veilContainer.setAttribute("locked", true);
-    this.highlighter.nodeInfo.container.setAttribute("locked", true);
+    this.highlighter.lock();
   },
 
   /**
    * Select an object in the tree view.
    * @param aNode
    *        node to inspect
    * @param forceUpdate
    *        force an update?
@@ -525,17 +491,17 @@ InspectorUI.prototype = {
       this.treePanel.closeEditor();
 
     if (!aNode)
       aNode = this.defaultSelection;
 
     if (forceUpdate || aNode != this.selection) {
       this.selection = aNode;
       if (!this.inspecting) {
-        this.highlighter.highlightNode(this.selection);
+        this.highlighter.highlight(this.selection);
       }
     }
 
     this.breadcrumbs.update();
     this.chromeWin.Tilt.update(aNode);
 
     this.toolsSelect(aScroll);
   },
@@ -544,37 +510,53 @@ InspectorUI.prototype = {
    * Called when the highlighted node is changed by a tool.
    *
    * @param object aUpdater
    *        The tool that triggered the update (if any), that tool's
    *        onChanged will not be called.
    */
   nodeChanged: function IUI_nodeChanged(aUpdater)
   {
-    this.highlighter.highlight();
+    this.highlighter.invalidateSize();
     this.toolsOnChanged(aUpdater);
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Event Handling
 
   highlighterReady: function IUI_highlighterReady()
   {
     // Setup the InspectorStore or restore state
     this.initializeStore();
 
+    let self = this;
+
+    this.highlighter.addListener("locked", function() {
+      self.stopInspecting();
+    });
+
+    this.highlighter.addListener("unlocked", function() {
+      self.startInspecting();
+    });
+
+    this.highlighter.addListener("nodeselected", function() {
+      self.select(self.highlighter.getNode(), false, false);
+    });
+
     if (this.store.getValue(this.winID, "inspecting")) {
       this.startInspecting();
     }
 
     this.restoreToolState(this.winID);
 
     this.win.focus();
     Services.obs.notifyObservers({wrappedJSObject: this},
                                  INSPECTOR_NOTIFICATIONS.OPENED, null);
+
+    this.highlighter.highlight();
   },
 
   /**
    * Main callback handler for events.
    *
    * @param event
    *        The event to be handled.
    */
@@ -631,84 +613,16 @@ InspectorUI.prototype = {
         break;
       case "keypress":
         switch (event.keyCode) {
           case this.chromeWin.KeyEvent.DOM_VK_ESCAPE:
             this.closeInspectorUI(false);
             event.preventDefault();
             event.stopPropagation();
             break;
-          case this.chromeWin.KeyEvent.DOM_VK_RETURN:
-            this.toggleInspection();
-            event.preventDefault();
-            event.stopPropagation();
-            break;
-          case this.chromeWin.KeyEvent.DOM_VK_LEFT:
-            let node;
-            if (this.selection) {
-              node = this.selection.parentNode;
-            } else {
-              node = this.defaultSelection;
-            }
-            if (node && this.highlighter.isNodeHighlightable(node)) {
-              this.inspectNode(node, true);
-            }
-            event.preventDefault();
-            event.stopPropagation();
-            break;
-          case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
-            if (this.selection) {
-              // Find the first child that is highlightable.
-              for (let i = 0; i < this.selection.childNodes.length; i++) {
-                node = this.selection.childNodes[i];
-                if (node && this.highlighter.isNodeHighlightable(node)) {
-                  break;
-                }
-              }
-            } else {
-              node = this.defaultSelection;
-            }
-            if (node && this.highlighter.isNodeHighlightable(node)) {
-              this.inspectNode(node, true);
-            }
-            event.preventDefault();
-            event.stopPropagation();
-            break;
-          case this.chromeWin.KeyEvent.DOM_VK_UP:
-            if (this.selection) {
-              // Find a previous sibling that is highlightable.
-              node = this.selection.previousSibling;
-              while (node && !this.highlighter.isNodeHighlightable(node)) {
-                node = node.previousSibling;
-              }
-            } else {
-              node = this.defaultSelection;
-            }
-            if (node && this.highlighter.isNodeHighlightable(node)) {
-              this.inspectNode(node, true);
-            }
-            event.preventDefault();
-            event.stopPropagation();
-            break;
-          case this.chromeWin.KeyEvent.DOM_VK_DOWN:
-            if (this.selection) {
-              // Find a next sibling that is highlightable.
-              node = this.selection.nextSibling;
-              while (node && !this.highlighter.isNodeHighlightable(node)) {
-                node = node.nextSibling;
-              }
-            } else {
-              node = this.defaultSelection;
-            }
-            if (node && this.highlighter.isNodeHighlightable(node)) {
-              this.inspectNode(node, true);
-            }
-            event.preventDefault();
-            event.stopPropagation();
-            break;
         }
         break;
     }
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// CssRuleView methods
 
@@ -821,88 +735,23 @@ InspectorUI.prototype = {
    * @param aNode
    *        the element in the document to inspect
    * @param aScroll
    *        force scroll?
    */
   inspectNode: function IUI_inspectNode(aNode, aScroll)
   {
     this.select(aNode, true, true);
-    this.highlighter.highlightNode(aNode, { scroll: aScroll });
-  },
-
-  /**
-   * Find an element from the given coordinates. This method descends through
-   * frames to find the element the user clicked inside frames.
-   *
-   * @param DOMDocument aDocument the document to look into.
-   * @param integer aX
-   * @param integer aY
-   * @returns Node|null the element node found at the given coordinates.
-   */
-  elementFromPoint: function IUI_elementFromPoint(aDocument, aX, aY)
-  {
-    let node = aDocument.elementFromPoint(aX, aY);
-    if (node && node.contentDocument) {
-      if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
-        let rect = node.getBoundingClientRect();
-
-        // Gap between the iframe and its content window.
-        let [offsetTop, offsetLeft] = this.getIframeContentOffset(node);
-
-        aX -= rect.left + offsetLeft;
-        aY -= rect.top + offsetTop;
-
-        if (aX < 0 || aY < 0) {
-          // Didn't reach the content document, still over the iframe.
-          return node;
-        }
-      }
-      if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
-          node instanceof Ci.nsIDOMHTMLFrameElement) {
-        let subnode = this.elementFromPoint(node.contentDocument, aX, aY);
-        if (subnode) {
-          node = subnode;
-        }
-      }
-    }
-    return node;
+    this.highlighter.highlight(aNode, aScroll);
   },
 
   ///////////////////////////////////////////////////////////////////////////
   //// Utility functions
 
   /**
-   * Returns iframe content offset (iframe border + padding).
-   * Note: this function shouldn't need to exist, had the platform provided a
-   * suitable API for determining the offset between the iframe's content and
-   * its bounding client rect. Bug 626359 should provide us with such an API.
-   *
-   * @param aIframe
-   *        The iframe.
-   * @returns array [offsetTop, offsetLeft]
-   *          offsetTop is the distance from the top of the iframe and the
-   *            top of the content document.
-   *          offsetLeft is the distance from the left of the iframe and the
-   *            left of the content document.
-   */
-  getIframeContentOffset: function IUI_getIframeContentOffset(aIframe)
-  {
-    let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
-
-    let paddingTop = parseInt(style.getPropertyValue("padding-top"));
-    let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
-
-    let borderTop = parseInt(style.getPropertyValue("border-top-width"));
-    let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
-
-    return [borderTop + paddingTop, borderLeft + paddingLeft];
-  },
-
-  /**
    * Retrieve the unique ID of a window object.
    *
    * @param nsIDOMWindow aWindow
    * @returns integer ID
    */
   getWindowID: function IUI_getWindowID(aWindow)
   {
     if (!aWindow) {
@@ -1247,16 +1096,22 @@ InspectorUI.prototype = {
         }
       }.bind(this));
       this.sidebarTools.forEach(function(tool) {
         if (tool != activeSidebarTool)
           this.chromeDoc.getElementById(
             this.getToolbarButtonId(tool.id)).removeAttribute("checked");
       }.bind(this));
     }
+    if (this.store.getValue(this.winID, "inspecting")) {
+      this.highlighter.unlock();
+    } else {
+      this.highlighter.lock();
+    }
+
     Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null);
   },
 
   /**
    * For each tool in the tools collection select the current node that is
    * selected in the highlighter
    * @param aScroll boolean
    *        Do you want to scroll the treepanel?
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/LayoutHelpers.jsm
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Mozilla LayoutHelpers Module.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Rob Campbell <rcampbell@mozilla.com> (original author)
+ *   Mihai Șucan <mihai.sucan@gmail.com>
+ *   Julian Viereck <jviereck@mozilla.com>
+ *   Paul Rouget <paul@mozilla.com>
+ *   Kyle Simpson <ksimpson@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+var EXPORTED_SYMBOLS = ["LayoutHelpers"];
+
+LayoutHelpers = {
+
+  /**
+   * Compute the position and the dimensions for the visible portion
+   * of a node, relativalely to the root window.
+   *
+   * @param nsIDOMNode aNode
+   *        a DOM element to be highlighted
+   */
+  getDirtyRect: function LH_getDirectyRect(aNode) {
+    let frameWin = aNode.ownerDocument.defaultView;
+    let clientRect = aNode.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};
+
+    // 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.getIframeContentOffset(frameWin.frameElement);
+
+      rect.top += frameRect.top + offsetTop;
+      rect.left += frameRect.left + offsetLeft;
+
+      frameWin = frameWin.parent;
+    }
+
+    return rect;
+  },
+
+  /**
+   * Returns iframe content offset (iframe border + padding).
+   * Note: this function shouldn't need to exist, had the platform provided a
+   * suitable API for determining the offset between the iframe's content and
+   * its bounding client rect. Bug 626359 should provide us with such an API.
+   *
+   * @param aIframe
+   *        The iframe.
+   * @returns array [offsetTop, offsetLeft]
+   *          offsetTop is the distance from the top of the iframe and the
+   *            top of the content document.
+   *          offsetLeft is the distance from the left of the iframe and the
+   *            left of the content document.
+   */
+  getIframeContentOffset: function LH_getIframeContentOffset(aIframe) {
+    let style = aIframe.contentWindow.getComputedStyle(aIframe, null);
+
+    let paddingTop = parseInt(style.getPropertyValue("padding-top"));
+    let paddingLeft = parseInt(style.getPropertyValue("padding-left"));
+
+    let borderTop = parseInt(style.getPropertyValue("border-top-width"));
+    let borderLeft = parseInt(style.getPropertyValue("border-left-width"));
+
+    return [borderTop + paddingTop, borderLeft + paddingLeft];
+  },
+
+  /**
+   * Apply the page zoom factor.
+   */
+  getZoomedRect: function LH_getZoomedRect(aWin, aRect) {
+    // get page zoom factor, if any
+    let zoom =
+      aWin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+        .getInterface(Components.interfaces.nsIDOMWindowUtils)
+        .screenPixelsPerCSSPixel;
+
+    // adjust rect for zoom scaling
+    let aRectScaled = {};
+    for (let prop in aRect) {
+      aRectScaled[prop] = aRect[prop] * zoom;
+    }
+
+    return aRectScaled;
+  },
+
+
+  /**
+   * Find an element from the given coordinates. This method descends through
+   * frames to find the element the user clicked inside frames.
+   *
+   * @param DOMDocument aDocument the document to look into.
+   * @param integer aX
+   * @param integer aY
+   * @returns Node|null the element node found at the given coordinates.
+   */
+  getElementFromPoint: function LH_elementFromPoint(aDocument, aX, aY)
+  {
+    let node = aDocument.elementFromPoint(aX, aY);
+    if (node && node.contentDocument) {
+      if (node instanceof Ci.nsIDOMHTMLIFrameElement) {
+        let rect = node.getBoundingClientRect();
+
+        // Gap between the iframe and its content window.
+        let [offsetTop, offsetLeft] = LayoutHelpers.getIframeContentOffset(node);
+
+        aX -= rect.left + offsetLeft;
+        aY -= rect.top + offsetTop;
+
+        if (aX < 0 || aY < 0) {
+          // Didn't reach the content document, still over the iframe.
+          return node;
+        }
+      }
+      if (node instanceof Ci.nsIDOMHTMLIFrameElement ||
+          node instanceof Ci.nsIDOMHTMLFrameElement) {
+        let subnode = this.getElementFromPoint(node.contentDocument, aX, aY);
+        if (subnode) {
+          node = subnode;
+        }
+      }
+    }
+    return node;
+  },
+};
--- a/browser/devtools/shared/Makefile.in
+++ b/browser/devtools/shared/Makefile.in
@@ -49,8 +49,9 @@ ifdef ENABLE_TESTS
 	DIRS += test
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 libs::
 	$(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools
 	$(NSINSTALL) $(srcdir)/Promise.jsm $(FINAL_TARGET)/modules/devtools
+	$(NSINSTALL) $(srcdir)/LayoutHelpers.jsm $(FINAL_TARGET)/modules/devtools