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 id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersjwalker
bugs912915
milestone26.0a1
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() {