Bug 1020291 - Rework the way tooltips and highlighters are added to the style inspector; r=harth
authorPatrick Brosset <pbrosset@mozilla.com>
Wed, 18 Jun 2014 10:25:40 +0200
changeset 189385 1140b6e20b843fb285d8611d3233101c773ec8be
parent 189384 bd48de0a468dda54ff61541678e236e773bda379
child 189386 6e4e98a7485d0baa8bfe7eb6e3e2cc265e3e1472
push id26986
push userryanvm@gmail.com
push dateWed, 18 Jun 2014 20:15:38 +0000
treeherdermozilla-central@f78e532e8a10 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs1020291
milestone33.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 1020291 - Rework the way tooltips and highlighters are added to the style inspector; r=harth
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/style-inspector-overlays.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js
browser/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js
browser/devtools/styleinspector/test/browser_ruleview_eyedropper.js
browser/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -7,31 +7,30 @@
 const {Cc, Ci, Cu} = require("chrome");
 
 const ToolDefinitions = require("main").Tools;
 const {CssLogic} = require("devtools/styleinspector/css-logic");
 const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const {EventEmitter} = require("devtools/toolkit/event-emitter");
 const {OutputParser} = require("devtools/output-parser");
-const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+const overlays = require("devtools/styleinspector/style-inspector-overlays");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                   "resource://gre/modules/PluralForm.jsm");
 
 const FILTER_CHANGED_TIMEOUT = 300;
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
 
 /**
  * Helper for long-running processes that should yield occasionally to
  * the mainloop.
  *
  * @param {Window} aWin
  *        Timeouts will be set on this window when appropriate.
  * @param {Generator} aGenerator
@@ -129,16 +128,17 @@ UpdateProcess.prototype = {
  *
  * @constructor
  */
 function CssHtmlTree(aStyleInspector, aPageStyle)
 {
   this.styleWindow = aStyleInspector.window;
   this.styleDocument = aStyleInspector.window.document;
   this.styleInspector = aStyleInspector;
+  this.inspector = this.styleInspector.inspector;
   this.pageStyle = aPageStyle;
   this.propertyViews = [];
 
   this._outputParser = new OutputParser();
 
   let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
     getService(Ci.nsIXULChromeRegistry);
   this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
@@ -154,20 +154,20 @@ function CssHtmlTree(aStyleInspector, aP
 
   this.styleDocument.addEventListener("copy", this._onCopy);
   this.styleDocument.addEventListener("mousedown", this.focusWindow);
   this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
 
   // Nodes used in templating
   this.root = this.styleDocument.getElementById("root");
   this.templateRoot = this.styleDocument.getElementById("templateRoot");
-  this.propertyContainer = this.styleDocument.getElementById("propertyContainer");
+  this.element = this.styleDocument.getElementById("propertyContainer");
 
   // Listen for click events
-  this.propertyContainer.addEventListener("click", this._onClick, false);
+  this.element.addEventListener("click", this._onClick, false);
 
   // No results text.
   this.noResults = this.styleDocument.getElementById("noResults");
 
   // Refresh panel when color unit changed.
   this._handlePrefChange = this._handlePrefChange.bind(this);
   gDevTools.on("pref-changed", this._handlePrefChange);
 
@@ -176,29 +176,24 @@ function CssHtmlTree(aStyleInspector, aP
   this._prefObserver = new PrefObserver("devtools.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks);
 
   CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
 
   // The element that we're inspecting, and the document that it comes from.
   this.viewedElement = null;
 
-  // Properties preview tooltip
-  this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
-  this.tooltip.startTogglingOnHover(this.propertyContainer,
-    this._onTooltipTargetHover.bind(this));
-
   this._buildContextMenu();
   this.createStyleViews();
 
-  // Initialize the css transform highlighter if the target supports it
-  let hUtils = this.styleInspector.inspector.toolbox.highlighterUtils;
-  if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
-    this._initTransformHighlighter();
-  }
+  // Add the tooltips and highlightersoverlay
+  this.tooltips = new overlays.TooltipsOverlay(this);
+  this.tooltips.addToView();
+  this.highlighters = new overlays.HighlightersOverlay(this);
+  this.highlighters.addToView();
 }
 
 /**
  * Memoized lookup of a l10n string from a string bundle.
  * @param {string} aName The key to lookup.
  * @returns A localized version of the given key.
  */
 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName)
@@ -305,28 +300,70 @@ CssHtmlTree.prototype = {
       }
       // Hiding all properties
       for (let propView of this.propertyViews) {
         propView.refresh();
       }
       return promise.resolve(undefined);
     }
 
-    this.tooltip.hide();
-
     if (aElement === this.viewedElement) {
       return promise.resolve(undefined);
     }
 
     this.viewedElement = aElement;
     this.refreshSourceFilter();
 
     return this.refreshPanel();
   },
 
+  /**
+   * Get the type of a given node in the computed-view
+   * @param {DOMNode} node The node which we want information about
+   * @return {Object} The type information object contains the following props:
+   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+   *   style-inspector-overlays
+   * - value {Object} Depends on the type of the node
+   * returns null of the node isn't anything we care about
+   */
+  getNodeInfo: function(node) {
+    let type, value;
+    let classes = node.classList;
+
+    if (classes.contains("property-name") ||
+        classes.contains("property-value") ||
+        (classes.contains("theme-link") && !classes.contains("link"))) {
+      // Go up to the common parent to find the property and value
+      let parent = node.parentNode;
+      while (!parent.classList.contains("property-view")) {
+        parent = parent.parentNode;
+      }
+      value = {
+        property: parent.querySelector(".property-name").textContent,
+        value: parent.querySelector(".property-value").textContent
+      };
+    }
+
+    if (classes.contains("property-name")) {
+      type = overlays.VIEW_NODE_PROPERTY_TYPE;
+    } else if (classes.contains("property-value")) {
+      type = overlays.VIEW_NODE_VALUE_TYPE;
+    } else if (classes.contains("theme-link")) {
+      type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
+      value.url = node.textContent;
+    } else {
+      return null;
+    }
+
+    return {
+      type: type,
+      value: value
+    };
+  },
+
   _createPropertyViews: function()
   {
     if (this._createViewsPromise) {
       return this._createViewsPromise;
     }
 
     let deferred = promise.defer();
     this._createViewsPromise = deferred.promise;
@@ -347,17 +384,17 @@ CssHtmlTree.prototype = {
         }
         this.propertyViews.push(propView);
       },
       onCancel: () => {
         deferred.reject("_createPropertyViews cancelled");
       },
       onDone: () => {
         // Completed callback.
-        this.propertyContainer.appendChild(fragment);
+        this.element.appendChild(fragment);
         this.noResults.hidden = this.numVisibleProperties > 0;
         deferred.resolve(undefined);
       }
     });
 
     this._createViewsProcess.schedule();
     return deferred.promise;
   },
@@ -402,17 +439,17 @@ CssHtmlTree.prototype = {
       let deferred = promise.defer();
       this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
         onItem: (aPropView) => {
           aPropView.refresh();
         },
         onDone: () => {
           this._refreshProcess = null;
           this.noResults.hidden = this.numVisibleProperties > 0;
-          this.styleInspector.inspector.emit("computed-view-refreshed");
+          this.inspector.emit("computed-view-refreshed");
           deferred.resolve(undefined);
         }
       });
       this._refreshProcess.schedule();
       return deferred.promise;
     }).then(null, (err) => console.error(err));
   },
 
