Bug 912915 - Implement a simple generic highlighter. r=jwalker
authorPaul Rouget <paul@mozilla.com>
Sat, 07 Sep 2013 11:39:50 +0200
changeset 146079 7015fcdd43a24c5dcdc53e166e3ec7294dfb74bf
parent 146078 e2bf999b88570725efd0736a5ed57c4e74974fd0
child 146080 0899b763a06608d2ecab5f2e2b96b437e828de32
push id33462
push userphilringnalda@gmail.com
push dateSun, 08 Sep 2013 15:39:49 +0000
treeherdermozilla-inbound@c7cc85e13f7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker
bugs912915
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 912915 - Implement a simple generic highlighter. r=jwalker
browser/devtools/commandline/BuiltinCommands.jsm
browser/devtools/debugger/debugger-controller.js
browser/devtools/inspector/breadcrumbs.js
browser/devtools/inspector/highlighter.js
browser/devtools/inspector/inspector-panel.js
browser/devtools/inspector/inspector.xul
browser/devtools/inspector/test/Makefile.in
browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
browser/devtools/inspector/test/browser_inspector_destroyselection.js
browser/devtools/inspector/test/head.js
browser/devtools/layoutview/view.js
browser/devtools/markupview/markup-view.js
browser/devtools/scratchpad/scratchpad.js
browser/devtools/shared/LayoutHelpers.jsm
browser/devtools/shared/test/browser_layoutHelpers.js
browser/devtools/tilt/test/head.js
browser/devtools/tilt/tilt-utils.js
toolkit/devtools/LayoutHelpers.jsm
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/commandline/BuiltinCommands.jsm
+++ b/browser/devtools/commandline/BuiltinCommands.jsm
@@ -1643,17 +1643,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     }
   });
 }(this));
 
 /* CmdScreenshot ----------------------------------------------------------- */
 
 (function(module) {
   XPCOMUtils.defineLazyModuleGetter(this, "LayoutHelpers",
-                                    "resource:///modules/devtools/LayoutHelpers.jsm");
+                                    "resource://gre/modules/devtools/LayoutHelpers.jsm");
 
   // String used as an indication to generate default file name in the following
   // format: "Screen Shot yyyy-mm-dd at HH.MM.SS.png"
   const FILENAME_DEFAULT_VALUE = " ";
 
   /**
    * 'screenshot' command
    */
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -14,17 +14,17 @@ const FETCH_SOURCE_RESPONSE_DELAY = 50; 
 const FRAME_STEP_CLEAR_DELAY = 100; // ms
 const CALL_STACK_PAGE_SIZE = 25; // frames
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource:///modules/source-editor.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/BreadcrumbsWidget.jsm");
 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
 Cu.import("resource:///modules/devtools/VariablesView.jsm");
 Cu.import("resource:///modules/devtools/VariablesViewController.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Parser",
   "resource:///modules/devtools/Parser.jsm");
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -6,17 +6,17 @@
 
 const {Cc, Cu, Ci} = require("chrome");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 const ENSURE_SELECTION_VISIBLE_DELAY = 50; // ms
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/DOMHelpers.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let promise = require("sdk/core/promise");
 
 const LOW_PRIORITY_ELEMENTS = {
   "HEAD": true,
   "BASE": true,
   "BASEFONT": true,
--- a/browser/devtools/inspector/highlighter.js
+++ b/browser/devtools/inspector/highlighter.js
@@ -2,24 +2,37 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 const {Cu, Cc, Ci} = require("chrome");
 
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let EventEmitter = require("devtools/shared/event-emitter");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
   // add ":visited" and ":link" after bug 713106 is fixed
 
+exports._forceBasic = {value: false};
+
+exports.Highlighter = function Highlighter(aTarget, aInspector, aToolbox) {
+  if (aTarget.isLocalTab && !exports._forceBasic.value) {
+    return new LocalHighlighter(aTarget, aInspector, aToolbox);
+  } else {
+    return new BasicHighlighter(aTarget, aInspector, aToolbox);
+  }
+}
+
+exports.LocalHighlighter = LocalHighlighter;
+exports.BasicHighlighter = BasicHighlighter;
+
 /**
  * A highlighter mechanism.
  *
  * 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:
@@ -66,39 +79,37 @@ const PSEUDO_CLASSES = [":hover", ":acti
 
 /**
  * Constructor.
  *
  * @param aTarget The inspection target.
  * @param aInspector Inspector panel.
  * @param aToolbox The toolbox holding the inspector.
  */
-function Highlighter(aTarget, aInspector, aToolbox)
+function LocalHighlighter(aTarget, aInspector, aToolbox)
 {
   this.target = aTarget;
   this.tab = aTarget.tab;
   this.toolbox = aToolbox;
   this.browser = this.tab.linkedBrowser;
   this.chromeDoc = this.tab.ownerDocument;
   this.chromeWin = this.chromeDoc.defaultView;
   this.inspector = aInspector
 
   EventEmitter.decorate(this);
 
   this._init();
 }
 
-exports.Highlighter = Highlighter;
-
-Highlighter.prototype = {
+LocalHighlighter.prototype = {
   get selection() {
     return this.inspector.selection;
   },
 
-  _init: function Highlighter__init()
+  _init: function LocalHighlighter__init()
   {
     this.toggleLockState = this.toggleLockState.bind(this);
     this.unlockAndFocus = this.unlockAndFocus.bind(this);
     this.updateInfobar = this.updateInfobar.bind(this);
     this.highlight = this.highlight.bind(this);
 
     let stack = this.browser.parentNode;
     this.win = this.browser.contentWindow;
@@ -152,17 +163,17 @@ Highlighter.prototype = {
 
     this.hidden = true;
     this.highlight();
   },
 
   /**
    * Destroy the nodes. Remove listeners.
    */
-  destroy: function Highlighter_destroy()
+  destroy: function LocalHighlighter_destroy()
   {
     this.inspectButton.removeEventListener("command", this.unlockAndFocus);
     this.inspectButton = null;
 
     this.toolbox.off("select", this.onToolSelected);
     this.toolbox = null;
 
     this.selection.off("new-node", this.highlight);
@@ -190,17 +201,17 @@ Highlighter.prototype = {
     this.tabbrowser = null;
 
     this.emit("closed");
   },
 
   /**
    * Show the outline, and select a node.
    */
-  highlight: function Highlighter_highlight()
+  highlight: function LocalHighlighter_highlight()
   {
     if (this.selection.reason != "highlighter") {
       this.lock();
     }
 
     let canHighlightNode = this.selection.isNode() &&
                           this.selection.isConnected() &&
                           this.selection.isElementNode();
@@ -220,17 +231,17 @@ Highlighter.prototype = {
       this.disabled = true;
       this.hide();
     }
   },
 
   /**
    * Update the highlighter size and position.
    */
-  invalidateSize: function Highlighter_invalidateSize()
+  invalidateSize: function LocalHighlighter_invalidateSize()
   {
     let canHiglightNode = this.selection.isNode() &&
                           this.selection.isConnected() &&
                           this.selection.isElementNode();
 
     if (!canHiglightNode)
       return;
 
@@ -328,76 +339,76 @@ Highlighter.prototype = {
       this.selection.setNode(this.startNode);
       this.lock();
     }
   },
 
   /**
    * Focus the browser before unlocking.
    */
-  unlockAndFocus: function Highlighter_unlockAndFocus() {
+  unlockAndFocus: function LocalHighlighter_unlockAndFocus() {
     if (this.locked === false) return;
     this.chromeWin.focus();
     this.unlock();
   },
 
   /**
    * Hide the infobar
    */
-   hideInfobar: function Highlighter_hideInfobar() {
+   hideInfobar: function LocalHighlighter_hideInfobar() {
      this.nodeInfo.container.setAttribute("force-transitions", "true");
      this.nodeInfo.container.setAttribute("hidden", "true");
    },
 
   /**
    * Show the infobar
    */
-   showInfobar: function Highlighter_showInfobar() {
+   showInfobar: function LocalHighlighter_showInfobar() {
      this.nodeInfo.container.removeAttribute("hidden");
      this.moveInfobar();
      this.nodeInfo.container.removeAttribute("force-transitions");
    },
 
   /**
    * Hide the outline
    */
-   hideOutline: function Highlighter_hideOutline() {
+   hideOutline: function LocalHighlighter_hideOutline() {
      this.outline.setAttribute("hidden", "true");
    },
 
   /**
    * Show the outline
    */
-   showOutline: function Highlighter_showOutline() {
+   showOutline: function LocalHighlighter_showOutline() {
      if (this._highlighting)
        this.outline.removeAttribute("hidden");
    },
 
   /**
    * Build the node Infobar.
    *
    * <box class="highlighter-nodeinfobar-container">
-   *   <box class="Highlighter-nodeinfobar-arrow-top"/>
+   *   <box class="highlighter-nodeinfobar-arrow-top"/>
    *   <hbox class="highlighter-nodeinfobar">
    *     <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-inspectbutton"/>
    *     <hbox class="highlighter-nodeinfobar-text">
    *       <xhtml:span class="highlighter-nodeinfobar-tagname"/>
    *       <xhtml:span class="highlighter-nodeinfobar-id"/>
    *       <xhtml:span class="highlighter-nodeinfobar-classes"/>
    *       <xhtml:span class="highlighter-nodeinfobar-pseudo-classes"/>
    *     </hbox>
    *     <toolbarbutton class="highlighter-nodeinfobar-button highlighter-nodeinfobar-menu"/>
    *   </hbox>
-   *   <box class="Highlighter-nodeinfobar-arrow-bottom"/>
+   *   <box class="highlighter-nodeinfobar-arrow-bottom"/>
    * </box>
    *
    * @param nsIDOMElement aParent
    *        The container of the infobar.
    */
-  buildInfobar: function Highlighter_buildInfobar(aParent)
+  buildInfobar: function LocalHighlighter_buildInfobar(aParent)
   {
     let container = this.chromeDoc.createElement("box");
     container.className = "highlighter-nodeinfobar-container";
     container.setAttribute("position", "top");
     container.setAttribute("disabled", "true");
 
     let nodeInfobar = this.chromeDoc.createElement("hbox");
     nodeInfobar.className = "highlighter-nodeinfobar";
@@ -484,17 +495,17 @@ Highlighter.prototype = {
   /**
    * 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)
+  highlightRectangle: function LocalHighlighter_highlightRectangle(aRect)
   {
     if (!aRect) {
       this.unhighlight();
       return;
     }
 
     let oldRect = this._contentRect;
 
@@ -527,26 +538,26 @@ Highlighter.prototype = {
     this._highlightRect = aRectScaled; // and save the scaled rect.
 
     return;
   },
 
   /**
    * Clear the highlighter surface.
    */
-  unhighlight: function Highlighter_unhighlight()
+  unhighlight: function LocalHighlighter_unhighlight()
   {
     this._highlighting = false;
     this.hideOutline();
   },
 
   /**
    * Update node information (tagName#id.class)
    */
-  updateInfobar: function Highlighter_updateInfobar()
+  updateInfobar: function LocalHighlighter_updateInfobar()
   {
     if (!this.selection.isElementNode()) {
       this.nodeInfo.tagNameLabel.textContent = "";
       this.nodeInfo.idLabel.textContent = "";
       this.nodeInfo.classesBox.textContent = "";
       this.nodeInfo.pseudoClassesBox.textContent = "";
       return;
     }
@@ -572,17 +583,17 @@ Highlighter.prototype = {
 
     let pseudoBox = this.nodeInfo.pseudoClassesBox;
     pseudoBox.textContent = pseudos.join("");
   },
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
-  moveInfobar: function Highlighter_moveInfobar()
+  moveInfobar: function LocalHighlighter_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,
@@ -639,65 +650,65 @@ Highlighter.prototype = {
       this.nodeInfo.container.setAttribute("position", "top");
       this.nodeInfo.container.setAttribute("hide-arrow", "true");
     }
   },
 
   /**
    * Store page zoom factor.
    */
-  computeZoomFactor: function Highlighter_computeZoomFactor() {
+  computeZoomFactor: function LocalHighlighter_computeZoomFactor() {
     this.zoom =
       this.win.QueryInterface(Ci.nsIInterfaceRequestor)
       .getInterface(Ci.nsIDOMWindowUtils)
       .fullZoom;
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Event Handling
 
-  attachMouseListeners: function Highlighter_attachMouseListeners()
+  attachMouseListeners: function LocalHighlighter_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);
   },
 
-  detachMouseListeners: function Highlighter_detachMouseListeners()
+  detachMouseListeners: function LocalHighlighter_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()
+  attachPageListeners: function LocalHighlighter_attachPageListeners()
   {
     this.browser.addEventListener("resize", this, true);
     this.browser.addEventListener("scroll", this, true);
     this.browser.addEventListener("MozAfterPaint", this, true);
   },
 
-  detachPageListeners: function Highlighter_detachPageListeners()
+  detachPageListeners: function LocalHighlighter_detachPageListeners()
   {
     this.browser.removeEventListener("resize", this, true);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("MozAfterPaint", this, true);
   },
 
   /**
    * Generic event handler.
    *
    * @param nsIDOMEvent aEvent
    *        The DOM event object.
    */
-  handleEvent: function Highlighter_handleEvent(aEvent)
+  handleEvent: function LocalHighlighter_handleEvent(aEvent)
   {
     switch (aEvent.type) {
       case "click":
         this.handleClick(aEvent);
         break;
       case "mousemove":
         this.brieflyIgnorePageEvents();
         this.handleMouseMove(aEvent);
@@ -718,17 +729,17 @@ Highlighter.prototype = {
         break;
     }
   },
 
   /**
    * Disable the CSS transitions for a short time to avoid laggy animations
    * during scrolling or resizing.
    */
-  brieflyDisableTransitions: function Highlighter_brieflyDisableTransitions()
+  brieflyDisableTransitions: function LocalHighlighter_brieflyDisableTransitions()
   {
     if (this.transitionDisabler) {
       this.chromeWin.clearTimeout(this.transitionDisabler);
     } else {
       this.outline.setAttribute("disable-transitions", "true");
       this.nodeInfo.container.setAttribute("disable-transitions", "true");
     }
     this.transitionDisabler =
@@ -737,17 +748,17 @@ Highlighter.prototype = {
         this.nodeInfo.container.removeAttribute("disable-transitions");
         this.transitionDisabler = null;
       }.bind(this), 500);
   },
 
   /**
    * Don't listen to page events while inspecting with the mouse.
    */
-  brieflyIgnorePageEvents: function Highlighter_brieflyIgnorePageEvents()
+  brieflyIgnorePageEvents: function LocalHighlighter_brieflyIgnorePageEvents()
   {
     // The goal is to keep smooth animations while inspecting.
     // CSS Transitions might be interrupted because of a MozAfterPaint
     // event that would triger an invalidateSize() call.
     // So we don't listen to events that would trigger an invalidateSize()
     // call.
     //
     // Side effect, zoom levels are not updated during this short period.
@@ -768,17 +779,17 @@ Highlighter.prototype = {
   },
 
   /**
    * Handle clicks.
    *
    * @param nsIDOMEvent aEvent
    *        The DOM event.
    */
-  handleClick: function Highlighter_handleClick(aEvent)
+  handleClick: function LocalHighlighter_handleClick(aEvent)
   {
     // Stop inspection when the user clicks on a node.
     if (aEvent.button == 0) {
       this.lock();
       let node = this.selection.node;
       this.selection.setNode(node, "highlighter-lock");
       aEvent.preventDefault();
       aEvent.stopPropagation();
@@ -786,34 +797,79 @@ Highlighter.prototype = {
   },
 
   /**
    * Handle mousemoves in panel.
    *
    * @param nsiDOMEvent aEvent
    *        The MouseEvent triggering the method.
    */
-  handleMouseMove: function Highlighter_handleMouseMove(aEvent)
+  handleMouseMove: function LocalHighlighter_handleMouseMove(aEvent)
   {
     let doc = aEvent.target.ownerDocument;
 
     // This should never happen, but just in case, we don't let the
     // highlighter highlight browser nodes.
     if (doc && doc != this.chromeDoc) {
       let element = LayoutHelpers.getElementFromPoint(aEvent.target.ownerDocument,
         aEvent.clientX, aEvent.clientY);
       if (element && element != this.selection.node) {
         this.selection.setNode(element, "highlighter");
       }
     }
   },
 };
 
+// BasicHighlighter. Doesn't implement any fancy features. Just change
+// the outline of the selected node. Works with remote target.
+
+function BasicHighlighter(aTarget, aInspector)
+{
+  this.walker = aInspector.walker;
+  this.selection = aInspector.selection;
+  this.highlight = this.highlight.bind(this);
+  this.selection.on("new-node-front", this.highlight);
+  EventEmitter.decorate(this);
+  this.locked = true;
+}
+
+BasicHighlighter.prototype = {
+  destroy: function() {
+    this.selection.off("new-node-front", this.highlight);
+    this.walker = null;
+    this.selection = null;
+  },
+  toggleLockState: function() {
+    this.locked = !this.locked;
+    if (this.locked) {
+      this.walker.cancelPick();
+    } else {
+      this.emit("unlocked");
+      this.walker.pick().then(
+        (node) => this._onPick(node),
+        () => this._onPick(null)
+      );
+    }
+  },
+  highlight: function() {
+    this.walker.highlight(this.selection.nodeFront);
+  },
+  _onPick: function(node) {
+    if (node) {
+      this.selection.setNodeFront(node);
+    }
+    this.locked = true;
+    this.emit("locked");
+  },
+  hide: function() {},
+  show: function() {},
+}
+
 ///////////////////////////////////////////////////////////////////////////
 
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
 });
 
-XPCOMUtils.defineLazyGetter(Highlighter.prototype, "strings", function () {
+XPCOMUtils.defineLazyGetter(LocalHighlighter.prototype, "strings", function () {
     return Services.strings.createBundle(
             "chrome://browser/locale/devtools/inspector.properties");
 });
--- a/browser/devtools/inspector/inspector-panel.js
+++ b/browser/devtools/inspector/inspector-panel.js
@@ -77,30 +77,16 @@ InspectorPanel.prototype = {
 
     this.breadcrumbs = new HTMLBreadcrumbs(this);
 
     if (this.target.isLocalTab) {
       this.browser = this.target.tab.linkedBrowser;
       this.scheduleLayoutChange = this.scheduleLayoutChange.bind(this);
       this.browser.addEventListener("resize", this.scheduleLayoutChange, true);
 
-      this.highlighter = new Highlighter(this.target, this, this._toolbox);
-      let button = this.panelDoc.getElementById("inspector-inspect-toolbutton");
-      button.hidden = false;
-      this.onLockStateChanged = function() {
-        if (this.highlighter.locked) {
-          button.removeAttribute("checked");
-          this._toolbox.raise();
-        } else {
-          button.setAttribute("checked", "true");
-        }
-      }.bind(this);
-      this.highlighter.on("locked", this.onLockStateChanged);
-      this.highlighter.on("unlocked", this.onLockStateChanged);
-
       // Show a warning when the debugger is paused.
       // We show the warning only when the inspector
       // is selected.
       this.updateDebuggerPausedWarning = function() {
         let notificationBox = this._toolbox.getNotificationBox();
         let notification = notificationBox.getNotificationWithValue("inspector-script-paused");
         if (!notification && this._toolbox.currentToolId == "inspector" &&
             this.target.isThreadPaused) {
@@ -119,16 +105,29 @@ InspectorPanel.prototype = {
 
       }.bind(this);
       this.target.on("thread-paused", this.updateDebuggerPausedWarning);
       this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
       this._toolbox.on("select", this.updateDebuggerPausedWarning);
       this.updateDebuggerPausedWarning();
     }
 
+    this.highlighter = new Highlighter(this.target, this, this._toolbox);
+    let button = this.panelDoc.getElementById("inspector-inspect-toolbutton");
+    this.onLockStateChanged = function() {
+      if (this.highlighter.locked) {
+        button.removeAttribute("checked");
+        this._toolbox.raise();
+      } else {
+        button.setAttribute("checked", "true");
+      }
+    }.bind(this);
+    this.highlighter.on("locked", this.onLockStateChanged);
+    this.highlighter.on("unlocked", this.onLockStateChanged);
+
     this._initMarkup();
     this.isReady = false;
 
     this.once("markuploaded", function() {
       this.isReady = true;
 
       // All the components are initialized. Let's select a node.
       this._selection.setNodeFront(defaultSelection);
--- a/browser/devtools/inspector/inspector.xul
+++ b/browser/devtools/inspector/inspector.xul
@@ -66,17 +66,16 @@
   <box flex="1" class="devtools-responsive-container">
     <vbox flex="1">
       <toolbar id="inspector-toolbar"
         class="devtools-toolbar"
         nowindowdrag="true">
         <toolbarbutton id="inspector-inspect-toolbutton"
           tooltiptext="&inspector.selectButton.tooltip;"
           class="devtools-toolbarbutton"
-          hidden="true"
           oncommand="inspector.highlighter.toggleLockState()"/>
         <arrowscrollbox id="inspector-breadcrumbs"
           class="breadcrumbs-widget-container"
           flex="1" orient="horizontal"
           clicktoscroll="true"/>
         <textbox id="inspector-searchbox"
           type="search"
           timeout="50"
--- a/browser/devtools/inspector/test/Makefile.in
+++ b/browser/devtools/inspector/test/Makefile.in
@@ -36,10 +36,11 @@ MOCHITEST_BROWSER_FILES := \
 		browser_inspector_bug_831693_combinator_suggestions.js \
 		browser_inspector_bug_831693_search_suggestions.html \
 		browser_inspector_bug_835722_infobar_reappears.js \
 		browser_inspector_bug_840156_destroy_after_navigation.js \
 		browser_inspector_reload.js \
 		browser_inspector_select_last_selected.js \
 		browser_inspector_select_last_selected.html \
 		browser_inspector_select_last_selected2.html \
+		browser_inspector_basic_highlighter.js \
 		head.js \
 		$(NULL)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_basic_highlighter.js
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+function test()
+{
+  let inspector, doc;
+  let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+  let {require} = devtools;
+
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+
+  content.location = "data:text/html,<h1>foo<h1><h2>bar</h2>";
+
+  function setupTest()
+  {
+    let h = require("devtools/inspector/highlighter");
+    h._forceBasic.value = true;
+    openInspector(runTests);
+  }
+
+  function runTests(aInspector)
+  {
+    inspector = aInspector;
+    let h1 = doc.querySelector("h1");
+    inspector.selection.once("new-node-front", () => executeSoon(testH1Selected));
+    inspector.selection.setNode(h1);
+  }
+
+  function testH1Selected() {
+    let h1 = doc.querySelector("h1");
+    let nodes = doc.querySelectorAll(":-moz-devtools-highlighted");
+    is(nodes.length, 1, "only one node selected");
+    is(nodes[0], h1, "h1 selected");
+    inspector.selection.once("new-node-front", () => executeSoon(testNoNodeSelected));
+    inspector.selection.setNode(null);
+  }
+
+  function testNoNodeSelected() {
+    ok(doc.querySelectorAll(":-moz-devtools-highlighted").length, 0, "no node selected");
+    finishUp();
+  }
+
+  function finishUp() {
+    let h = require("devtools/inspector/highlighter");
+    h._forceBasic.value = false;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
+
+
--- a/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
+++ b/browser/devtools/inspector/test/browser_inspector_bug_817558_delete_node.js
@@ -27,17 +27,17 @@ function test()
   {
     inspector = aInspector;
     inspector.selection.setNode(node);
     inspector.once("inspector-updated", () => {
       let parentNode = node.parentNode;
       parentNode.removeChild(node);
 
       let tmp = {};
-      Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
+      Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tmp);
       ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
 
       // Wait for the inspector to process the mutation
       inspector.once("inspector-updated", () => {
         is(inspector.selection.node, parentNode, "parent of selection got selected");
         finishUp();
       });
     });
--- a/browser/devtools/inspector/test/browser_inspector_destroyselection.js
+++ b/browser/devtools/inspector/test/browser_inspector_destroyselection.js
@@ -27,17 +27,17 @@ function test()
   {
     inspector = aInspector;
     inspector.selection.setNode(node);
 
     iframe.parentNode.removeChild(iframe);
     iframe = null;
 
     let tmp = {};
-    Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
+    Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tmp);
     ok(!tmp.LayoutHelpers.isNodeConnected(node), "Node considered as disconnected.");
     ok(!inspector.selection.isConnected(), "Selection considered as disconnected");
 
     finishUp();
   }
 
   function finishUp() {
     node = null;
--- a/browser/devtools/inspector/test/head.js
+++ b/browser/devtools/inspector/test/head.js
@@ -8,17 +8,17 @@ const Cc = Components.classes;
 
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 SimpleTest.registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.debugger.log");
 });
 
 
 let tempScope = {};
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tempScope);
 let LayoutHelpers = tempScope.LayoutHelpers;
 
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
 let TargetFactory = devtools.TargetFactory;
 
 Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
 let console = tempScope.console;
 
--- a/browser/devtools/layoutview/view.js
+++ b/browser/devtools/layoutview/view.js
@@ -3,17 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const Cu = Components.utils;
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 
 const promise = devtools.require("sdk/core/promise");
 
 function LayoutView(aInspector, aWindow)
 {
   this.inspector = aInspector;
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -12,17 +12,17 @@ const PAGE_SIZE = 10;
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 
 let {UndoStack} = require("devtools/shared/undo");
 let EventEmitter = require("devtools/shared/event-emitter");
 let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 let promise = require("sdk/core/promise");
 
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -21,17 +21,17 @@ let promise = require("sdk/core/promise"
 let Telemetry = require("devtools/shared/telemetry");
 let TargetFactory = require("devtools/framework/target").TargetFactory;
 const escodegen = require("escodegen/escodegen");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/source-editor.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource://gre/modules/reflect.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
deleted file mode 100644
--- a/browser/devtools/shared/LayoutHelpers.jsm
+++ /dev/null
@@ -1,384 +0,0 @@
-/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const Cu = Components.utils;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
-  return Services.strings.createBundle(
-    "chrome://global-platform/locale/platformKeys.properties");
-});
-
-this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
-
-this.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;
-  },
-
-  /**
-   * Compute the absolute position and the dimensions of a node, relativalely
-   * to the root window.
-   *
-   * @param nsIDOMNode aNode
-   *        a DOM element to get the bounds for
-   * @param nsIWindow aContentWindow
-   *        the content window holding the node
-   */
-  getRect: function LH_getRect(aNode, aContentWindow) {
-    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 + aContentWindow.pageYOffset,
-            left: clientRect.left + aContentWindow.pageXOffset,
-            width: clientRect.width,
-            height: clientRect.height};
-
-    // We iterate through all the parent windows.
-    while (true) {
-
-      // 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);
-
-    // In some cases, the computed style is null
-    if (!style) {
-      return [0, 0];
-    }
-
-    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)
-        .fullZoom;
-
-    // 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;
-  },
-
-  /**
-   * Scroll the document so that the element "elem" appears in the viewport.
-   *
-   * @param Element elem the element that needs to appear in the viewport.
-   * @param bool centered true if you want it centered, false if you want it to
-   * appear on the top of the viewport. It is true by default, and that is
-   * usually what you want.
-   */
-  scrollIntoViewIfNeeded:
-  function LH_scrollIntoViewIfNeeded(elem, centered) {
-    // We want to default to centering the element in the page,
-    // so as to keep the context of the element.
-    centered = centered === undefined? true: !!centered;
-
-    let win = elem.ownerDocument.defaultView;
-    let clientRect = elem.getBoundingClientRect();
-
-    // The following are always from the {top, bottom, left, right}
-    // of the viewport, to the {top, …} of the box.
-    // Think of them as geometrical vectors, it helps.
-    // The origin is at the top left.
-
-    let topToBottom = clientRect.bottom;
-    let bottomToTop = clientRect.top - win.innerHeight;
-    let leftToRight = clientRect.right;
-    let rightToLeft = clientRect.left - win.innerWidth;
-    let xAllowed = true;  // We allow one translation on the x axis,
-    let yAllowed = true;  // and one on the y axis.
-
-    // Whatever `centered` is, the behavior is the same if the box is
-    // (even partially) visible.
-
-    if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
-      win.scrollBy(0, topToBottom - elem.offsetHeight);
-      yAllowed = false;
-    } else
-    if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) {
-      win.scrollBy(0, bottomToTop + elem.offsetHeight);
-      yAllowed = false;
-    }
-
-    if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) {
-      if (xAllowed) {
-        win.scrollBy(leftToRight - elem.offsetWidth, 0);
-        xAllowed = false;
-      }
-    } else
-    if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) {
-      if (xAllowed) {
-        win.scrollBy(rightToLeft + elem.offsetWidth, 0);
-        xAllowed = false;
-      }
-    }
-
-    // If we want it centered, and the box is completely hidden,
-    // then we center it explicitly.
-
-    if (centered) {
-
-      if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
-        win.scroll(win.scrollX,
-                   win.scrollY + clientRect.top
-                   - (win.innerHeight - elem.offsetHeight) / 2);
-      }
-
-      if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
-        win.scroll(win.scrollX + clientRect.left
-                   - (win.innerWidth - elem.offsetWidth) / 2,
-                   win.scrollY);
-      }
-    }
-
-    if (win.parent !== win) {
-      // We are inside an iframe.
-      LH_scrollIntoViewIfNeeded(win.frameElement, centered);
-    }
-  },
-
-  /**
-   * Check if a node and its document are still alive
-   * and attached to the window.
-   *
-   * @param aNode
-   */
-  isNodeConnected: function LH_isNodeConnected(aNode)
-  {
-    try {
-      let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
-                      !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
-                      aNode.DOCUMENT_POSITION_DISCONNECTED));
-      return connected;
-    } catch (e) {
-      // "can't access dead object" error
-      return false;
-    }
-  },
-
-  /**
-   * Prettifies the modifier keys for an element.
-   *
-   * @param Node aElemKey
-   *        The key element to get the modifiers from.
-   * @param boolean aAllowCloverleaf
-   *        Pass true to use the cloverleaf symbol instead of a descriptive string.
-   * @return string
-   *         A prettified and properly separated modifier keys string.
-   */
-  prettyKey: function LH_prettyKey(aElemKey, aAllowCloverleaf)
-  {
-    let elemString = "";
-    let elemMod = aElemKey.getAttribute("modifiers");
-
-    if (elemMod.match("accel")) {
-      if (Services.appinfo.OS == "Darwin") {
-        // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
-        // Orion adds variable height lines.
-        if (!aAllowCloverleaf) {
-          elemString += "Cmd-";
-        } else {
-          elemString += PlatformKeys.GetStringFromName("VK_META") +
-                        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-        }
-      } else {
-        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      }
-    }
-    if (elemMod.match("access")) {
-      if (Services.appinfo.OS == "Darwin") {
-        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      } else {
-        elemString += PlatformKeys.GetStringFromName("VK_ALT") +
-                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-      }
-    }
-    if (elemMod.match("shift")) {
-      elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("alt")) {
-      elemString += PlatformKeys.GetStringFromName("VK_ALT") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("ctrl") || elemMod.match("control")) {
-      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-    if (elemMod.match("meta")) {
-      elemString += PlatformKeys.GetStringFromName("VK_META") +
-                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
-    }
-
-    return elemString +
-      (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
-       aElemKey.getAttribute("key")).toUpperCase();
-  }
-};
--- a/browser/devtools/shared/test/browser_layoutHelpers.js
+++ b/browser/devtools/shared/test/browser_layoutHelpers.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that scrollIntoViewIfNeeded works properly.
 
 let imported = {};
-Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm",
+Components.utils.import("resource://gre/modules/devtools/LayoutHelpers.jsm",
     imported);
 registerCleanupFunction(function () {
   imported = {};
 });
 
 let LayoutHelpers = imported.LayoutHelpers;
 
 const TEST_URI = "http://example.com/browser/browser/devtools/shared/test/browser_layoutHelpers.html";
--- a/browser/devtools/tilt/test/head.js
+++ b/browser/devtools/tilt/test/head.js
@@ -5,17 +5,17 @@
 let {devtools} = Components.utils.import("resource://gre/modules/devtools/Loader.jsm", {});
 let TiltManager = devtools.require("devtools/tilt/tilt").TiltManager;
 let TiltGL = devtools.require("devtools/tilt/tilt-gl");
 let {EPSILON, TiltMath, vec3, mat3, mat4, quat4} = devtools.require("devtools/tilt/tilt-math");
 let TiltUtils = devtools.require("devtools/tilt/tilt-utils");
 let {TiltVisualizer} = devtools.require("devtools/tilt/tilt-visualizer");
 
 let tempScope = {};
-Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
+Components.utils.import("resource://gre/modules/devtools/LayoutHelpers.jsm", tempScope);
 let LayoutHelpers = tempScope.LayoutHelpers;
 
 
 const DEFAULT_HTML = "data:text/html," +
   "<DOCTYPE html>" +
   "<html>" +
     "<head>" +
       "<meta charset='utf-8'/>" +
--- a/browser/devtools/tilt/tilt-utils.js
+++ b/browser/devtools/tilt/tilt-utils.js
@@ -4,17 +4,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 const STACK_THICKNESS = 15;
 
 /**
  * Module containing various helper functions used throughout Tilt.
  */
 this.TiltUtils = {};
 module.exports = this.TiltUtils;
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/LayoutHelpers.jsm
@@ -0,0 +1,384 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cu = Components.utils;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
+  return Services.strings.createBundle(
+    "chrome://global-platform/locale/platformKeys.properties");
+});
+
+this.EXPORTED_SYMBOLS = ["LayoutHelpers"];
+
+this.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;
+  },
+
+  /**
+   * Compute the absolute position and the dimensions of a node, relativalely
+   * to the root window.
+   *
+   * @param nsIDOMNode aNode
+   *        a DOM element to get the bounds for
+   * @param nsIWindow aContentWindow
+   *        the content window holding the node
+   */
+  getRect: function LH_getRect(aNode, aContentWindow) {
+    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 + aContentWindow.pageYOffset,
+            left: clientRect.left + aContentWindow.pageXOffset,
+            width: clientRect.width,
+            height: clientRect.height};
+
+    // We iterate through all the parent windows.
+    while (true) {
+
+      // 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);
+
+    // In some cases, the computed style is null
+    if (!style) {
+      return [0, 0];
+    }
+
+    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)
+        .fullZoom;
+
+    // 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;
+  },
+
+  /**
+   * Scroll the document so that the element "elem" appears in the viewport.
+   *
+   * @param Element elem the element that needs to appear in the viewport.
+   * @param bool centered true if you want it centered, false if you want it to
+   * appear on the top of the viewport. It is true by default, and that is
+   * usually what you want.
+   */
+  scrollIntoViewIfNeeded:
+  function LH_scrollIntoViewIfNeeded(elem, centered) {
+    // We want to default to centering the element in the page,
+    // so as to keep the context of the element.
+    centered = centered === undefined? true: !!centered;
+
+    let win = elem.ownerDocument.defaultView;
+    let clientRect = elem.getBoundingClientRect();
+
+    // The following are always from the {top, bottom, left, right}
+    // of the viewport, to the {top, …} of the box.
+    // Think of them as geometrical vectors, it helps.
+    // The origin is at the top left.
+
+    let topToBottom = clientRect.bottom;
+    let bottomToTop = clientRect.top - win.innerHeight;
+    let leftToRight = clientRect.right;
+    let rightToLeft = clientRect.left - win.innerWidth;
+    let xAllowed = true;  // We allow one translation on the x axis,
+    let yAllowed = true;  // and one on the y axis.
+
+    // Whatever `centered` is, the behavior is the same if the box is
+    // (even partially) visible.
+
+    if ((topToBottom > 0 || !centered) && topToBottom <= elem.offsetHeight) {
+      win.scrollBy(0, topToBottom - elem.offsetHeight);
+      yAllowed = false;
+    } else
+    if ((bottomToTop < 0 || !centered) && bottomToTop >= -elem.offsetHeight) {
+      win.scrollBy(0, bottomToTop + elem.offsetHeight);
+      yAllowed = false;
+    }
+
+    if ((leftToRight > 0 || !centered) && leftToRight <= elem.offsetWidth) {
+      if (xAllowed) {
+        win.scrollBy(leftToRight - elem.offsetWidth, 0);
+        xAllowed = false;
+      }
+    } else
+    if ((rightToLeft < 0 || !centered) && rightToLeft >= -elem.offsetWidth) {
+      if (xAllowed) {
+        win.scrollBy(rightToLeft + elem.offsetWidth, 0);
+        xAllowed = false;
+      }
+    }
+
+    // If we want it centered, and the box is completely hidden,
+    // then we center it explicitly.
+
+    if (centered) {
+
+      if (yAllowed && (topToBottom <= 0 || bottomToTop >= 0)) {
+        win.scroll(win.scrollX,
+                   win.scrollY + clientRect.top
+                   - (win.innerHeight - elem.offsetHeight) / 2);
+      }
+
+      if (xAllowed && (leftToRight <= 0 || rightToLeft <= 0)) {
+        win.scroll(win.scrollX + clientRect.left
+                   - (win.innerWidth - elem.offsetWidth) / 2,
+                   win.scrollY);
+      }
+    }
+
+    if (win.parent !== win) {
+      // We are inside an iframe.
+      LH_scrollIntoViewIfNeeded(win.frameElement, centered);
+    }
+  },
+
+  /**
+   * Check if a node and its document are still alive
+   * and attached to the window.
+   *
+   * @param aNode
+   */
+  isNodeConnected: function LH_isNodeConnected(aNode)
+  {
+    try {
+      let connected = (aNode.ownerDocument && aNode.ownerDocument.defaultView &&
+                      !(aNode.compareDocumentPosition(aNode.ownerDocument.documentElement) &
+                      aNode.DOCUMENT_POSITION_DISCONNECTED));
+      return connected;
+    } catch (e) {
+      // "can't access dead object" error
+      return false;
+    }
+  },
+
+  /**
+   * Prettifies the modifier keys for an element.
+   *
+   * @param Node aElemKey
+   *        The key element to get the modifiers from.
+   * @param boolean aAllowCloverleaf
+   *        Pass true to use the cloverleaf symbol instead of a descriptive string.
+   * @return string
+   *         A prettified and properly separated modifier keys string.
+   */
+  prettyKey: function LH_prettyKey(aElemKey, aAllowCloverleaf)
+  {
+    let elemString = "";
+    let elemMod = aElemKey.getAttribute("modifiers");
+
+    if (elemMod.match("accel")) {
+      if (Services.appinfo.OS == "Darwin") {
+        // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
+        // Orion adds variable height lines.
+        if (!aAllowCloverleaf) {
+          elemString += "Cmd-";
+        } else {
+          elemString += PlatformKeys.GetStringFromName("VK_META") +
+                        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+        }
+      } else {
+        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+      }
+    }
+    if (elemMod.match("access")) {
+      if (Services.appinfo.OS == "Darwin") {
+        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+      } else {
+        elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+                      PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+      }
+    }
+    if (elemMod.match("shift")) {
+      elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
+                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+    if (elemMod.match("alt")) {
+      elemString += PlatformKeys.GetStringFromName("VK_ALT") +
+                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+    if (elemMod.match("ctrl") || elemMod.match("control")) {
+      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
+                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+    if (elemMod.match("meta")) {
+      elemString += PlatformKeys.GetStringFromName("VK_META") +
+                    PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
+    }
+
+    return elemString +
+      (aElemKey.getAttribute("keycode").replace(/^.*VK_/, "") ||
+       aElemKey.getAttribute("key")).toUpperCase();
+  }
+};
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -53,27 +53,33 @@
 const {Cc, Ci, Cu, Cr} = require("chrome");
 
 const protocol = require("devtools/server/protocol");
 const {Arg, Option, method, RetVal, types} = protocol;
 const {LongStringActor, ShortLongString} = require("devtools/server/actors/string");
 const promise = require("sdk/core/promise");
 const object = require("sdk/util/object");
 const events = require("sdk/event/core");
+const {setTimeout, clearTimeout} = require('sdk/timers');
 const { Unknown } = require("sdk/platform/xpcom");
 const { Class } = require("sdk/core/heritage");
 const {PageStyleActor} = require("devtools/server/actors/styles");
 
 const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
 
 const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";
 
-const HELPER_SHEET = "." + HIDDEN_CLASS + " { visibility: hidden !important }";
+const HIGHLIGHTED_PSEUDO_CLASS = ":-moz-devtools-highlighted";
+const HIGHLIGHTED_TIMEOUT = 2000;
+
+let HELPER_SHEET = ".__fx-devtools-hide-shortcut__ { visibility: hidden !important } ";
+HELPER_SHEET += ":-moz-devtools-highlighted { outline: 2px dashed #F06!important; outline-offset: -2px!important } ";
 
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 
 exports.register = function(handle) {
   handle.addTabActor(InspectorActor, "inspectorActor");
 };
 
 exports.unregister = function(handle) {
   handle.removeTabActor(InspectorActor);
 };
@@ -837,16 +843,104 @@ var WalkerActor = protocol.ActorClass({
 
     if (node.nodeType === Ci.nsIDOMNode.DOCUMENT_NODE) {
       this._watchDocument(actor);
     }
     return actor;
   },
 
   /**
+   * Pick a node on click.
+   */
+  _pickDeferred: null,
+  pick: method(function() {
+    if (this._pickDeferred) {
+      return this._pickDeferred.promise;
+    }
+
+    this._pickDeferred = promise.defer();
+
+    let window = this.rootDoc.defaultView;
+    let isTouch = 'ontouchstart' in window;
+    let event = isTouch ? 'touchstart' : 'click';
+
+    this._onPick = function(e) {
+      e.stopImmediatePropagation();
+      e.preventDefault();
+      window.removeEventListener(event, this._onPick, true);
+      let u = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+
+      let x, y;
+      if (isTouch) {
+        x = e.touches[0].clientX;
+        y = e.touches[0].clientY;
+      } else {
+        x = e.clientX;
+        y = e.clientY;
+      }
+
+      let node = u.elementFromPoint(x, y, false, false);
+      node = this._ref(node);
+      let newParents = this.ensurePathToRoot(node);
+
+      this._pickDeferred.resolve({
+        node: node,
+        newParents: [parent for (parent of newParents)]
+      });
+      this._pickDeferred = null;
+
+    }.bind(this);
+
+    window.addEventListener(event, this._onPick, true);
+
+    return this._pickDeferred.promise;
+  }, { request: { }, response: RetVal("disconnectedNode") }),
+
+  cancelPick: method(function() {
+    if (this._pickDeferred) {
+      let window = this.rootDoc.defaultView;
+      let isTouch = 'ontouchstart' in window;
+      let event = isTouch ? 'touchstart' : 'click';
+      window.removeEventListener(event, this._onPick, true);
+      this._pickDeferred.resolve(null);
+      this._pickDeferred = null;
+    }
+  }),
+
+  /**
+   * Simple highlight mechanism.
+   */
+  _unhighlight: function() {
+    clearTimeout(this._highlightTimeout);
+    if (!this.rootDoc) {
+      return;
+    }
+    let nodes = this.rootDoc.querySelectorAll(HIGHLIGHTED_PSEUDO_CLASS);
+    for (let node of nodes) {
+      DOMUtils.removePseudoClassLock(node, HIGHLIGHTED_PSEUDO_CLASS);
+    }
+  },
+
+  highlight: method(function(node) {
+    this._installHelperSheet(node);
+    this._unhighlight();
+
+    if (!node ||
+        !node.rawNode ||
+         node.rawNode.nodeType !== Ci.nsIDOMNode.ELEMENT_NODE) {
+      return;
+    }
+
+    LayoutHelpers.scrollIntoViewIfNeeded(node.rawNode);
+    DOMUtils.addPseudoClassLock(node.rawNode, HIGHLIGHTED_PSEUDO_CLASS);
+    this._highlightTimeout = setTimeout(this._unhighlight.bind(this), HIGHLIGHTED_TIMEOUT);
+
+  }, { request: { node: Arg(0, "nullable:domnode") }}),
+
+  /**
    * Watch the given document node for mutations using the DOM observer
    * API.
    */
   _watchDocument: function(actor) {
     let node = actor.rawNode;
     // Create the observer on the node's actor.  The node will make sure
     // the observer is cleaned up when the actor is released.
     actor.observer = actor.rawNode.defaultView.MutationObserver(this.onMutations);
@@ -1737,16 +1831,24 @@ var WalkerActor = protocol.ActorClass({
 
 /**
  * Client side of the DOM walker.
  */
 var WalkerFront = exports.WalkerFront = protocol.FrontClass(WalkerActor, {
   // Set to true if cleanup should be requested after every mutation list.
   autoCleanup: true,
 
+  pick: protocol.custom(function() {
+    return this._pick().then(response => {
+      return response.node;
+    });
+  }, {
+    impl: "_pick"
+  }),
+
   initialize: function(client, form) {
     this._rootNodeDeferred = promise.defer();
     protocol.Front.prototype.initialize.call(this, client, form);
     this._orphaned = new Set();
     this._retainedOrphans = new Set();
   },
 
   destroy: function() {