@@ -516,116 +553,16 @@ CssHtmlTree.prototype = {
    */
   focusWindow: function(aEvent)
   {
     let win = this.styleDocument.defaultView;
     win.focus();
   },
 
   /**
-   * Get the css transform highlighter front, initializing it if needed
-   * @param a promise that resolves to the highlighter
-   */
-  getTransformHighlighter: function() {
-    if (this.transformHighlighterPromise) {
-      return this.transformHighlighterPromise;
-    }
-
-    let utils = this.styleInspector.inspector.toolbox.highlighterUtils;
-    this.transformHighlighterPromise =
-      utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
-        this.transformHighlighter = highlighter;
-        return this.transformHighlighter;
-      });
-
-    return this.transformHighlighterPromise;
-  },
-
-  _initTransformHighlighter: function() {
-    this.isTransformHighlighterShown = false;
-
-    this._onMouseMove = this._onMouseMove.bind(this);
-    this._onMouseLeave = this._onMouseLeave.bind(this);
-
-    this.propertyContainer.addEventListener("mousemove", this._onMouseMove, false);
-    this.propertyContainer.addEventListener("mouseleave", this._onMouseLeave, false);
-  },
-
-  _onMouseMove: function(event) {
-    if (event.target === this._lastHovered) {
-      return;
-    }
-
-    if (this.isTransformHighlighterShown) {
-      this.isTransformHighlighterShown = false;
-      this.getTransformHighlighter().then(highlighter => highlighter.hide());
-    }
-
-    this._lastHovered = event.target;
-    if (this._lastHovered.classList.contains("property-value")) {
-      let propName = this._lastHovered.parentNode.querySelector(".property-name");
-
-      if (propName.textContent === "transform") {
-        this.isTransformHighlighterShown = true;
-        let node = this.styleInspector.inspector.selection.nodeFront;
-        this.getTransformHighlighter().then(highlighter => highlighter.show(node));
-      }
-    }
-  },
-
-  _onMouseLeave: function(event) {
-    this._lastHovered = null;
-    if (this.isTransformHighlighterShown) {
-      this.isTransformHighlighterShown = false;
-      this.getTransformHighlighter().then(highlighter => highlighter.hide());
-    }
-  },
-
-  /**
-   * Executed by the tooltip when the pointer hovers over an element of the view.
-   * Used to decide whether the tooltip should be shown or not and to actually
-   * put content in it.
-   * Checks if the hovered target is a css value we support tooltips for.
-   */
-  _onTooltipTargetHover: function(target)
-  {
-    let inspector = this.styleInspector.inspector;
-
-    // Test for image url
-    if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) {
-      let propValue = target.parentNode;
-      let propName = propValue.parentNode.querySelector(".property-name");
-      if (propName.textContent === "background-image") {
-        let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
-        let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent);
-        return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim);
-      }
-    }
-
-    if (target.classList.contains("property-value")) {
-      let propValue = target;
-      let propName = target.parentNode.querySelector(".property-name");
-
-      // Test for font family
-      if (propName.textContent === "font-family") {
-        let prop = propValue.textContent.toLowerCase();
-
-        if (prop !== "inherit" && prop !== "unset" && prop !== "initial") {
-          return this.tooltip.setFontFamilyContent(propValue.textContent,
-            inspector.selection.nodeFront);
-        }
-      }
-    }
-
-    // If the target isn't one that should receive a tooltip, signal it by rejecting
-    // a promise
-    return promise.reject();
-  },
-
-  /**
    * Create a context menu.
    */
   _buildContextMenu: function()
   {
     let doc = this.styleDocument.defaultView.parent.document;
 
     this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
     this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
@@ -751,18 +688,17 @@ CssHtmlTree.prototype = {
   },
 
   _onClick: function(event) {
     let target = event.target;
 
     if (target.nodeName === "a") {
       event.stopPropagation();
       event.preventDefault();
-      let browserWin = this.styleInspector.inspector.target
-                           .tab.ownerDocument.defaultView;
+      let browserWin = this.inspector.target.tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(target.href, "tab");
     }
   },
 
   _onCopyColor: function() {
     clipboardHelper.copyString(this._colorToCopy, this.styleDocument);
   },
 
@@ -820,18 +756,18 @@ CssHtmlTree.prototype = {
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
   /**
    * Destructor for CssHtmlTree.
    */
   destroy: function CssHtmlTree_destroy()
   {
-    delete this.viewedElement;
-    delete this._outputParser;
+    this.viewedElement = null;
+    this._outputParser = null;
 
     // Remove event listeners
     this.includeBrowserStylesCheckbox.removeEventListener("command",
       this.includeBrowserStylesChanged);
     this.searchField.removeEventListener("command", this.filterChanged);
     gDevTools.off("pref-changed", this._handlePrefChange);
 
     this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
@@ -840,17 +776,17 @@ CssHtmlTree.prototype = {
     // Cancel tree construction
     if (this._createViewsProcess) {
       this._createViewsProcess.cancel();
     }
     if (this._refreshProcess) {
       this._refreshProcess.cancel();
     }
 
-    this.propertyContainer.removeEventListener("click", this._onClick, false);
+    this.element.removeEventListener("click", this._onClick, false);
 
     // Remove context menu
     if (this._contextmenu) {
       // Destroy the Select All menuitem.
       this.menuitemCopy.removeEventListener("command", this._onCopy);
       this.menuitemCopy = null;
 
       // Destroy the Copy menuitem.
@@ -864,51 +800,41 @@ CssHtmlTree.prototype = {
       // Destroy the context menu.
       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
       this._contextmenu.parentNode.removeChild(this._contextmenu);
       this._contextmenu = null;
     }
 
     this.popupNode = null;
 
-    this.tooltip.stopTogglingOnHover(this.propertyContainer);
-    this.tooltip.destroy();
-
-    if (this.transformHighlighter) {
-      this.transformHighlighter.finalize();
-      this.transformHighlighter = null;
-
-      this.propertyContainer.removeEventListener("mousemove", this._onMouseMove, false);
-      this.propertyContainer.removeEventListener("mouseleave", this._onMouseLeave, false);
-
-      this._lastHovered = null;
-    }
+    this.tooltips.destroy();
+    this.highlighters.destroy();
 
     // Remove bound listeners
     this.styleDocument.removeEventListener("contextmenu", this._onContextMenu);
     this.styleDocument.removeEventListener("copy", this._onCopy);
     this.styleDocument.removeEventListener("mousedown", this.focusWindow);
 
     // Nodes used in templating
-    delete this.root;
-    delete this.propertyContainer;
-    delete this.panel;
+    this.root = null;
+    this.element = null;
+    this.panel = null;
 
     // The document in which we display the results (csshtmltree.xul).
-    delete this.styleDocument;
+    this.styleDocument = null;
 
     for (let propView of this.propertyViews)  {
       propView.destroy();
     }
 
     // The element that we're inspecting, and the document that it comes from.
-    delete this.propertyViews;
-    delete this.styleWindow;
-    delete this.styleDocument;
-    delete this.styleInspector;
+    this.propertyViews = null;
+    this.styleWindow = null;
+    this.styleDocument = null;
+    this.styleInspector = null;
   }
 };
 
 function PropertyInfo(aTree, aName) {
   this.tree = aTree;
   this.name = aName;
 }
 PropertyInfo.prototype = {
@@ -1187,22 +1113,22 @@ PropertyView.prototype = {
         if (!this.matchedExpanded) {
           return;
         }
 
         this._matchedSelectorResponse = matched;
         CssHtmlTree.processTemplate(this.templateMatchedSelectors,
           this.matchedSelectorsContainer, this);
         this.matchedExpander.setAttribute("open", "");
-        this.tree.styleInspector.inspector.emit("computed-view-property-expanded");
+        this.tree.inspector.emit("computed-view-property-expanded");
       }).then(null, console.error);
     } else {
       this.matchedSelectorsContainer.innerHTML = "";
       this.matchedExpander.removeAttribute("open");
-      this.tree.styleInspector.inspector.emit("computed-view-property-collapsed");
+      this.tree.inspector.emit("computed-view-property-collapsed");
       return promise.resolve(undefined);
     }
   },
 
   get matchedSelectors()
   {
     return this._matchedSelectorResponse;
   },
@@ -1251,17 +1177,17 @@ PropertyView.prototype = {
     aEvent.preventDefault();
   },
 
   /**
    * The action when a user clicks on the MDN help link for a property.
    */
   mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
   {
-    let inspector = this.tree.styleInspector.inspector;
+    let inspector = this.tree.inspector;
 
     if (inspector.target.tab) {
       let browserWin = inspector.target.tab.ownerDocument.defaultView;
       browserWin.openUILinkIn(this.link, "tab");
     }
     aEvent.preventDefault();
   },
 
@@ -1393,19 +1319,19 @@ SelectorView.prototype = {
 
   /**
    * Update the text of the source link to reflect whether we're showing
    * original sources or not.
    */
   updateSourceLink: function()
   {
     this.updateSource().then((oldSource) => {
-      if (oldSource != this.source && this.tree.propertyContainer) {
+      if (oldSource != this.source && this.tree.element) {
         let selector = '[sourcelocation="' + oldSource + '"]';
-        let link = this.tree.propertyContainer.querySelector(selector);
+        let link = this.tree.element.querySelector(selector);
         if (link) {
           link.textContent = this.source;
           link.setAttribute("sourcelocation", this.source);
         }
       }
     });
   },
 
@@ -1464,17 +1390,17 @@ SelectorView.prototype = {
    *
    *   We can only view stylesheets contained in document.styleSheets inside the
    *   style editor.
    *
    * @param aEvent The click event
    */
   openStyleEditor: function(aEvent)
   {
-    let inspector = this.tree.styleInspector.inspector;
+    let inspector = this.tree.inspector;
     let rule = this.selectorInfo.rule;
 
     // The style editor can only display stylesheets coming from content because
     // chrome stylesheets are not listed in the editor's stylesheet selector.
     //
     // If the stylesheet is a content stylesheet we send it to the style
     // editor else we display it in the view source window.
     let sheet = rule.parentStyleSheet;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -7,29 +7,28 @@
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const {CssLogic} = require("devtools/styleinspector/css-logic");
 const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
 const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
-const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
 const {OutputParser} = require("devtools/output-parser");
 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
 const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
+const overlays = require("devtools/styleinspector/style-inspector-overlays");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
 const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
-const TRANSFORM_HIGHLIGHTER_TYPE = "CssTransformHighlighter";
 
 /**
  * These regular expressions are adapted from firebug's css.js, and are
  * used to parse CSSStyleDeclaration's cssText attribute.
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
@@ -1095,33 +1094,24 @@ function CssRuleView(aInspector, aDoc, a
   this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
 
   let options = {
     autoSelect: true,
     theme: "auto"
   };
   this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
 
-  // Create a tooltip for previewing things in the rule view (images for now)
-  this.previewTooltip = new Tooltip(this.inspector.panelDoc);
-  this.previewTooltip.startTogglingOnHover(this.element,
-    this._onTooltipTargetHover.bind(this));
-
-  // Also create a more complex tooltip for editing colors with the spectrum
-  // color picker
-  this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc);
-
   this._buildContextMenu();
   this._showEmpty();
 
-  // Initialize the css transform highlighter if the target supports it
-  let hUtils = this.inspector.toolbox.highlighterUtils;
-  if (hUtils.hasCustomHighlighter(TRANSFORM_HIGHLIGHTER_TYPE)) {
-    this._initTransformHighlighter();
-  }
+  // Add the tooltips and highlighters to the view
+  this.tooltips = new overlays.TooltipsOverlay(this);
+  this.tooltips.addToView();
+  this.highlighters = new overlays.HighlightersOverlay(this);
+  this.highlighters.addToView();
 }
 
 exports.CssRuleView = CssRuleView;
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
 
@@ -1161,147 +1151,16 @@ CssRuleView.prototype = {
       popupset = doc.createElementNS(XUL_NS, "popupset");
       doc.documentElement.appendChild(popupset);
     }
 
     popupset.appendChild(this._contextmenu);
   },
 
   /**
-   * Get the css transform highlighter front, initializing it if needed
-   * @param a promise that resolves to the highlighter
-   */
-  getTransformHighlighter: function() {
-    if (this.transformHighlighterPromise) {
-      return this.transformHighlighterPromise;
-    }
-
-    let utils = this.inspector.toolbox.highlighterUtils;
-    this.transformHighlighterPromise =
-    utils.getHighlighterByType(TRANSFORM_HIGHLIGHTER_TYPE).then(highlighter => {
-      this.transformHighlighter = highlighter;
-      return this.transformHighlighter;
-    });
-
-    return this.transformHighlighterPromise;
-  },
-
-  _initTransformHighlighter: function() {
-    this.isTransformHighlighterShown = false;
-
-    this._onMouseMove = this._onMouseMove.bind(this);
-    this._onMouseLeave = this._onMouseLeave.bind(this);
-
-    this.element.addEventListener("mousemove", this._onMouseMove, false);
-    this.element.addEventListener("mouseleave", this._onMouseLeave, false);
-  },
-
-  _onMouseMove: function(event) {
-    if (event.target === this._lastHovered) {
-      return;
-    }
-
-    if (this.isTransformHighlighterShown) {
-      this.isTransformHighlighterShown = false;
-      this.getTransformHighlighter().then(highlighter => highlighter.hide());
-    }
-
-    this._lastHovered = event.target;
-    let prop = event.target.textProperty;
-    let isHighlightable = prop && prop.name === "transform" &&
-                          prop.enabled && !prop.overridden &&
-                          !prop.rule.pseudoElement;
-
-    if (isHighlightable) {
-      this.isTransformHighlighterShown = true;
-      let node = this.inspector.selection.nodeFront;
-      this.getTransformHighlighter().then(highlighter => highlighter.show(node));
-    }
-  },
-
-  _onMouseLeave: function(event) {
-    this._lastHovered = null;
-    if (this.isTransformHighlighterShown) {
-      this.isTransformHighlighterShown = false;
-      this.getTransformHighlighter().then(highlighter => highlighter.hide());
-    }
-  },
-
-  /**
-   * Which type of hover-tooltip should be shown for the given element?
-   * This depends on the element: does it contain a URL, a font-family, ...
-   * @param {DOMNode} el The element to test
-   * @return {String} The type of hover-tooltip
-   */
-  _getHoverTooltipTypeForTarget: function(el) {
-    let prop = el.textProperty;
-
-    // Test for image
-    let isUrl = el.classList.contains("theme-link") &&
-                el.parentNode.classList.contains("ruleview-propertyvalue");
-    if (this.inspector.hasUrlToImageDataResolver && isUrl) {
-      return "image";
-    }
-
-    // Test for font-family
-    let propertyRoot = el.parentNode;
-    let propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
-    if (!propertyNameNode) {
-      propertyRoot = propertyRoot.parentNode;
-      propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
-    }
-    let propertyName;
-    if (propertyNameNode) {
-      propertyName = propertyNameNode.textContent;
-    }
-    if (propertyName === "font-family" && el.classList.contains("ruleview-propertyvalue")) {
-      return "font";
-    }
-  },
-
-  /**
-   * Executed by the tooltip when the pointer hovers over an element of the view.
-   * Used to decide whether the tooltip should be shown or not and to actually
-   * put content in it.
-   * Checks if the hovered target is a css value we support tooltips for.
-   * @param {DOMNode} target
-   * @return {Boolean|Promise} Either a boolean or a promise, used by the
-   * Tooltip class to wait for the content to be put in the tooltip and finally
-   * decide whether or not the tooltip should be shown.
-   */
-  _onTooltipTargetHover: function(target) {
-    let tooltipType = this._getHoverTooltipTypeForTarget(target);
-    if (!tooltipType) {
-      return false;
-    }
-
-    if (this.colorPicker.tooltip.isShown()) {
-      this.colorPicker.revert();
-      this.colorPicker.hide();
-    }
-
-    if (tooltipType === "image") {
-      let prop = target.parentNode.textProperty;
-      let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
-      let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href);
-      return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim);
-    }
-    if (tooltipType === "font") {
-      let prop = target.textContent.toLowerCase();
-
-      if (prop !== "inherit" && prop !== "unset" && prop !== "initial") {
-        return this.previewTooltip.setFontFamilyContent(target.textContent,
-          this.inspector.selection.nodeFront);
-      }
-    }
-
-    return false;
-  },
-
-  /**
    * Update the context menu. This means enabling or disabling menuitems as
    * appropriate.
    */
   _contextMenuUpdate: function() {
     let win = this.doc.defaultView;
 
     // Copy selection.
     let selection = win.getSelection();
@@ -1334,16 +1193,75 @@ CssRuleView.prototype = {
                                       _strings.GetStringFromName(label));
 
     let accessKey = label + ".accessKey";
     this.menuitemSources.setAttribute("accesskey",
                                       _strings.GetStringFromName(accessKey));
   },
 
   /**
+   * Get the type of a given node in the rule-view
+   * @param {DOMNode} node The node which we want information about
+   * @return {Object} The type information object contains the following props:
+   * - type {String} One of the VIEW_NODE_XXX_TYPE const in
+   *   style-inspector-overlays
+   * - value {Object} Depends on the type of the node
+   * returns null of the node isn't anything we care about
+   */
+  getNodeInfo: function(node) {
+    let type, value;
+    let classes = node.classList;
+    let prop = getParentTextProperty(node);
+
+    if (classes.contains("ruleview-propertyname") && prop) {
+      type = overlays.VIEW_NODE_PROPERTY_TYPE;
+      value = {
+        property: node.textContent,
+        value: getPropertyNameAndValue(node).value,
+        enabled: prop.enabled,
+        overridden: prop.overridden,
+        pseudoElement: prop.rule.pseudoElement,
+        sheetHref: prop.rule.domRule.href
+      };
+    } else if (classes.contains("ruleview-propertyvalue") && prop) {
+      type = overlays.VIEW_NODE_VALUE_TYPE;
+      value = {
+        property: getPropertyNameAndValue(node).name,
+        value: node.textContent,
+        enabled: prop.enabled,
+        overridden: prop.overridden,
+        pseudoElement: prop.rule.pseudoElement,
+        sheetHref: prop.rule.domRule.href
+      };
+    } else if (classes.contains("theme-link") && prop) {
+      type = overlays.VIEW_NODE_IMAGE_URL_TYPE;
+      value = {
+        property: getPropertyNameAndValue(node).name,
+        value: node.parentNode.textContent,
+        url: node.textContent,
+        enabled: prop.enabled,
+        overridden: prop.overridden,
+        pseudoElement: prop.rule.pseudoElement,
+        sheetHref: prop.rule.domRule.href
+      };
+    } else if (classes.contains("ruleview-selector-unmatched") ||
+               classes.contains("ruleview-selector-matched")) {
+      type = overlays.VIEW_NODE_SELECTOR_TYPE;
+      value = node.textContent;
+    } else {
+      return null;
+    }
+
+    return {
+      type: type,
+      value: value
+    };
+  },
+
+  /**
    * A helper that determines if the popup was opened with a click to a color
    * value and saves the color to this._colorToCopy.
    *
    * @return {Boolean}
    *         true if click on color opened the popup, false otherwise.
    */
   _isColorPopup: function () {
     this._colorToCopy = "";
@@ -1440,17 +1358,17 @@ CssRuleView.prototype = {
     this.pageStyle = aPageStyle;
   },
 
   /**
    * Return {bool} true if the rule view currently has an input editor visible.
    */
   get isEditing() {
     return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0
-      || this.colorPicker.tooltip.isShown();
+      || this.tooltips.colorPicker.tooltip.isShown();
   },
 
   _handlePrefChange: function(pref) {
     if (pref === PREF_UA_STYLES) {
       this.showUserAgentStyles = Services.prefs.getBoolPref(pref);
     }
 
     // Reselect the currently selected element
@@ -1512,29 +1430,18 @@ CssRuleView.prototype = {
       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
       this._contextmenu.parentNode.removeChild(this._contextmenu);
       this._contextmenu = null;
     }
 
     // We manage the popupNode ourselves so we also need to destroy it.
     this.doc.popupNode = null;
 
-    this.previewTooltip.stopTogglingOnHover(this.element);
-    this.previewTooltip.destroy();
-    this.colorPicker.destroy();
-
-    if (this.transformHighlighter) {
-      this.transformHighlighter.finalize();
-      this.transformHighlighter = null;
-
-      this.element.removeEventListener("mousemove", this._onMouseMove, false);
-      this.element.removeEventListener("mouseleave", this._onMouseLeave, false);
-
-      this._lastHovered = null;
-    }
+    this.tooltips.destroy();
+    this.highlighters.destroy();
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
     if (this.elementStyle) {
       this.elementStyle.destroy();
     }
@@ -1635,19 +1542,16 @@ CssRuleView.prototype = {
 
   /**
    * Clear the rule view.
    */
   clear: function() {
     this._clearRules();
     this._viewedElement = null;
     this._elementStyle = null;
-
-    this.previewTooltip.hide();
-    this.colorPicker.hide();
   },
 
   /**
    * Called when the user has made changes to the ElementStyle.
    * Emits an event that clients can listen to.
    */
   _changed: function() {
     var evt = this.doc.createEvent("Events");
@@ -2149,18 +2053,18 @@ function TextPropertyEditor(aRuleEditor,
 }
 
 TextPropertyEditor.prototype = {
   /**
    * Boolean indicating if the name or value is being currently edited.
    */
   get editing() {
     return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
-      this.ruleEditor.ruleView.colorPicker.tooltip.isShown() ||
-      this.ruleEditor.ruleView.colorPicker.eyedropperOpen) ||
+      this.ruleEditor.ruleView.tooltips.colorPicker.tooltip.isShown() ||
+      this.ruleEditor.ruleView.tooltips.colorPicker.eyedropperOpen) ||
       this.popup.isOpen;
   },
 
   /**
    * Create the property editor's DOM.
    */
   _create: function() {
     this.element = this.doc.createElementNS(HTML_NS, "li");
@@ -2202,19 +2106,20 @@ TextPropertyEditor.prototype = {
     // Property value, editable when focused.  Changes to the
     // property value are applied as they are typed, and reverted
     // if the user presses escape.
     this.valueSpan = createChild(propertyContainer, "span", {
       class: "ruleview-propertyvalue theme-fg-color1",
       tabindex: this.ruleEditor.isEditable ? "0" : "-1",
     });
 
-    // Storing the TextProperty on the valuespan for easy access
+    // Storing the TextProperty on the elements for easy access
     // (for instance by the tooltip)
     this.valueSpan.textProperty = this.prop;
+    this.nameSpan.textProperty = this.prop;
 
     // Save the initial value as the last committed value,
     // for restoring after pressing escape.
     this.committed = { name: this.prop.name,
                        value: this.prop.value,
                        priority: this.prop.priority };
 
     appendText(propertyContainer, ";");
@@ -2400,17 +2305,17 @@ TextPropertyEditor.prototype = {
 
     // Attach the color picker tooltip to the color swatches
     this._swatchSpans = this.valueSpan.querySelectorAll("." + swatchClass);
     if (this.ruleEditor.isEditable) {
       for (let span of this._swatchSpans) {
         // Capture the original declaration value to be able to revert later
         let originalValue = this.valueSpan.textContent;
         // Adding this swatch to the list of swatches our colorpicker knows about
-        this.ruleEditor.ruleView.colorPicker.addSwatch(span, {
+        this.ruleEditor.ruleView.tooltips.colorPicker.addSwatch(span, {
           onPreview: () => this._previewValue(this.valueSpan.textContent),
           onCommit: () => this._applyNewValue(this.valueSpan.textContent),
           onRevert: () => this._applyNewValue(originalValue)
         });
       }
     }
 
     // Populate the computed styles.
@@ -2541,22 +2446,23 @@ TextPropertyEditor.prototype = {
 
   /**
    * Remove property from style and the editors from DOM.
    * Begin editing next available property.
    */
   remove: function() {
     if (this._swatchSpans && this._swatchSpans.length) {
       for (let span of this._swatchSpans) {
-        this.ruleEditor.ruleView.colorPicker.removeSwatch(span);
+        this.ruleEditor.ruleView.tooltips.colorPicker.removeSwatch(span);
       }
     }
 
     this.element.parentNode.removeChild(this.element);
     this.ruleEditor.rule.editClosestTextProperty(this.prop);
+    this.nameSpan.textProperty = null;
     this.valueSpan.textProperty = null;
     this.prop.remove();
   },
 
   /**
    * Called when a value editor closes.  If the user pressed escape,
    * revert to the value this property had before editing.
    *
@@ -2865,16 +2771,73 @@ function blurOnMultipleProperties(e) {
 
 /**
  * Append a text node to an element.
  */
 function appendText(aParent, aText) {
   aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
 }
 
+/**
+ * Walk up the DOM from a given node until a parent property holder is found.
+ * For elements inside the computed property list, the non-computed parent
+ * property holder will be returned
+ * @param {DOMNode} node The node to start from
+ * @return {DOMNode} The parent property holder node, or null if not found
+ */
+function getParentTextPropertyHolder(node) {
+  while (true) {
+    if (!node || !node.classList) {
+      return null;
+    }
+    if (node.classList.contains("ruleview-property")) {
+      return node;
+    }
+    node = node.parentNode;
+  }
+}
+
+/**
+ * For any given node, find the TextProperty it is in if any
+ * @param {DOMNode} node The node to start from
+ * @return {TextProperty}
+ */
+function getParentTextProperty(node) {
+  let parent = getParentTextPropertyHolder(node);
+  if (!parent) {
+    return null;
+  }
+  return parent.querySelector(".ruleview-propertyvalue").textProperty;
+}
+
+/**
+ * Walker up the DOM from a given node until a parent property holder is found,
+ * and return the textContent for the name and value nodes.
+ * Stops at the first property found, so if node is inside the computed property
+ * list, the computed property will be returned
+ * @param {DOMNode} node The node to start from
+ * @return {Object} {name, value}
+ */
+function getPropertyNameAndValue(node) {
+  while (true) {
+    if (!node || !node.classList) {
+      return null;
+    }
+    // Check first for ruleview-computed since it's the deepest
+    if (node.classList.contains("ruleview-computed") ||
+        node.classList.contains("ruleview-property")) {
+      return {
+        name: node.querySelector(".ruleview-propertyname").textContent,
+        value: node.querySelector(".ruleview-propertyvalue").textContent
+      };
+    }
+    node = node.parentNode;
+  }
+}
+
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
 
 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
   return Services.strings.createBundle(
     "chrome://global/locale/devtools/styleinspector.properties");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/style-inspector-overlays.js
@@ -0,0 +1,375 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set 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/. */
+
+"use strict";
+
+// The style-inspector overlays are:
+// - tooltips that appear when hovering over property values
+// - editor tooltips that appear when clicking color swatches, etc.
+// - in-content highlighters that appear when hovering over property values
+// - etc.
+
+const {Cc, Ci, Cu} = require("chrome");
+const {
+  Tooltip,
+  SwatchColorPickerTooltip
+} = require("devtools/shared/widgets/Tooltip");
+const {CssLogic} = require("devtools/styleinspector/css-logic");
+const {Promise:promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
+
+// Types of existing tooltips
+const TOOLTIP_IMAGE_TYPE = "image";
+const TOOLTIP_FONTFAMILY_TYPE = "font-family";
+
+// Types of existing highlighters
+const HIGHLIGHTER_TRANSFORM_TYPE = "CssTransformHighlighter";
+const HIGHLIGHTER_TYPES = [
+  HIGHLIGHTER_TRANSFORM_TYPE
+];
+
+// Types of nodes in the rule/computed-view
+const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
+const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
+const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
+const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
+
+/**
+ * Manages all highlighters in the style-inspector.
+ * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view
+ * panel
+ */
+function HighlightersOverlay(view) {
+  this.view = view;
+
+  let {CssRuleView} = require("devtools/styleinspector/rule-view");
+  this.isRuleView = view instanceof CssRuleView;
+
+  this.highlighterUtils = this.view.inspector.toolbox.highlighterUtils;
+
+  this._onMouseMove = this._onMouseMove.bind(this);
+  this._onMouseLeave = this._onMouseLeave.bind(this);
+
+  this.promises = {};
+  this.highlighters = {};
+
+  // Only initialize the overlay if at least one of the highlighter types is
+  // supported
+  this.supportsHighlighters = HIGHLIGHTER_TYPES.some(type => {
+    return this.highlighterUtils.hasCustomHighlighter(type);
+  });
+}
+
+exports.HighlightersOverlay = HighlightersOverlay;
+
+HighlightersOverlay.prototype = {
+  /**
+   * Add the highlighters overlay to the view. This will start tracking mouse
+   * movements and display highlighters when needed
+   */
+  addToView: function() {
+    if (!this.supportsHighlighters || this._isStarted || this._isDestroyed) {
+      return;
+    }
+
+    let el = this.view.element;
+    el.addEventListener("mousemove", this._onMouseMove, false);
+    el.addEventListener("mouseleave", this._onMouseLeave, false);
+
+    this._isStarted = true;
+  },
+
+  /**
+   * Remove the overlay from the current view. This will stop tracking mouse
+   * movement and showing highlighters
+   */
+  removeFromView: function() {
+    if (!this.supportsHighlighters || !this._isStarted || this._isDestroyed) {
+      return;
+    }
+
+    this._hideCurrent();
+
+    let el = this.view.element;
+    el.removeEventListener("mousemove", this._onMouseMove, false);
+    el.removeEventListener("mouseleave", this._onMouseLeave, false);
+
+    this._isStarted = false;
+  },
+
+  _onMouseMove: function(event) {
+    // Bail out if the target is the same as for the last mousemove
+    if (event.target === this._lastHovered) {
+      return;
+    }
+
+    // Only one highlighter can be displayed at a time, hide the currently shown
+    this._hideCurrent();
+
+    this._lastHovered = event.target;
+
+    let nodeInfo = this.view.getNodeInfo(event.target);
+    if (!nodeInfo) {
+      return;
+    }
+
+    // Choose the type of highlighter required for the hovered node
+    let type;
+    if (this._isRuleViewTransform(nodeInfo) ||
+        this._isComputedViewTransform(nodeInfo)) {
+      type = HIGHLIGHTER_TRANSFORM_TYPE;
+    }
+
+    if (type) {
+      this.highlighterShown = type;
+      let node = this.view.inspector.selection.nodeFront;
+      this._getHighlighter(type).then(highlighter => highlighter.show(node));
+    }
+  },
+
+  _onMouseLeave: function(event) {
+    this._lastHovered = null;
+    this._hideCurrent();
+  },
+
+  /**
+   * Is the current hovered node a css transform property value in the rule-view
+   * @param {Object} nodeInfo
+   * @return {Boolean}
+   */
+  _isRuleViewTransform: function(nodeInfo) {
+    let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+                      nodeInfo.value.property === "transform";
+    let isEnabled = nodeInfo.value.enabled &&
+                    !nodeInfo.value.overridden &&
+                    !nodeInfo.value.pseudoElement;
+    return this.isRuleView && isTransform && isEnabled;
+  },
+
+  /**
+   * Is the current hovered node a css transform property value in the
+   * computed-view
+   * @param {Object} nodeInfo
+   * @return {Boolean}
+   */
+  _isComputedViewTransform: function(nodeInfo) {
+    let isTransform = nodeInfo.type === VIEW_NODE_VALUE_TYPE &&
+                      nodeInfo.value.property === "transform";
+    return !this.isRuleView && isTransform;
+  },
+
+  /**
+   * Hide the currently shown highlighter
+   */
+  _hideCurrent: function() {
+    if (this.highlighterShown) {
+      this._getHighlighter(this.highlighterShown).then(highlighter => {
+        highlighter.hide();
+        this.highlighterShown = null;
+      });
+    }
+  },
+
+  /**
+   * Get a highlighter front given a type. It will only be initialized once
+   * @param {String} type The highlighter type. One of this.highlighters
+   * @return a promise that resolves to the highlighter
+   */
+  _getHighlighter: function(type) {
+    let utils = this.highlighterUtils;
+    if (!utils.hasCustomHighlighter(type)) {
+      return promise.reject();
+    }
+
+    if (this.promises[type]) {
+      return this.promises[type];
+    }
+
+    return this.promises[type] = utils.getHighlighterByType(type).then(highlighter => {
+      this.highlighters[type] = highlighter;
+      return highlighter;
+    });
+  },
+
+  /**
+   * Destroy this overlay instance, removing it from the view and destroying
+   * all initialized highlighters
+   */
+  destroy: function() {
+    this.removeFromView();
+
+    for (let type in this.highlighters) {
+      if (this.highlighters[type]) {
+        this.highlighters[type].finalize();
+        this.highlighters[type] = null;
+      }
+    }
+
+    this.promises = null;
+    this.view = null;
+    this.highlighterUtils = null;
+
+    this._isDestroyed = true;
+  }
+};
+
+/**
+ * Manages all tooltips in the style-inspector.
+ * @param {CssRuleView|CssHtmlTree} view Either the rule-view or computed-view
+ * panel
+ */
+function TooltipsOverlay(view) {
+  this.view = view;
+
+  let {CssRuleView} = require("devtools/styleinspector/rule-view");
+  this.isRuleView = view instanceof CssRuleView;
+
+  this._onNewSelection = this._onNewSelection.bind(this);
+  this.view.inspector.selection.on("new-node-front", this._onNewSelection);
+}
+
+exports.TooltipsOverlay = TooltipsOverlay;
+
+TooltipsOverlay.prototype = {
+  /**
+   * Add the tooltips overlay to the view. This will start tracking mouse
+   * movements and display tooltips when needed
+   */
+  addToView: function() {
+    if (this._isStarted || this._isDestroyed) {
+      return;
+    }
+
+    // Image, fonts, ... preview tooltip
+    this.previewTooltip = new Tooltip(this.view.inspector.panelDoc);
+    this.previewTooltip.startTogglingOnHover(this.view.element,
+      this._onPreviewTooltipTargetHover.bind(this));
+
+    // Color picker tooltip
+    if (this.isRuleView) {
+      this.colorPicker = new SwatchColorPickerTooltip(this.view.inspector.panelDoc);
+    }
+
+    this._isStarted = true;
+  },
+
+  /**
+   * Remove the tooltips overlay from the view. This will stop tracking mouse
+   * movements and displaying tooltips
+   */
+  removeFromView: function() {
+    if (!this._isStarted || this._isDestroyed) {
+      return;
+    }
+
+    this.previewTooltip.stopTogglingOnHover(this.view.element);
+    this.previewTooltip.destroy();
+
+    if (this.colorPicker) {
+      this.colorPicker.destroy();
+    }
+
+    this._isStarted = false;
+  },
+
+  /**
+   * Given a hovered node info, find out which type of tooltip should be shown,
+   * if any
+   * @param {Object} nodeInfo
+   * @return {String} The tooltip type to be shown, or null
+   */
+  _getTooltipType: function({type, value:prop}) {
+    let tooltipType = null;
+    let inspector = this.view.inspector;
+
+    // Image preview tooltip
+    if (type === VIEW_NODE_IMAGE_URL_TYPE && inspector.hasUrlToImageDataResolver) {
+      let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
+      let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value,
+        prop.sheetHref); // sheetHref is undefined for computed-view properties,
+                         // but we don't care as URIs are absolute
+      tooltipType = TOOLTIP_IMAGE_TYPE;
+    }
+
+    // Font preview tooltip
+    if (type === VIEW_NODE_VALUE_TYPE && prop.property === "font-family") {
+      let value = prop.value.toLowerCase();
+      if (value !== "inherit" && value !== "unset" && value !== "initial") {
+        tooltipType = TOOLTIP_FONTFAMILY_TYPE;
+      }
+    }
+
+    return tooltipType;
+  },
+
+  /**
+   * Executed by the tooltip when the pointer hovers over an element of the view.
+   * Used to decide whether the tooltip should be shown or not and to actually
+   * put content in it.
+   * Checks if the hovered target is a css value we support tooltips for.
+   * @param {DOMNode} target The currently hovered node
+   */
+  _onPreviewTooltipTargetHover: function(target) {
+    let nodeInfo = this.view.getNodeInfo(target);
+    if (!nodeInfo) {
+      // The hovered node isn't something we care about
+      return promise.reject();
+    }
+
+    let type = this._getTooltipType(nodeInfo);
+    if (!type) {
+      // There is no tooltip type defined for the hovered node
+      return promise.reject();
+    }
+
+    if (this.isRuleView && this.colorPicker.tooltip.isShown()) {
+      this.colorPicker.revert();
+      this.colorPicker.hide();
+    }
+
+    let inspector = this.view.inspector;
+
+    if (type === TOOLTIP_IMAGE_TYPE) {
+      let dim = Services.prefs.getIntPref(PREF_IMAGE_TOOLTIP_SIZE);
+      let uri = CssLogic.getBackgroundImageUriFromProperty(nodeInfo.value.value,
+        nodeInfo.value.sheetHref); // sheetHref is undefined for computed-view
+                                   // properties, but we don't care as uris are
+                                   // absolute
+      return this.previewTooltip.setRelativeImageContent(uri,
+        inspector.inspector, dim);
+    }
+
+    if (type === TOOLTIP_FONTFAMILY_TYPE) {
+      return this.previewTooltip.setFontFamilyContent(nodeInfo.value.value,
+        inspector.selection.nodeFront);
+    }
+  },
+
+  _onNewSelection: function() {
+    if (this.previewTooltip) {
+      this.previewTooltip.hide();
+    }
+
+    if (this.colorPicker) {
+      this.colorPicker.hide();
+    }
+  },
+
+  /**
+   * Destroy this overlay instance, removing it from the view
+   */
+  destroy: function() {
+    this.removeFromView();
+
+    this.view.inspector.selection.off("new-node-front", this._onNewSelection);
+    this.view = null;
+
+    this._isDestroyed = true;
+  }
+};
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_01.js
@@ -25,22 +25,22 @@ let test = asyncTest(function*() {
   let value = getRuleViewProperty(view, "body", "background").valueSpan;
   let swatch = value.querySelector(".ruleview-colorswatch");
   let url = value.querySelector(".theme-link");
   yield testImageTooltipAfterColorChange(swatch, url, view);
 });
 
 function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
   info("First, verify that the image preview tooltip works");
-  let anchor = yield isHoverTooltipTarget(ruleView.previewTooltip, url);
+  let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
   ok(anchor, "The image preview tooltip is shown on the url span");
   is(anchor, url, "The anchor returned by the showOnHover callback is correct");
 
   info("Open the color picker tooltip and change the color");
-  let picker = ruleView.colorPicker;
+  let picker = ruleView.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
   yield simulateColorPickerChange(picker, [0, 0, 0, 1], {
     element: content.document.body,
     name: "backgroundImage",
     value: 'url("chrome://global/skin/icons/warning-64.png"), linear-gradient(rgb(0, 0, 0), rgb(255, 0, 102) 400px)'
   });
@@ -49,12 +49,12 @@ function* testImageTooltipAfterColorChan
   let onHidden = picker.tooltip.once("hidden");
   EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
   yield onHidden;
 
   info("Verify again that the image preview tooltip works");
   // After a color change, the property is re-populated, we need to get the new
   // dom node
   url = getRuleViewProperty(ruleView, "body", "background").valueSpan.querySelector(".theme-link");
-  let anchor = yield isHoverTooltipTarget(ruleView.previewTooltip, url);
+  let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
   ok(anchor, "The image preview tooltip is shown on the url span");
   is(anchor, url, "The anchor returned by the showOnHover callback is correct");
 }
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-and-image-tooltip_02.js
@@ -26,17 +26,17 @@ let test = asyncTest(function*() {
   yield testColorChangeIsntRevertedWhenOtherTooltipIsShown(view);
 });
 
 function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
   let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   info("Open the color picker tooltip and change the color");
-  let picker = ruleView.colorPicker;
+  let picker = ruleView.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   yield simulateColorPickerChange(picker, [0, 0, 0, 1], {
     element: content.document.body,
     name: "backgroundColor",
     value: "rgb(0, 0, 0)"
@@ -44,18 +44,18 @@ function* testColorChangeIsntRevertedWhe
   let spectrum = yield picker.spectrum;
   let onHidden = picker.tooltip.once("hidden");
   EventUtils.sendKey("RETURN", spectrum.element.ownerDocument.defaultView);
   yield onHidden;
 
   info("Open the image preview tooltip");
   let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
   let url = value.querySelector(".theme-link");
-  let onShown = ruleView.previewTooltip.once("shown");
-  let anchor = yield isHoverTooltipTarget(ruleView.previewTooltip, url);
-  ruleView.previewTooltip.show(anchor);
+  let onShown = ruleView.tooltips.previewTooltip.once("shown");
+  let anchor = yield isHoverTooltipTarget(ruleView.tooltips.previewTooltip, url);
+  ruleView.tooltips.previewTooltip.show(anchor);
   yield onShown;
 
   info("Image tooltip is shown, verify that the swatch is still correct");
   let swatch = value.querySelector(".ruleview-colorswatch");
   is(swatch.style.backgroundColor, "rgb(0, 0, 0)", "The swatch's color is correct");
   is(swatch.nextSibling.textContent, "#000", "The color name is correct");
 }
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-appears-on-swatch-click.js
@@ -32,17 +32,17 @@ let test = asyncTest(function*() {
 
   for (let swatch of [cSwatch, bgSwatch, bSwatch]) {
     info("Testing that the colorpicker appears colorswatch click");
     yield testColorPickerAppearsOnColorSwatchClick(view, swatch);
   }
 });
 
 function* testColorPickerAppearsOnColorSwatchClick(view, swatch) {
-  let cPicker = view.colorPicker;
+  let cPicker = view.tooltips.colorPicker;
   ok(cPicker, "The rule-view has the expected colorPicker property");
 
   let cPickerPanel = cPicker.tooltip.panel;
   ok(cPickerPanel, "The XUL panel for the color picker exists");
 
   let onShown = cPicker.tooltip.once("shown");
   swatch.click();
   yield onShown;
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-commit-on-ENTER.js
@@ -21,17 +21,17 @@ let test = asyncTest(function*() {
   let {toolbox, inspector, view} = yield openRuleView();
 
   let swatch = getRuleViewProperty(view, "body" , "border").valueSpan
     .querySelector(".ruleview-colorswatch");
   yield testPressingEnterCommitsChanges(swatch, view);
 });
 
 function* testPressingEnterCommitsChanges(swatch, ruleView) {
-  let cPicker = ruleView.colorPicker;
+  let cPicker = ruleView.tooltips.colorPicker;
 
   let onShown = cPicker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   yield simulateColorPickerChange(cPicker, [0, 255, 0, .5], {
     element: content.document.body,
     name: "borderLeftColor",
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-edit-gradient.js
@@ -52,17 +52,17 @@ function testColorParsing(view) {
 
 function* testPickingNewColor(view) {
   // Grab the first color swatch and color in the gradient
   let ruleEl = getRuleViewProperty(view, "body", "background-image");
   let swatchEl = ruleEl.valueSpan.querySelector(".ruleview-colorswatch");
   let colorEl = ruleEl.valueSpan.querySelector(".ruleview-color");
 
   info("Getting the color picker tooltip and clicking on the swatch to show it");
-  let cPicker = view.colorPicker;
+  let cPicker = view.tooltips.colorPicker;
   let onShown = cPicker.tooltip.once("shown");
   swatchEl.click();
   yield onShown;
 
   yield simulateColorPickerChange(cPicker, [1, 1, 1, 1]);
 
   is(swatchEl.style.backgroundColor, "rgb(1, 1, 1)",
     "The color swatch's background was updated");
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-hides-on-tooltip.js
@@ -27,22 +27,22 @@ let test = asyncTest(function*() {
     .querySelector(".ruleview-colorswatch");
 
   yield testColorPickerHidesWhenImageTooltipAppears(view, swatch);
 });
 
 function* testColorPickerHidesWhenImageTooltipAppears(view, swatch) {
   let bgImageSpan = getRuleViewProperty(view, "body", "background-image").valueSpan;
   let uriSpan = bgImageSpan.querySelector(".theme-link");
-  let tooltip = view.colorPicker.tooltip;
+  let tooltip = view.tooltips.colorPicker.tooltip;
 
   info("Showing the color picker tooltip by clicking on the color swatch");
   let onShown = tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   info("Now showing the image preview tooltip to hide the color picker");
   let onHidden = tooltip.once("hidden");
-  yield assertHoverTooltipOn(view.previewTooltip, uriSpan);
+  yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
   yield onHidden;
 
   ok(true, "The color picker closed when the image preview tooltip appeared");
 }
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-multiple-changes.js
@@ -37,17 +37,17 @@ let test = asyncTest(function*() {
 function* testSimpleMultipleColorChanges(inspector, ruleView) {
   yield selectNode("p", inspector);
 
   info("Getting the <p> tag's color property");
   let swatch = getRuleViewProperty(ruleView, "p", "color").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   info("Opening the color picker");
-  let picker = ruleView.colorPicker;
+  let picker = ruleView.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   info("Changing the color several times");
   let colors = [
     {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
     {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
@@ -65,17 +65,17 @@ function* testSimpleMultipleColorChanges
 function* testComplexMultipleColorChanges(inspector, ruleView) {
   yield selectNode("body", inspector);
 
   info("Getting the <body> tag's color property");
   let swatch = getRuleViewProperty(ruleView, "body", "background").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   info("Opening the color picker");
-  let picker = ruleView.colorPicker;
+  let picker = ruleView.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   info("Changing the color several times");
   let colors = [
     {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
     {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
@@ -98,17 +98,17 @@ function* testComplexMultipleColorChange
 function* testOverriddenMultipleColorChanges(inspector, ruleView) {
   yield selectNode("p", inspector);
 
   info("Getting the <body> tag's color property");
   let swatch = getRuleViewProperty(ruleView, "body", "color").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   info("Opening the color picker");
-  let picker = ruleView.colorPicker;
+  let picker = ruleView.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   info("Changing the color several times");
   let colors = [
     {rgba: [0, 0, 0, 1], computed: "rgb(0, 0, 0)"},
     {rgba: [100, 100, 100, 1], computed: "rgb(100, 100, 100)"},
--- a/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_colorpicker-revert-on-ESC.js
@@ -21,17 +21,17 @@ let test = asyncTest(function*() {
   let {toolbox, inspector, view} = yield openRuleView();
 
   let swatch = getRuleViewProperty(view, "body", "background-color").valueSpan
     .querySelector(".ruleview-colorswatch");
   yield testPressingEscapeRevertsChanges(swatch, view);
 });
 
 function* testPressingEscapeRevertsChanges(swatch, ruleView) {
-  let cPicker = ruleView.colorPicker;
+  let cPicker = ruleView.tooltips.colorPicker;
 
   let onShown = cPicker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   yield simulateColorPickerChange(cPicker, [0, 0, 0, 1], {
     element: content.document.body,
     name: "backgroundColor",
--- a/browser/devtools/styleinspector/test/browser_ruleview_eyedropper.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_eyedropper.js
@@ -35,17 +35,17 @@ let test = asyncTest(function*() {
   yield inspector.once("inspector-updated");
 
   let property = getRuleViewProperty(view, "div", "background-color");
   let swatch = property.valueSpan.querySelector(".ruleview-colorswatch");
   ok(swatch, "Color swatch is displayed for the bg-color property");
 
   let dropper = yield openEyedropper(view, swatch);
 
-  let tooltip = view.colorPicker.tooltip;
+  let tooltip = view.tooltips.colorPicker.tooltip;
   ok(tooltip.isHidden(),
      "color picker tooltip is closed after opening eyedropper");
 
   yield testESC(swatch, dropper);
 
   dropper = yield openEyedropper(view, swatch);
 
   ok(dropper, "dropper opened");
@@ -93,17 +93,17 @@ function testSelect(swatch, dropper) {
 }
 
 
 /* Helpers */
 
 function openEyedropper(view, swatch) {
   let deferred = promise.defer();
 
-  let tooltip = view.colorPicker.tooltip;
+  let tooltip = view.tooltips.colorPicker.tooltip;
 
   tooltip.once("shown", () => {
     let tooltipDoc = tooltip.content.contentDocument;
     let dropperButton = tooltipDoc.querySelector("#eyedropper-button");
 
     tooltip.once("eyedropper-opened", (event, dropper) => {
       deferred.resolve(dropper)
     });
--- a/browser/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_user-agent-styles-uneditable.js
@@ -39,12 +39,12 @@ function* userAgentStylesUneditable(insp
     ok (rule.editor.element.hasAttribute("uneditable"), "UA rules have uneditable attribute");
 
     ok (!rule.textProps[0].editor.nameSpan._editable, "nameSpan is not editable");
     ok (!rule.textProps[0].editor.valueSpan._editable, "valueSpan is not editable");
     ok (!rule.editor.closeBrace._editable, "closeBrace is not editable");
 
     let colorswatch = rule.editor.element.querySelector(".ruleview-colorswatch");
     if (colorswatch) {
-      ok (!view.colorPicker.swatches.has(colorswatch), "The swatch is not editable");
+      ok (!view.tooltips.colorPicker.swatches.has(colorswatch), "The swatch is not editable");
     }
   }
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_context-menu-copy-color_02.js
@@ -77,17 +77,17 @@ function* testManualEdit(inspector, view
 function* testColorPickerEdit(inspector, view) {
   info("Testing colors edited via color picker");
   yield selectNode("div", inspector);
 
   let swatch = getRuleViewProperty(view, "div", "color").valueSpan
     .querySelector(".ruleview-colorswatch");
 
   info("Opening the color picker");
-  let picker = view.colorPicker;
+  let picker = view.tooltips.colorPicker;
   let onShown = picker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   let rgbaColor = [83, 183, 89, 1];
   let rgbaColorText = "rgba(83, 183, 89, 1)";
   yield simulateColorPickerChange(picker, rgbaColor);
 
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-background-image.js
@@ -45,77 +45,77 @@ let test = asyncTest(function*() {
   let {view} = yield openComputedView();
 
   info("Testing that the background-image computed style has a tooltip too");
   yield testComputedView(view);
 });
 
 function* testBodyRuleView(view) {
   info("Testing tooltips in the rule view");
-  let panel = view.previewTooltip.panel;
+  let panel = view.tooltips.previewTooltip.panel;
 
   // Check that the rule view has a tooltip and that a XUL panel has been created
-  ok(view.previewTooltip, "Tooltip instance exists");
+  ok(view.tooltips.previewTooltip, "Tooltip instance exists");
   ok(panel, "XUL panel exists");
 
   // Get the background-image property inside the rule view
   let {valueSpan} = getRuleViewProperty(view, "body", "background-image");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
-  yield assertHoverTooltipOn(view.previewTooltip, uriSpan);
+  yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1,
     "The image URL seems fine");
 }
 
 function* testDivRuleView(view) {
-  let panel = view.previewTooltip.panel;
+  let panel = view.tooltips.previewTooltip.panel;
 
   // Get the background property inside the rule view
   let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
-  yield assertHoverTooltipOn(view.previewTooltip, uriSpan);
+  yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 }
 
 function* testTooltipAppearsEvenInEditMode(view) {
-  let panel = view.previewTooltip.panel;
+  let panel = view.tooltips.previewTooltip.panel;
 
   info("Switching to edit mode in the rule view");
   let editor = yield turnToEditMode(view);
 
   info("Now trying to show the preview tooltip");
   let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
   let uriSpan = valueSpan.querySelector(".theme-link");
-  yield assertHoverTooltipOn(view.previewTooltip, uriSpan);
+  yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
   is(view.doc.activeElement, editor.input,
     "Tooltip was shown in edit mode, and inplace-editor still focused");
 }
 
 function turnToEditMode(ruleView) {
   let brace = ruleView.doc.querySelector(".ruleview-ruleclose");
   return focusEditableField(brace);
 }
 
 function* testComputedView(view) {
-  let tooltip = view.tooltip;
+  let tooltip = view.tooltips.previewTooltip;
   ok(tooltip, "The computed-view has a tooltip defined");
 
   let panel = tooltip.panel;
   ok(panel, "The computed-view tooltip has a XUL panel");
 
   let {valueSpan} = getComputedViewProperty(view, "background-image");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
-  yield assertHoverTooltipOn(view.tooltip, uriSpan);
+  yield assertHoverTooltipOn(view.tooltips.previewTooltip, uriSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
 
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri in the computed-view too");
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-closes-on-new-selection.js
@@ -17,31 +17,31 @@ let test = asyncTest(function*() {
 
   info("Testing computed view tooltip closes on new selection");
   let {view} = yield openComputedView();
   yield testComputedView(view, inspector);
 });
 
 function* testRuleView(ruleView, inspector) {
   info("Showing the tooltip");
-  let tooltip = ruleView.previewTooltip;
+  let tooltip = ruleView.tooltips.previewTooltip;
   let onShown = tooltip.once("shown");
   tooltip.show();
   yield onShown;
 
   info("Selecting a new node");
   let onHidden = tooltip.once("hidden");
   yield selectNode(".two", inspector);
 
   ok(true, "Rule view tooltip closed after a new node got selected");
 }
 
 function* testComputedView(computedView, inspector) {
   info("Showing the tooltip");
-  let tooltip = computedView.tooltip;
+  let tooltip = computedView.tooltips.previewTooltip;
   let onShown = tooltip.once("shown");
   tooltip.show();
   yield onShown;
 
   info("Selecting a new node");
   let onHidden = tooltip.once("hidden");
   yield selectNode(".one", inspector);
 
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-longhand-fontfamily.js
@@ -35,43 +35,45 @@ let test = asyncTest(function*() {
   let {toolbox, inspector, view} = yield openComputedView();
 
   yield testComputedView(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
   info("Testing font-family tooltips in the rule view");
 
-  let panel = ruleView.previewTooltip.panel;
+  let tooltip = ruleView.tooltips.previewTooltip;
+  let panel = tooltip.panel;
 
   // Check that the rule view has a tooltip and that a XUL panel has been created
-  ok(ruleView.previewTooltip, "Tooltip instance exists");
+  ok(tooltip, "Tooltip instance exists");
   ok(panel, "XUL panel exists");
 
   // Get the font family property inside the rule view
   let {valueSpan} = getRuleViewProperty(ruleView, "#testElement", "font-family");
 
   // And verify that the tooltip gets shown on this property
-  yield assertHoverTooltipOn(ruleView.previewTooltip, valueSpan);
+  yield assertHoverTooltipOn(tooltip, valueSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
 
 function* testComputedView(computedView, nodeFront) {
   info("Testing font-family tooltips in the computed view");
 
-  let panel = computedView.tooltip.panel;
+  let tooltip = computedView.tooltips.previewTooltip;
+  let panel = tooltip.panel;
   let {valueSpan} = getComputedViewProperty(computedView, "font-family");
 
-  yield assertHoverTooltipOn(computedView.tooltip, valueSpan);
+  yield assertHoverTooltipOn(tooltip, valueSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-shorthand-fontfamily.js
@@ -33,48 +33,51 @@ let test = asyncTest(function*() {
   let {toolbox, inspector, view} = yield openComputedView();
 
   yield testComputedView(view, inspector.selection.nodeFront);
 });
 
 function* testRuleView(ruleView, nodeFront) {
   info("Testing font-family tooltips in the rule view");
 
-  let panel = ruleView.previewTooltip.panel;
+  let tooltip = ruleView.tooltips.previewTooltip;
+  let panel = tooltip.panel;
 
   // Check that the rule view has a tooltip and that a XUL panel has been created
-  ok(ruleView.previewTooltip, "Tooltip instance exists");
+  ok(tooltip, "Tooltip instance exists");
   ok(panel, "XUL panel exists");
 
   // Get the computed font family property inside the font rule view
   let propertyList = ruleView.element.querySelectorAll(".ruleview-propertylist");
   let fontExpander = propertyList[1].querySelectorAll(".ruleview-expander")[0];
   fontExpander.click();
 
   let rule = getRuleViewRule(ruleView, "#testElement");
   let valueSpan = rule.querySelector(".ruleview-computed .ruleview-propertyvalue");
 
   // And verify that the tooltip gets shown on this property
-  yield assertHoverTooltipOn(ruleView.previewTooltip, valueSpan);
+  yield assertHoverTooltipOn(tooltip, valueSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
 
 function* testComputedView(computedView, nodeFront) {
   info("Testing font-family tooltips in the computed view");
 
-  let panel = computedView.tooltip.panel;
+  let tooltip = computedView.tooltips.previewTooltip;
+  let panel = tooltip.panel;
+
   let {valueSpan} = getComputedViewProperty(computedView, "font-family");
 
-  yield assertHoverTooltipOn(computedView.tooltip, valueSpan);
+  yield assertHoverTooltipOn(tooltip, valueSpan);
 
   let images = panel.getElementsByTagName("image");
   is(images.length, 1, "Tooltip contains an image");
   ok(images[0].getAttribute("src").startsWith("data:"), "Tooltip contains a data-uri image as expected");
 
   let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
   is(images[0].getAttribute("src"), dataURL, "Tooltip contains the correct data-uri image");
 }
--- a/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_tooltip-size.js
@@ -26,17 +26,17 @@ let test = asyncTest(function*() {
 
   yield testImageDimension(view);
   yield testPickerDimension(view);
 });
 
 function* testImageDimension(ruleView) {
   info("Testing background-image tooltip dimensions");
 
-  let tooltip = ruleView.previewTooltip;
+  let tooltip = ruleView.tooltips.previewTooltip;
   let panel = tooltip.panel;
   let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
   let uriSpan = valueSpan.querySelector(".theme-link");
 
   // Make sure there is a hover tooltip for this property, this also will fill
   // the tooltip with its content
   yield assertHoverTooltipOn(tooltip, uriSpan);
 
@@ -60,17 +60,17 @@ function* testImageDimension(ruleView) {
   yield onHidden;
 }
 
 function* testPickerDimension(ruleView) {
   info("Testing color-picker tooltip dimensions");
 
   let {valueSpan} = getRuleViewProperty(ruleView, "div", "background");
   let swatch = valueSpan.querySelector(".ruleview-colorswatch");
-  let cPicker = ruleView.colorPicker;
+  let cPicker = ruleView.tooltips.colorPicker;
 
   let onShown = cPicker.tooltip.once("shown");
   swatch.click();
   yield onShown;
 
   // The colorpicker spectrum's iframe has a fixed width height, so let's
   // make sure the tooltip is at least as big as that
   let w = cPicker.tooltip.panel.querySelector("iframe").width;
--- a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-01.js
@@ -10,29 +10,33 @@ const PAGE_CONTENT = [
   '<style type="text/css">',
   '  body {',
   '    transform: skew(16deg);',
   '  }',
   '</style>',
   'Test the css transform highlighter'
 ].join("\n");
 
+const TYPE = "CssTransformHighlighter";
+
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + PAGE_CONTENT);
 
   let {view: rView} = yield openRuleView();
+  let overlay = rView.highlighters;
 
-  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view");
-  let h = yield rView.getTransformHighlighter();
-  ok(rView.transformHighlighter, "The highlighter has been created in the rule-view");
-  is(h, rView.transformHighlighter, "The right highlighter has been created");
-  let h2 = yield rView.getTransformHighlighter();
+  ok(!overlay.highlighters[TYPE], "No highlighter exists in the rule-view");
+  let h = yield overlay._getHighlighter(TYPE);
+  ok(overlay.highlighters[TYPE], "The highlighter has been created in the rule-view");
+  is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+  let h2 = yield overlay._getHighlighter(TYPE);
   is(h, h2, "The same instance of highlighter is returned everytime in the rule-view");
 
   let {view: cView} = yield openComputedView();
+  let overlay = cView.highlighters;
 
-  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view");
-  let h = yield cView.getTransformHighlighter();
-  ok(cView.transformHighlighter, "The highlighter has been created in the computed-view");
-  is(h, cView.transformHighlighter, "The right highlighter has been created");
-  let h2 = yield cView.getTransformHighlighter();
+  ok(!overlay.highlighters[TYPE], "No highlighter exists in the computed-view");
+  let h = yield overlay._getHighlighter(TYPE);
+  ok(overlay.highlighters[TYPE], "The highlighter has been created in the computed-view");
+  is(h, overlay.highlighters[TYPE], "The right highlighter has been created");
+  let h2 = yield overlay._getHighlighter(TYPE);
   is(h, h2, "The same instance of highlighter is returned everytime in the computed-view");
 });
--- a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-02.js
@@ -12,43 +12,52 @@ const PAGE_CONTENT = [
   '  body {',
   '    transform: skew(16deg);',
   '    color: yellow;',
   '  }',
   '</style>',
   'Test the css transform highlighter'
 ].join("\n");
 
+let TYPE = "CssTransformHighlighter";
+
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + PAGE_CONTENT);
 
+
   let {view: rView} = yield openRuleView();
-  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (1)");
+  let hs = rView.highlighters;
+
+  ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (1)");
+  ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (1)");
 
   info("Faking a mousemove on a non-transform property");
   let {valueSpan} = getRuleViewProperty(rView, "body", "color");
-  rView._onMouseMove({target: valueSpan});
-  ok(!rView.transformHighlighter, "No highlighter exists in the rule-view (2)");
-  ok(!rView.transformHighlighterPromise, "No highlighter is being initialized");
+  hs._onMouseMove({target: valueSpan});
+  ok(!hs.highlighters[TYPE], "No highlighter exists in the rule-view (2)");
+  ok(!hs.promises[TYPE], "No highlighter is being created in the rule-view (2)");
 
   info("Faking a mousemove on a transform property");
   let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
-  rView._onMouseMove({target: valueSpan});
-  ok(rView.transformHighlighterPromise, "The highlighter is being initialized");
-  let h = yield rView.transformHighlighterPromise;
-  is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
+  hs._onMouseMove({target: valueSpan});
+  ok(hs.promises[TYPE], "The highlighter is being initialized");
+  let h = yield hs.promises[TYPE];
+  is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
 
   let {view: cView} = yield openComputedView();
-  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (1)");
+  let hs = cView.highlighters;
+
+  ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (1)");
+  ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (1)");
 
   info("Faking a mousemove on a non-transform property");
   let {valueSpan} = getComputedViewProperty(cView, "color");
-  cView._onMouseMove({target: valueSpan});
-  ok(!cView.transformHighlighter, "No highlighter exists in the computed-view (2)");
-  ok(!cView.transformHighlighterPromise, "No highlighter is being initialized");
+  hs._onMouseMove({target: valueSpan});
+  ok(!hs.highlighters[TYPE], "No highlighter exists in the computed-view (2)");
+  ok(!hs.promises[TYPE], "No highlighter is being created in the computed-view (2)");
 
   info("Faking a mousemove on a transform property");
   let {valueSpan} = getComputedViewProperty(cView, "transform");
-  cView._onMouseMove({target: valueSpan});
-  ok(cView.transformHighlighterPromise, "The highlighter is being initialized");
-  let h = yield cView.transformHighlighterPromise;
-  is(h, cView.transformHighlighter, "The initialized highlighter is the right one");
+  hs._onMouseMove({target: valueSpan});
+  ok(hs.promises[TYPE], "The highlighter is being initialized");
+  let h = yield hs.promises[TYPE];
+  is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
 });
--- a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-03.js
@@ -18,16 +18,18 @@ const PAGE_CONTENT = [
   '  body {',
   '    transform: skew(16deg);',
   '    color: purple;',
   '  }',
   '</style>',
   'Test the css transform highlighter'
 ].join("\n");
 
+const TYPE = "CssTransformHighlighter";
+
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + PAGE_CONTENT);
 
   let {inspector, view: rView} = yield openRuleView();
 
   // Mock the highlighter front to get the reference of the NodeFront
   let HighlighterFront = {
     isShown: false,
@@ -40,50 +42,50 @@ let test = asyncTest(function*() {
     },
     hide: function() {
       this.nodeFront = null;
       this.isShown = false;
     }
   };
 
   // Inject the mock highlighter in the rule-view
-  rView.transformHighlighterPromise = {
+  rView.highlighters.promises[TYPE] = {
     then: function(cb) {
       cb(HighlighterFront);
     }
   };
 
   let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
 
   info("Checking that the HighlighterFront's show/hide methods are called");
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   ok(HighlighterFront.isShown, "The highlighter is shown");
-  rView._onMouseLeave();
+  rView.highlighters._onMouseLeave();
   ok(!HighlighterFront.isShown, "The highlighter is hidden");
 
   info("Checking that hovering several times over the same property doesn't" +
     " show the highlighter several times");
   let nb = HighlighterFront.nbOfTimesShown;
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   is(HighlighterFront.nbOfTimesShown, nb + 1, "The highlighter was shown once");
-  rView._onMouseMove({target: valueSpan});
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   is(HighlighterFront.nbOfTimesShown, nb + 1,
     "The highlighter was shown once, after several mousemove");
 
   info("Checking that the right NodeFront reference is passed");
   yield selectNode(content.document.documentElement, inspector);
   let {valueSpan} = getRuleViewProperty(rView, "html", "transform");
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   is(HighlighterFront.nodeFront.tagName, "HTML",
     "The right NodeFront is passed to the highlighter (1)");
 
   yield selectNode("body", inspector);
   let {valueSpan} = getRuleViewProperty(rView, "body", "transform");
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   is(HighlighterFront.nodeFront.tagName, "BODY",
     "The right NodeFront is passed to the highlighter (2)");
 
   info("Checking that the highlighter gets hidden when hovering a non-transform property");
   let {valueSpan} = getRuleViewProperty(rView, "body", "color");
-  rView._onMouseMove({target: valueSpan});
+  rView.highlighters._onMouseMove({target: valueSpan});
   ok(!HighlighterFront.isShown, "The highlighter is hidden");
 });
--- a/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
+++ b/browser/devtools/styleinspector/test/browser_styleinspector_transform-highlighter-04.js
@@ -20,39 +20,43 @@ const PAGE_CONTENT = [
   '  }',
   '  .test {',
   '    transform: skew(25deg);',
   '  }',
   '</style>',
   '<div class="test"></div>'
 ].join("\n");
 
+const TYPE = "CssTransformHighlighter";
+
 let test = asyncTest(function*() {
   yield addTab("data:text/html," + PAGE_CONTENT);
 
   let {view: rView, inspector} = yield openRuleView();
   yield selectNode(".test", inspector);
 
+  let hs = rView.highlighters;
+
   info("Faking a mousemove on the overriden property");
   let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
-  rView._onMouseMove({target: valueSpan});
-  ok(!rView.transformHighlighter, "No highlighter was created for the overriden property");
-  ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
+  hs._onMouseMove({target: valueSpan});
+  ok(!hs.highlighters[TYPE], "No highlighter was created for the overriden property");
+  ok(!hs.promises[TYPE], "And no highlighter is being initialized either");
 
   info("Disabling the applied property");
   let classRuleEditor = rView.element.children[1]._ruleEditor;
   let propEditor = classRuleEditor.rule.textProps[0].editor;
   propEditor.enable.click();
   yield classRuleEditor.rule._applyingModifications;
 
   info("Faking a mousemove on the disabled property");
   let {valueSpan} = getRuleViewProperty(rView, ".test", "transform");
-  rView._onMouseMove({target: valueSpan});
-  ok(!rView.transformHighlighter, "No highlighter was created for the disabled property");
-  ok(!rView.transformHighlighterPromise, "And no highlighter is being initialized either");
+  hs._onMouseMove({target: valueSpan});
+  ok(!hs.highlighters[TYPE], "No highlighter was created for the disabled property");
+  ok(!hs.promises[TYPE], "And no highlighter is being initialized either");
 
   info("Faking a mousemove on the now unoverriden property");
   let {valueSpan} = getRuleViewProperty(rView, "div", "transform");
-  rView._onMouseMove({target: valueSpan});
-  ok(rView.transformHighlighterPromise, "The highlighter is being initialized now");
-  let h = yield rView.transformHighlighterPromise;
-  is(h, rView.transformHighlighter, "The initialized highlighter is the right one");
+  hs._onMouseMove({target: valueSpan});
+  ok(hs.promises[TYPE], "The highlighter is being initialized now");
+  let h = yield hs.promises[TYPE];
+  is(h, hs.highlighters[TYPE], "The initialized highlighter is the right one");
 });