Bug 765105 - Tooltip shared component, showing image previews in markup, css rules and computed views, r=miker,harth
authorPatrick Brosset <pbrosset@mozilla.com>
Sat, 26 Oct 2013 00:51:01 +0530
changeset 167048 2bcd412cf362a2b11525c28ec8a91458faf22b11
parent 167047 02b75478ab473e7c83759f5a9e3019398d9ce014
child 167049 954741a373f33169f3253e0477ae2a5ddd825284
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmiker, harth
bugs765105
milestone27.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 765105 - Tooltip shared component, showing image previews in markup, css rules and computed views, r=miker,harth
browser/devtools/markupview/markup-view.js
browser/devtools/markupview/test/browser.ini
browser/devtools/markupview/test/browser_inspector_markup_765105_tooltip.js
browser/devtools/markupview/test/browser_inspector_markup_765105_tooltip.png
browser/devtools/shared/Makefile.in
browser/devtools/shared/widgets/Tooltip.js
browser/devtools/styleinspector/computed-view.js
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/style-inspector.js
browser/devtools/styleinspector/test/Makefile.in
browser/devtools/styleinspector/test/browser_bug765105_background_image_tooltip.js
browser/locales/en-US/chrome/browser/devtools/inspector.properties
browser/themes/shared/devtools/common.inc.css
toolkit/devtools/server/actors/inspector.js
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -16,16 +16,17 @@ const COLLAPSE_DATA_URL_LENGTH = 60;
 const CONTAINER_FLASHING_DURATION = 500;
 
 const {UndoStack} = require("devtools/shared/undo");
 const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 const {HTMLEditor} = require("devtools/markupview/html-editor");
 const {OutputParser} = require("devtools/output-parser");
 const promise = require("sdk/core/promise");
+const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 
 Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 loader.lazyGetter(this, "DOMParser", function() {
  return Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
 });
@@ -369,17 +370,17 @@ MarkupView.prototype = {
       return this._containers.get(aNode);
     }
 
     if (aNode === this.walker.rootNode) {
       var container = new RootContainer(this, aNode);
       this._elt.appendChild(container.elt);
       this._rootNode = aNode;
     } else {
-      var container = new MarkupContainer(this, aNode);
+      var container = new MarkupContainer(this, aNode, this._inspector);
       if (aFlashNode) {
         container.flashMutation();
       }
     }
 
     this._containers.set(aNode, container);
     container.childrenDirty = true;
 
@@ -1041,22 +1042,25 @@ MarkupView.prototype = {
  * tree.  Manages creation of the editor for the node and
  * a <ul> for placing child elements, and expansion/collapsing
  * of the element.
  *
  * @param MarkupView aMarkupView
  *        The markup view that owns this container.
  * @param DOMNode aNode
  *        The node to display.
+ * @param Inspector aInspector
+ *        The inspector tool container the markup-view
  */
-function MarkupContainer(aMarkupView, aNode) {
+function MarkupContainer(aMarkupView, aNode, aInspector) {
   this.markup = aMarkupView;
   this.doc = this.markup.doc;
   this.undo = this.markup.undo;
   this.node = aNode;
+  this._inspector = aInspector;
 
   if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE) {
     this.editor = new TextEditor(this, aNode, "text");
   } else if (aNode.nodeType == Ci.nsIDOMNode.COMMENT_NODE) {
     this.editor = new TextEditor(this, aNode, "comment");
   } else if (aNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
     this.editor = new ElementEditor(this, aNode);
   } else if (aNode.nodeType == Ci.nsIDOMNode.DOCUMENT_TYPE_NODE) {
@@ -1089,23 +1093,59 @@ function MarkupContainer(aMarkupView, aN
   this._onMouseOut = this._onMouseOut.bind(this);
   this.elt.addEventListener("mouseout", this._onMouseOut, false);
 
   // Appending the editor element and attaching event listeners
   this.tagLine.appendChild(this.editor.elt);
 
   this._onMouseDown = this._onMouseDown.bind(this);
   this.elt.addEventListener("mousedown", this._onMouseDown, false);
+
+  this.tooltip = null;
+  this._attachTooltipIfNeeded();
 }
 
 MarkupContainer.prototype = {
   toString: function() {
     return "[MarkupContainer for " + this.node + "]";
   },
 
+  _attachTooltipIfNeeded: function() {
+    if (this.node.tagName) {
+      let tagName = this.node.tagName.toLowerCase();
+      let isImage = tagName === "img" &&
+        this.editor.getAttributeElement("src");
+      let isCanvas = tagName && tagName === "canvas";
+
+      // Get the image data for later so that when the user actually hovers over
+      // the element, the tooltip does contain the image
+      if (isImage || isCanvas) {
+        this.tooltip = new Tooltip(this._inspector.panelDoc);
+
+        this.node.getImageData().then(data => {
+          if (data) {
+            data.string().then(str => {
+              this.tooltip.setImageContent(str);
+            });
+          }
+        });
+      }
+
+      // If it's an image, show the tooltip on the src attribute
+      if (isImage) {
+        this.tooltip.startTogglingOnHover(this.editor.getAttributeElement("src"));
+      }
+
+      // If it's a canvas, show it on the tag
+      if (isCanvas) {
+        this.tooltip.startTogglingOnHover(this.editor.tag);
+      }
+    }
+  },
+
   /**
    * True if the current node has children.  The MarkupView
    * will set this attribute for the MarkupContainer.
    */
   _hasChildren: false,
 
   get hasChildren() {
     return this._hasChildren;
@@ -1330,30 +1370,36 @@ MarkupContainer.prototype = {
     this.elt.removeEventListener("dblclick", this._onToggle, false);
     this.elt.removeEventListener("mouseover", this._onMouseOver, false);
     this.elt.removeEventListener("mouseout", this._onMouseOut, false);
     this.elt.removeEventListener("mousedown", this._onMouseDown, false);
     this.expander.removeEventListener("click", this._onToggle, false);
 
     // Destroy my editor
     this.editor.destroy();
+
+    // Destroy the tooltip if any
+    if (this.tooltip) {
+      this.tooltip.destroy();
+      this.tooltip = null;
+    }
   }
 };
 
 
 /**
  * Dummy container node used for the root document element.
  */
 function RootContainer(aMarkupView, aNode) {
   this.doc = aMarkupView.doc;
   this.elt = this.doc.createElement("ul");
   this.elt.container = this;
   this.children = this.elt;
   this.node = aNode;
-  this.toString = function() { return "[root container]"}
+  this.toString = () => "[root container]";
 }
 
 RootContainer.prototype = {
   hasChildren: true,
   expanded: true,
   update: function() {},
   destroy: function() {}
 };
@@ -1573,16 +1619,26 @@ ElementEditor.prototype = {
       }
     }
   },
 
   _startModifyingAttributes: function() {
     return this.node.startModifyingAttributes();
   },
 
+  /**
+   * Get the element used for one of the attributes of this element
+   * @param string attrName The name of the attribute to get the element for
+   * @return DOMElement
+   */
+  getAttributeElement: function(attrName) {
+    return this.attrList.querySelector(
+      ".attreditor[data-attr=" + attrName + "] .attr-value");
+  },
+
   _createAttribute: function(aAttr, aBefore = null) {
     // Create the template editor, which will save some variables here.
     let data = {
       attrName: aAttr.name,
     };
     this.template("attribute", data);
     var {attr, inner, name, val} = data;
 
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -10,8 +10,10 @@ skip-if = true
 [browser_inspector_markup_mutation.html]
 [browser_inspector_markup_mutation.js]
 [browser_inspector_markup_mutation_flashing.html]
 [browser_inspector_markup_mutation_flashing.js]
 [browser_inspector_markup_navigation.html]
 [browser_inspector_markup_navigation.js]
 [browser_inspector_markup_subset.html]
 [browser_inspector_markup_subset.js]
+[browser_inspector_markup_765105_tooltip.js]
+[browser_inspector_markup_765105_tooltip.png]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_765105_tooltip.js
@@ -0,0 +1,137 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let {PanelFactory} = devtools.require("devtools/shared/widgets/Tooltip");
+
+let contentDoc;
+let inspector;
+let markup;
+
+const PAGE_CONTENT = [
+  '<img class="local" src="chrome://branding/content/about-logo.png" />',
+  '<img class="data" src="data:image/png;base64,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" />',
+  '<img class="remote" src="http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_765105_tooltip.png" />',
+  '<canvas class="canvas" width="600" height="600"></canvas>'
+].join("\n");
+
+const TEST_NODES = [
+  "img.local",
+  "img.data",
+  "img.remote",
+  ".canvas"
+];
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    contentDoc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,markup view tooltip test";
+}
+
+function createDocument() {
+  contentDoc.body.innerHTML = PAGE_CONTENT;
+
+  var target = TargetFactory.forTab(gBrowser.selectedTab);
+  gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+    inspector = toolbox.getCurrentPanel();
+    markup = inspector.markup;
+    startTests();
+  });
+}
+
+function startTests() {
+  // Draw something in the canvas :)
+  let doc = content.document;
+  let context = doc.querySelector(".canvas").getContext("2d");
+
+  context.beginPath();
+  context.moveTo(300, 0);
+  context.lineTo(600, 600);
+  context.lineTo(0, 600);
+  context.closePath();
+  context.fillStyle = "#ffc821";
+  context.fill();
+
+  // Actually start testing
+  inspector.selection.setNode(contentDoc.querySelector("img"));
+  inspector.once("inspector-updated", () => {
+    testImageTooltip(0);
+  });
+}
+
+function endTests() {
+  contentDoc = inspector = markup = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function testImageTooltip(index) {
+  if (index === TEST_NODES.length) {
+    return endTests();
+  }
+
+  let node = contentDoc.querySelector(TEST_NODES[index]);
+  ok(node, "We have the [" + TEST_NODES[index] + "] image node to test for tooltip");
+  let isImg = node.tagName.toLowerCase() === "img";
+
+  let container = getContainerForRawNode(markup, node);
+
+  let target = container.editor.tag;
+  if (isImg) {
+    target = container.editor.getAttributeElement("src");
+  }
+
+  assertTooltipShownOn(container.tooltip, target, () => {
+    let images = container.tooltip.panel.getElementsByTagName("image");
+    is(images.length, 1, "Tooltip for [" + TEST_NODES[index] + "] contains an image");
+    if (isImg) {
+      compareImageData(node, images[0].src);
+    }
+
+    container.tooltip.hide();
+
+    testImageTooltip(index + 1);
+  });
+}
+
+function compareImageData(img, imgData) {
+  let canvas = content.document.createElement("canvas");
+  canvas.width = img.naturalWidth;
+  canvas.height = img.naturalHeight;
+  let ctx = canvas.getContext("2d");
+  let data = "";
+  try {
+    ctx.drawImage(img, 0, 0);
+    data = canvas.toDataURL("image/png");
+  } catch (e) {}
+
+  is(data, imgData, "Tooltip image has the right content");
+}
+
+function assertTooltipShownOn(tooltip, element, cb) {
+  // If there is indeed a show-on-hover on element, the xul panel will be shown
+  tooltip.panel.addEventListener("popupshown", function shown() {
+    tooltip.panel.removeEventListener("popupshown", shown, true);
+
+    // Poll until the image gets loaded in the tooltip. This is required because
+    // markup containers only load images in their associated tooltips when
+    // the image data comes back from the server. However, this test is executed
+    // synchronously as soon as "inspector-updated" is fired, which is before
+    // the data for images is known.
+    let hasImage = () => tooltip.panel.getElementsByTagName("image").length;
+    let poll = setInterval(() => {
+      if (hasImage()) {
+        clearInterval(poll);
+        cb();
+      }
+    }, 200);
+  }, true);
+  tooltip._showOnHover(element);
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..699ef7940b03179b35c17be4782447f0f2cda58b
GIT binary patch
literal 1095
zc%17D@N?(olHy`uVBq!ia0vp^Vn8g;!3-oV%>36dFfd*T@Ck8cU}pFa2FMIJ8!nD4
za{0<-h{&r~uYe38F(Fn?Rv=f+iyg>S@nBW;U{m#ESMy|7c4t*_XH#`&SM^|5bYW6-
zWma|tihz{KJ25CYGbuVVDY>#LyRs=cvr5}DC^@r9+A>JlF(^2&NZByR*|W&oGb=i<
zN?J3>+A<59Fv!|4%h|FB7&8c&G6)$n$XT;UTQCV2Fo>Bjuxo-yaZ`2)6NXQpK8YAI
zh#N6*XtG_ncmasS^%;Sd@@Oy!848tFl#1#yC|WDZn#o?jasB44n*tgPys8ZR>I_Oc
zN_=Vzf;xgghMuF|>o>1i<QN3h7+7Q(fDFGFKQ?(*PDNH8B?cZ9PF*`)eFuGzv5E}*
z8hpHJJTe9{EV9gOa;&PBs*XO6PJT``4K++WOf4NPlcr93_WT*UFgv>}!;>dZgw=$B
z8aZT{7<d`DWEuFBc@r}dlQNUoBw3jSnXg{Gs$;3+AL*xOtv7kv<SEmqboO;J@PVR$
zg_q^!%a=gL$4?)D<iCIaAdv!Lq)bu<lGl!?3Id~fR!NXwFarZC8y7bZpOCPKxTKVp
zt(~i@r)O|TVp3XKIuK+4K_&=ffk1XnPF`MKJ`fZX78Mm07nhWjR#a40R#sJ2*VNY5
zH8eCfHa54kwY9Z(baZrfc6N1jclY%4^g%#>|AdK?z+f^EOqnue>a^)IX3Us1YxcZ_
zix)3hvSj&+HERzXJappB)o1U%sJaWy1g63cPZ!6Kid(i9L&KdC1zH~}zRBAzXD2Gm
zZM|{#2I~Xw@-~R>J{t8W(z5wi%-JgaXGv`$XKY&{I;;)PtvYc$owd5IC**JH{JEFc
z8Hd}wwz~VVFm?awk6k^f=G*238rHp3PoKJUzk{&#<I~TQOj-YmO*M8u^Ngdzv)=5X
zOmUiWhfU5@DTZebX<`k_E<JO6$m}_xSi<<QW~Ra%w`|rR9#i#`Z<ZXJz!Yff(*D!o
z?(DmHDP_N-Whc~oWOq9JK6r8e-!g^TxnW5S$GVr-Kks!oW0L!#!RGbV*Xy;j<tA+K
zn%&y4?ccAz&3ogoXE)x_eYIv1!`}~g)pyHZV-5;cXYF0WwCKr?r?+1|-QmKpQrU6I
zJ-vRmN3-(w%k-_kz0ATv>wef|r4mK&IO87=3T${zX>WSw+$nNu+PoajMHw%jrLFXE
zIQ1?w^wjrn?_{<6rf*rAo4Yhhb6>@p^S8gsu5Y{Bx5+0qYL?#SHObo#ettjwRKn$Z
yA?3xz->=NmjPRT@(@NpgjvX02Tmizr85MRG#+={OHvyP(7(8A5T-G@yGywn*f5k@t
--- a/browser/devtools/shared/Makefile.in
+++ b/browser/devtools/shared/Makefile.in
@@ -4,8 +4,9 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 include $(topsrcdir)/config/rules.mk
 
 libs::
 	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
 	$(NSINSTALL) $(srcdir)/widgets/*.jsm $(FINAL_TARGET)/modules/devtools
 	$(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/shared
+	$(NSINSTALL) $(srcdir)/widgets/*.js $(FINAL_TARGET)/modules/devtools/shared/widgets
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/widgets/Tooltip.js
@@ -0,0 +1,420 @@
+/* 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 {Cc, Cu, Ci} = require("chrome");
+const promise = require("sdk/core/promise");
+const IOService = Cc["@mozilla.org/network/io-service;1"]
+  .getService(Ci.nsIIOService);
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+const GRADIENT_RE = /\b(repeating-)?(linear|radial)-gradient\(((rgb|hsl)a?\(.+?\)|[^\)])+\)/gi;
+const BORDERCOLOR_RE = /^border-[-a-z]*color$/ig;
+const BORDER_RE = /^border(-(top|bottom|left|right))?$/ig;
+const BACKGROUND_IMAGE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
+
+/**
+ * Tooltip widget.
+ *
+ * This widget is intended at any tool that may need to show rich content in the
+ * form of floating panels.
+ * A common use case is image previewing in the CSS rule view, but more complex
+ * use cases may include color pickers, object inspection, etc...
+ *
+ * Tooltips are based on XUL (namely XUL arrow-type <panel>s), and therefore
+ * need a XUL Document to live in.
+ * This is pretty much the only requirement they have on their environment.
+ *
+ * The way to use a tooltip is simply by instantiating a tooltip yourself and
+ * attaching some content in it, or using one of the ready-made content types.
+ *
+ * A convenient `startTogglingOnHover` method may avoid having to register event
+ * handlers yourself if the tooltip has to be shown when hovering over a
+ * specific element or group of elements (which is usually the most common case)
+ */
+
+/**
+ * The low level structure of a tooltip is a XUL element (a <panel>, although
+ * <tooltip> is supported too, it won't have the nice arrow shape).
+ */
+let PanelFactory = {
+  get: function(doc, xulTag="panel") {
+    // Create the tooltip
+    let panel = doc.createElement(xulTag);
+    panel.setAttribute("hidden", true);
+
+    if (xulTag === "panel") {
+      // Prevent the click used to close the panel from being consumed
+      panel.setAttribute("consumeoutsideclicks", false);
+      panel.setAttribute("type", "arrow");
+      panel.setAttribute("level", "top");
+    }
+
+    panel.setAttribute("class", "devtools-tooltip devtools-tooltip-" + xulTag);
+    doc.querySelector("window").appendChild(panel);
+
+    return panel;
+  }
+};
+
+/**
+ * Tooltip class.
+ *
+ * Basic usage:
+ *   let t = new Tooltip(xulDoc);
+ *   t.content = someXulContent;
+ *   t.show();
+ *   t.hide();
+ *   t.destroy();
+ *
+ * Better usage:
+ *   let t = new Tooltip(xulDoc);
+ *   t.startTogglingOnHover(container, target => {
+ *     if (<condition based on target>) {
+ *       t.setImageContent("http://image.png");
+ *       return true;
+ *     }
+ *   });
+ *   t.destroy();
+ *
+ * @param XULDocument doc
+ *        The XUL document hosting this tooltip
+ */
+function Tooltip(doc) {
+  this.doc = doc;
+  this.panel = PanelFactory.get(doc);
+
+  // Used for namedTimeouts in the mouseover handling
+  this.uid = "tooltip-" + Date.now();
+}
+
+module.exports.Tooltip = Tooltip;
+
+Tooltip.prototype = {
+  /**
+   * Show the tooltip. It might be wise to append some content first if you
+   * don't want the tooltip to be empty. You may access the content of the
+   * tooltip by setting a XUL node to t.tooltip.content.
+   * @param {node} anchor
+   *        Which node should the tooltip be shown on
+   * @param {string} position
+   *        https://developer.mozilla.org/en-US/docs/XUL/PopupGuide/Positioning
+   *        Defaults to before_start
+   */
+  show: function(anchor, position="before_start") {
+    this.panel.hidden = false;
+    this.panel.openPopup(anchor, position);
+  },
+
+  /**
+   * Hide the tooltip
+   */
+  hide: function() {
+    this.panel.hidden = true;
+    this.panel.hidePopup();
+  },
+
+  /**
+   * Empty the tooltip's content
+   */
+  empty: function() {
+    while (this.panel.hasChildNodes()) {
+      this.panel.removeChild(this.panel.firstChild);
+    }
+  },
+
+  /**
+   * Get rid of references and event listeners
+   */
+  destroy: function () {
+    this.hide();
+    this.content = null;
+
+    this.doc = null;
+
+    this.panel.parentNode.removeChild(this.panel);
+    this.panel = null;
+
+    if (this._basedNode) {
+      this.stopTogglingOnHover();
+    }
+  },
+
+  /**
+   * Show/hide the tooltip when the mouse hovers over particular nodes.
+   *
+   * 2 Ways to make this work:
+   * - Provide a single node to attach the tooltip to, as the baseNode, and
+   *   omit the second targetNodeCb argument
+   * - Provide a baseNode that is the container of possibly numerous children
+   *   elements that may receive a tooltip. In this case, provide the second
+   *   targetNodeCb argument to decide wether or not a child should receive
+   *   a tooltip.
+   *
+   * This works by tracking mouse movements on a base container node (baseNode)
+   * and showing the tooltip when the mouse stops moving. The targetNodeCb
+   * callback is used to know whether or not the particular element being
+   * hovered over should indeed receive the tooltip. If you don't provide it
+   * it's equivalent to a function that always returns true.
+   *
+   * Note that if you call this function a second time, it will itself call
+   * stopTogglingOnHover before adding mouse tracking listeners again.
+   *
+   * @param {node} baseNode
+   *        The container for all target nodes
+   * @param {Function} targetNodeCb
+   *        A function that accepts a node argument and returns true or false
+   *        to signify if the tooltip should be shown on that node or not.
+   *        Additionally, the function receives a second argument which is the
+   *        tooltip instance itself, to be used to add/modify the content of the
+   *        tooltip if needed. If omitted, the tooltip will be shown everytime.
+   * @param {Number} showDelay
+   *        An optional delay that will be observed before showing the tooltip.
+   *        Defaults to 750ms
+   */
+  startTogglingOnHover: function(baseNode, targetNodeCb, showDelay = 750) {
+    if (this._basedNode) {
+      this.stopTogglingOnHover();
+    }
+
+    this._basedNode = baseNode;
+    this._showDelay = showDelay;
+    this._targetNodeCb = targetNodeCb || (() => true);
+
+    this._onBaseNodeMouseMove = this._onBaseNodeMouseMove.bind(this);
+    this._onBaseNodeMouseLeave = this._onBaseNodeMouseLeave.bind(this);
+
+    baseNode.addEventListener("mousemove", this._onBaseNodeMouseMove, false);
+    baseNode.addEventListener("mouseleave", this._onBaseNodeMouseLeave, false);
+  },
+
+  /**
+   * If the startTogglingOnHover function has been used previously, and you want
+   * to get rid of this behavior, then call this function to remove the mouse
+   * movement tracking
+   */
+  stopTogglingOnHover: function() {
+    clearNamedTimeout(this.uid);
+
+    this._basedNode.removeEventListener("mousemove",
+      this._onBaseNodeMouseMove, false);
+    this._basedNode.removeEventListener("mouseleave",
+      this._onBaseNodeMouseLeave, false);
+
+    this._basedNode = null;
+    this._targetNodeCb = null;
+    this._lastHovered = null;
+  },
+
+  _onBaseNodeMouseMove: function(event) {
+    if (event.target !== this._lastHovered) {
+      this.hide();
+      this._lastHovered = null;
+      setNamedTimeout(this.uid, this._showDelay, () => {
+        this._showOnHover(event.target);
+      });
+    }
+  },
+
+  _showOnHover: function(target) {
+    if (this._targetNodeCb && this._targetNodeCb(target, this)) {
+      this.show(target);
+      this._lastHovered = target;
+    }
+  },
+
+  _onBaseNodeMouseLeave: function() {
+    clearNamedTimeout(this.uid);
+    this._lastHovered = null;
+  },
+
+  /**
+   * Set the content of this tooltip. Will first empty the tooltip and then
+   * append the new content element.
+   * Consider using one of the set<type>Content() functions instead.
+   * @param {node} content
+   *        A node that can be appended in the tooltip XUL element
+   */
+  set content(content) {
+    this.empty();
+    if (content) {
+      this.panel.appendChild(content);
+    }
+  },
+
+  get content() {
+    return this.panel.firstChild;
+  },
+
+  /**
+   * Fill the tooltip with an image, displayed over a tiled background useful
+   * for transparent images.
+   * Also adds the image dimension as a label at the bottom.
+   */
+  setImageContent: function(imageUrl, maxDim=400) {
+    // Main container
+    let vbox = this.doc.createElement("vbox");
+    vbox.setAttribute("align", "center")
+
+    // Transparency tiles (image will go in there)
+    let tiles = createTransparencyTiles(this.doc, vbox);
+
+    // Temporary label during image load
+    let label = this.doc.createElement("label");
+    label.classList.add("devtools-tooltip-caption");
+    label.textContent = l10n.strings.GetStringFromName("previewTooltip.image.brokenImage");
+    vbox.appendChild(label);
+
+    // Display the image
+    let image = this.doc.createElement("image");
+    image.setAttribute("src", imageUrl);
+    if (maxDim) {
+      image.style.maxWidth = maxDim + "px";
+      image.style.maxHeight = maxDim + "px";
+    }
+    tiles.appendChild(image);
+
+    this.content = vbox;
+
+    // Load the image to get dimensions and display it when done
+    let imgObj = new this.doc.defaultView.Image();
+    imgObj.src = imageUrl;
+    imgObj.onload = () => {
+      imgObj.onload = null;
+
+      // Display dimensions
+      label.textContent = imgObj.naturalWidth + " x " + imgObj.naturalHeight;
+      if (imgObj.naturalWidth > maxDim ||
+        imgObj.naturalHeight > maxDim) {
+        label.textContent += " *";
+      }
+    }
+  },
+
+  /**
+   * Exactly the same as the `image` function but takes a css background image
+   * value instead : url(....)
+   */
+  setCssBackgroundImageContent: function(cssBackground, sheetHref, maxDim=400) {
+    let uri = getBackgroundImageUri(cssBackground, sheetHref);
+    if (uri) {
+      this.setImageContent(uri, maxDim);
+    }
+  },
+
+  setCssGradientContent: function(cssGradient) {
+    let tiles = createTransparencyTiles(this.doc);
+
+    let gradientBox = this.doc.createElement("box");
+    gradientBox.width = "100";
+    gradientBox.height = "100";
+    gradientBox.style.background = this.cssGradient;
+    gradientBox.style.borderRadius = "2px";
+    gradientBox.style.boxShadow = "inset 0 0 4px #333";
+
+    tiles.appendChild(gradientBox)
+
+    this.content = tiles;
+  },
+
+  _setSimpleCssPropertiesContent: function(properties, width, height) {
+    let tiles = createTransparencyTiles(this.doc);
+
+    let box = this.doc.createElement("box");
+    box.width = width + "";
+    box.height = height + "";
+    properties.forEach(({name, value}) => {
+      box.style[name] = value;
+    });
+    tiles.appendChild(box);
+
+    this.content = tiles;
+  },
+
+  setCssColorContent: function(cssColor) {
+    this._setSimpleCssPropertiesContent([
+      {name: "background", value: cssColor},
+      {name: "borderRadius", value: "2px"},
+      {name: "boxShadow", value: "inset 0 0 4px #333"},
+    ], 50, 50);
+  },
+
+  setCssBoxShadowContent: function(cssBoxShadow) {
+    this._setSimpleCssPropertiesContent([
+      {name: "background", value: "white"},
+      {name: "boxShadow", value: cssBoxShadow}
+    ], 80, 80);
+  },
+
+  setCssBorderContent: function(cssBorder) {
+    this._setSimpleCssPropertiesContent([
+      {name: "background", value: "white"},
+      {name: "border", value: cssBorder}
+    ], 80, 80);
+  }
+};
+
+/**
+ * Internal utility function that creates a tiled background useful for
+ * displaying semi-transparent images
+ */
+function createTransparencyTiles(doc, parentEl) {
+  let tiles = doc.createElement("box");
+  tiles.classList.add("devtools-tooltip-tiles");
+  if (parentEl) {
+    parentEl.appendChild(tiles);
+  }
+  return tiles;
+}
+
+/**
+ * Internal util, checks whether a css declaration is a gradient
+ */
+function isGradientRule(property, value) {
+  return (property === "background" || property === "background-image") &&
+    value.match(GRADIENT_RE);
+}
+
+/**
+ * Internal util, checks whether a css declaration is a color
+ */
+function isColorOnly(property, value) {
+  return property === "background-color" ||
+         property === "color" ||
+         property.match(BORDERCOLOR_RE);
+}
+
+/**
+ * Internal util, returns the background image uri if any
+ */
+function getBackgroundImageUri(value, sheetHref) {
+  let uriMatch = BACKGROUND_IMAGE_RE.exec(value);
+  let uri = null;
+
+  if (uriMatch && uriMatch[1]) {
+    uri = uriMatch[1];
+    if (sheetHref) {
+      let sheetUri = IOService.newURI(sheetHref, null, null);
+      uri = sheetUri.resolve(uri);
+    }
+  }
+
+  return uri;
+}
+
+/**
+ * L10N utility class
+ */
+function L10N() {}
+L10N.prototype = {};
+
+let l10n = new L10N();
+
+loader.lazyGetter(L10N.prototype, "strings", () => {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/devtools/inspector.properties");
+});
--- a/browser/devtools/styleinspector/computed-view.js
+++ b/browser/devtools/styleinspector/computed-view.js
@@ -6,18 +6,18 @@
 
 const {Cc, Ci, Cu} = require("chrome");
 
 let ToolDefinitions = require("main").Tools;
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
 let promise = require("sdk/core/promise");
 let {EventEmitter} = require("devtools/shared/event-emitter");
-
 const {OutputParser} = require("devtools/output-parser");
+const {Tooltip} = require("devtools/shared/widgets/Tooltip");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PluralForm.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 
@@ -164,16 +164,21 @@ function CssHtmlTree(aStyleInspector, aP
   this._handlePrefChange = this._handlePrefChange.bind(this);
   gDevTools.on("pref-changed", this._handlePrefChange);
 
   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._buildTooltipContent.bind(this));
+
   this._buildContextMenu();
   this.createStyleViews();
 }
 
 /**
  * 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.
@@ -486,16 +491,39 @@ CssHtmlTree.prototype = {
    */
   focusWindow: function(aEvent)
   {
     let win = this.styleDocument.defaultView;
     win.focus();
   },
 
   /**
+   * Verify that target is indeed a css value we want a tooltip on, and if yes
+   * prepare some content for the tooltip
+   */
+  _buildTooltipContent: function(target)
+  {
+    // If the hovered element is not a property view and is not a background
+    // image, then don't show a tooltip
+    let isPropertyValue = target.classList.contains("property-value");
+    if (!isPropertyValue) {
+      return false;
+    }
+    let propName = target.parentNode.querySelector(".property-name");
+    let isBackgroundImage = propName.textContent === "background-image";
+    if (!isBackgroundImage) {
+      return false;
+    }
+
+    // Fill some content
+    this.tooltip.setCssBackgroundImageContent(target.textContent);
+    return true;
+  },
+
+  /**
    * 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);
@@ -643,16 +671,19 @@ CssHtmlTree.prototype = {
       this.menuitemSelectAll = null;
 
       // Destroy the context menu.
       this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
       this._contextmenu.parentNode.removeChild(this._contextmenu);
       this._contextmenu = null;
     }
 
+    this.tooltip.stopTogglingOnHover(this.propertyContainer);
+    this.tooltip.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;
@@ -826,19 +857,18 @@ PropertyView.prototype = {
   /**
    * Build the markup for on computed style
    * @return Element
    */
   buildMain: function PropertyView_buildMain()
   {
     let doc = this.tree.styleDocument;
 
+    // Build the container element
     this.onMatchedToggle = this.onMatchedToggle.bind(this);
-
-    // Build the container element
     this.element = doc.createElementNS(HTML_NS, "div");
     this.element.setAttribute("class", this.propertyHeaderClassName);
     this.element.addEventListener("dblclick", this.onMatchedToggle, false);
 
     // Make it keyboard navigable
     this.element.setAttribute("tabindex", "0");
     this.onKeyDown = (aEvent) => {
       let keyEvent = Ci.nsIDOMKeyEvent;
@@ -853,16 +883,18 @@ PropertyView.prototype = {
     this.element.addEventListener("keydown", this.onKeyDown, false);
 
     // Build the twisty expand/collapse
     this.matchedExpander = doc.createElementNS(HTML_NS, "div");
     this.matchedExpander.className = "expander theme-twisty";
     this.matchedExpander.addEventListener("click", this.onMatchedToggle, false);
     this.element.appendChild(this.matchedExpander);
 
+    this.focusElement = () => this.element.focus();
+
     // Build the style name element
     this.nameNode = doc.createElementNS(HTML_NS, "div");
     this.nameNode.setAttribute("class", "property-name theme-fg-color5");
     // Reset its tabindex attribute otherwise, if an ellipsis is applied
     // it will be reachable via TABing
     this.nameNode.setAttribute("tabindex", "");
     this.nameNode.textContent = this.nameNode.title = this.name;
     // Make it hand over the focus to the container
@@ -1029,17 +1061,17 @@ PropertyView.prototype = {
     this.nameNode = null;
 
     this.valueNode.removeEventListener("click", this.onFocus, false);
     this.valueNode = null;
   }
 };
 
 /**
- * A container to view us easy access to display data from a CssRule
+ * A container to give us easy access to display data from a CssRule
  * @param CssHtmlTree aTree, the owning CssHtmlTree
  * @param aSelectorInfo
  */
 function SelectorView(aTree, aSelectorInfo)
 {
   this.tree = aTree;
   this.selectorInfo = aSelectorInfo;
   this._cacheStatusNames();
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -8,16 +8,17 @@
 
 const {Cc, Ci, Cu} = require("chrome");
 const promise = require("sdk/core/promise");
 
 let {CssLogic} = require("devtools/styleinspector/css-logic");
 let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
 let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
+let {Tooltip} = require("devtools/shared/widgets/Tooltip");
 
 const {OutputParser} = require("devtools/output-parser");
 
 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";
@@ -1024,28 +1025,30 @@ TextProperty.prototype = {
  *   Can mark a property disabled or enabled.
  */
 
 /**
  * CssRuleView is a view of the style rules and declarations that
  * apply to a given element.  After construction, the 'element'
  * property will be available with the user interface.
  *
+ * @param {Inspector} aInspector
  * @param {Document} aDoc
  *        The document that will contain the rule view.
  * @param {object} aStore
  *        The CSS rule view can use this object to store metadata
  *        that might outlast the rule view, particularly the current
  *        set of disabled properties.
  * @param {PageStyleFront} aPageStyle
  *        The PageStyleFront for communicating with the remote server.
  * @constructor
  */
-function CssRuleView(aDoc, aStore, aPageStyle)
+function CssRuleView(aInspector, aDoc, aStore, aPageStyle)
 {
+  this.inspector = aInspector;
   this.doc = aDoc;
   this.store = aStore || {};
   this.pageStyle = aPageStyle;
   this.element = this.doc.createElementNS(HTML_NS, "div");
   this.element.className = "ruleview devtools-monospace";
   this.element.flex = 1;
 
   this._outputParser = new OutputParser();
@@ -1062,16 +1065,19 @@ function CssRuleView(aDoc, aStore, aPage
 
   let options = {
     fixedWidth: true,
     autoSelect: true,
     theme: "auto"
   };
   this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
 
+  this.tooltip = new Tooltip(this.inspector.panelDoc);
+  this.tooltip.startTogglingOnHover(this.element, this._buildTooltipContent.bind(this));
+
   this._buildContextMenu();
   this._showEmpty();
 }
 
 exports.CssRuleView = CssRuleView;
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
@@ -1103,16 +1109,47 @@ CssRuleView.prototype = {
       popupset = doc.createElementNS(XUL_NS, "popupset");
       doc.documentElement.appendChild(popupset);
     }
 
     popupset.appendChild(this._contextmenu);
   },
 
   /**
+   * Verify that target is indeed a css value we want a tooltip on, and if yes
+   * prepare some content for the tooltip
+   */
+  _buildTooltipContent: function(target) {
+    let isValueWithImage = target.classList.contains("ruleview-propertyvalue") &&
+      target.querySelector(".theme-link");
+
+    let isImageHref = target.classList.contains("theme-link") &&
+      target.parentNode.classList.contains("ruleview-propertyvalue");
+    if (isImageHref) {
+      target = target.parentNode;
+    }
+
+    let isEditing = this.isEditing;
+
+    // If the inplace-editor is visible or if this is not a background image
+    // don't show the tooltip
+    if (this.isEditing || (!isImageHref && !isValueWithImage)) {
+      return false;
+    }
+
+    // Retrieve the TextProperty for the hovered element
+    let property = target.textProperty;
+    let href = property.rule.domRule.href;
+
+    // Fill some content
+    this.tooltip.setCssBackgroundImageContent(property.value, href);
+    return true;
+  },
+
+  /**
    * 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();
@@ -1235,16 +1272,19 @@ 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.tooltip.stopTogglingOnHover(this.element);
+    this.tooltip.destroy();
+
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
     if (this.elementStyle) {
       this.elementStyle.destroy();
     }
 
@@ -1302,17 +1342,16 @@ CssRuleView.prototype = {
   _populate: function() {
     let elementStyle = this._elementStyle;
     return this._elementStyle.populate().then(() => {
       if (this._elementStyle != elementStyle) {
         return promise.reject("element changed");
       }
       this._createEditors();
 
-
       // Notify anyone that cares that we refreshed.
       var evt = this.doc.createEvent("Events");
       evt.initEvent("CssRuleViewRefreshed", true, false);
       this.element.dispatchEvent(evt);
       return undefined;
     }).then(null, promiseWarn);
   },
 
@@ -1848,16 +1887,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: "0",
     });
 
+    // Storing the TextProperty on the valuespan for easy access
+    // (for instance by the tooltip)
+    this.valueSpan.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, ";");
 
@@ -1966,17 +2009,16 @@ TextPropertyEditor.prototype = {
       let a = createChild(this.valueSpan, "a",  {
         target: "_blank",
         class: "theme-link",
         textContent: resourceURI,
         href: this.resolveURI(resourceURI)
       });
 
       a.addEventListener("click", (aEvent) => {
-
         // Clicks within the link shouldn't trigger editing.
         aEvent.stopPropagation();
         aEvent.preventDefault();
 
         this.browserWindow.openUILinkIn(aEvent.target.href, "tab");
 
       }, false);
 
@@ -2128,16 +2170,17 @@ TextPropertyEditor.prototype = {
   /**
    * Remove property from style and the editors from DOM.
    * Begin editing next available property.
    */
   remove: function TextPropertyEditor_remove()
   {
     this.element.parentNode.removeChild(this.element);
     this.ruleEditor.rule.editClosestTextProperty(this.prop);
+    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.
    *
    * @param {string} aValue
--- a/browser/devtools/styleinspector/style-inspector.js
+++ b/browser/devtools/styleinspector/style-inspector.js
@@ -20,17 +20,17 @@ loader.lazyGetter(this, "_strings", () =
 // registers inspector tools.
 
 function RuleViewTool(aInspector, aWindow, aIFrame)
 {
   this.inspector = aInspector;
   this.doc = aWindow.document;
   this.outerIFrame = aIFrame;
 
-  this.view = new RuleView.CssRuleView(this.doc);
+  this.view = new RuleView.CssRuleView(aInspector, this.doc);
   this.doc.documentElement.appendChild(this.view.element);
 
   this._changeHandler = () => {
     this.inspector.markDirty();
   };
 
   this.view.element.addEventListener("CssRuleViewChanged", this._changeHandler);
 
--- a/browser/devtools/styleinspector/test/Makefile.in
+++ b/browser/devtools/styleinspector/test/Makefile.in
@@ -33,16 +33,17 @@ MOCHITEST_BROWSER_FILES = \
   browser_bug893965_css_property_completion_new_property.js \
   browser_bug893965_css_property_completion_existing_property.js \
   browser_bug894376_css_value_completion_new_property_value_pair.js \
   browser_bug894376_css_value_completion_existing_property_value_pair.js \
   browser_ruleview_bug_902966_revert_value_on_ESC.js \
   browser_ruleview_pseudoelement.js \
   browser_computedview_bug835808_keyboard_nav.js \
   browser_bug913014_matched_expand.js \
+  browser_bug765105_background_image_tooltip.js \
   head.js \
   $(NULL)
 
 MOCHITEST_BROWSER_FILES += \
   browser_bug683672.html \
   browser_bug705707_is_content_stylesheet.html \
   browser_bug705707_is_content_stylesheet_imported.css \
   browser_bug705707_is_content_stylesheet_imported2.css \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_bug765105_background_image_tooltip.js
@@ -0,0 +1,162 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let contentDoc;
+let inspector;
+let ruleView;
+let computedView;
+
+const PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  body {',
+  '    padding: 1em;',
+  '    background-image: url(data:image/png;base64,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);',
+  '    background-repeat: repeat-y;',
+  '    background-position: right top;',
+  '  }',
+  '  .test-element {',
+  '    font-family: verdana;',
+  '    color: #333;',
+  '    background: url(chrome://global/skin/icons/warning-64.png) no-repeat left center;',
+  '    padding-left: 70px;',
+  '  }',
+  '</style>',
+  '<div class="test-element">test element</div>',
+  '<div class="test-element-2">test element 2</div>'
+].join("\n");
+
+function test() {
+  waitForExplicitFinish();
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    contentDoc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,rule view tooltip test";
+}
+
+function createDocument() {
+  contentDoc.body.innerHTML = PAGE_CONTENT;
+
+  openRuleView((aInspector, aRuleView) => {
+    inspector = aInspector;
+    ruleView = aRuleView;
+    startTests();
+  });
+}
+
+function startTests() {
+  // let testElement = contentDoc.querySelector(".test-element");
+
+  inspector.selection.setNode(contentDoc.body);
+  inspector.once("inspector-updated", testBodyRuleView);
+}
+
+function endTests() {
+  contentDoc = inspector = ruleView = computedView = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function assertTooltipShownOn(tooltip, element, cb) {
+  // If there is indeed a show-on-hover on element, the xul panel will be shown
+  tooltip.panel.addEventListener("popupshown", function shown() {
+    tooltip.panel.removeEventListener("popupshown", shown, true);
+    cb();
+  }, true);
+  tooltip._showOnHover(element);
+}
+
+function testBodyRuleView() {
+  info("Testing tooltips in the rule view");
+
+  let panel = ruleView.tooltip.panel;
+
+  // Check that the rule view has a tooltip and that a XUL panel has been created
+  ok(ruleView.tooltip, "Tooltip instance exists");
+  ok(panel, "XUL panel exists");
+
+  // Get the background-image property inside the rule view
+  let {nameSpan, valueSpan} = getRuleViewProperty("background-image");
+  // And verify that the tooltip gets shown on this property
+  assertTooltipShownOn(ruleView.tooltip, valueSpan, () => {
+    let images = panel.getElementsByTagName("image");
+    is(images.length, 1, "Tooltip contains an image");
+    ok(images[0].src.indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1, "The image URL seems fine");
+
+    ruleView.tooltip.hide();
+
+    inspector.selection.setNode(contentDoc.querySelector(".test-element"));
+    inspector.once("inspector-updated", testDivRuleView);
+  });
+}
+
+function testDivRuleView() {
+  let panel = ruleView.tooltip.panel;
+
+  // Get the background property inside the rule view
+  let {nameSpan, valueSpan} = getRuleViewProperty("background");
+  let uriSpan = valueSpan.querySelector(".theme-link");
+
+  // And verify that the tooltip gets shown on this property
+  assertTooltipShownOn(ruleView.tooltip, uriSpan, () => {
+    let images = panel.getElementsByTagName("image");
+    is(images.length, 1, "Tooltip contains an image");
+    ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
+
+    ruleView.tooltip.hide();
+
+    testComputedView();
+  });
+}
+
+function testComputedView() {
+  info("Testing tooltips in the computed view");
+
+  inspector.sidebar.select("computedview");
+  computedView = inspector.sidebar.getWindowForTab("computedview").computedview.view;
+  let doc = computedView.styleDocument;
+
+  let panel = computedView.tooltip.panel;
+  let {nameSpan, valueSpan} = getComputedViewProperty("background-image");
+
+  assertTooltipShownOn(computedView.tooltip, valueSpan, () => {
+    let images = panel.getElementsByTagName("image");
+    is(images.length, 1, "Tooltip contains an image");
+    ok(images[0].src === "chrome://global/skin/icons/warning-64.png");
+
+    computedView.tooltip.hide();
+
+    endTests();
+  });
+}
+
+function getRuleViewProperty(name) {
+  let prop = null;
+  [].forEach.call(ruleView.doc.querySelectorAll(".ruleview-property"), property => {
+    let nameSpan = property.querySelector(".ruleview-propertyname");
+    let valueSpan = property.querySelector(".ruleview-propertyvalue");
+
+    if (nameSpan.textContent === name) {
+      prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+    }
+  });
+  return prop;
+}
+
+function getComputedViewProperty(name) {
+  let prop = null;
+  [].forEach.call(computedView.styleDocument.querySelectorAll(".property-view"), property => {
+    let nameSpan = property.querySelector(".property-name");
+    let valueSpan = property.querySelector(".property-value");
+
+    if (nameSpan.textContent === name) {
+      prop = {nameSpan: nameSpan, valueSpan: valueSpan};
+    }
+  });
+  return prop;
+}
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -26,21 +26,23 @@ breadcrumbs.siblings=Siblings
 # the user switch to the inspector when the debugger is paused.
 debuggerPausedWarning.message=Debugger is paused. Some features like mouse selection will not work.
 
 # LOCALIZATION NOTE (nodeMenu.tooltiptext)
 # This menu appears in the Infobar (on top of the highlighted node) once
 # the node is selected.
 nodeMenu.tooltiptext=Node operations
 
-
 # LOCALIZATION NOTE (inspector.*)
 # Used for the menuitem in the tool menu
 inspector.label=Inspector
 inspector.commandkey=C
 inspector.accesskey=I
 
 # LOCALIZATION NOTE (markupView.more.*)
 # When there are too many nodes to load at once, we will offer to
 # show all the nodes.
 markupView.more.showing=Some nodes were hidden.
 markupView.more.showAll=Show All %S Nodes
 inspector.tooltip=DOM and Style Inspector
+
+#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded
+previewTooltip.image.brokenImage=Could not load the image
--- a/browser/themes/shared/devtools/common.inc.css
+++ b/browser/themes/shared/devtools/common.inc.css
@@ -106,8 +106,30 @@
     cursor: n-resize;
   }
 
   .devtools-responsive-container > .devtools-sidebar-tabs {
     min-height: 35vh;
     max-height: 75vh;
   }
 }
+
+/* Tooltip widget (see browser/devtools/shared/widgets/Tooltip.js) */
+
+.devtools-tooltip.devtools-tooltip-tooltip {
+  /* If the tooltip uses a <tooltip> XUL element */
+  -moz-appearance: none;
+  padding: 4px;
+  background: #eee;
+  border-radius: 3px;
+}
+.devtools-tooltip.devtools-tooltip-panel .panel-arrowcontent {
+  /* If the tooltip uses a <panel> XUL element instead */
+  padding: 4px;
+}
+
+.devtools-tooltip-tiles {
+  background-color: #eee;
+  background-image: linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc),
+    linear-gradient(45deg, #ccc 25%, transparent 25%, transparent 75%, #ccc 75%, #ccc);
+  background-size: 20px 20px;
+  background-position: 0 0, 10px 10px;
+}
--- a/toolkit/devtools/server/actors/inspector.js
+++ b/toolkit/devtools/server/actors/inspector.js
@@ -244,16 +244,57 @@ var NodeActor = protocol.ActorClass({
   setNodeValue: method(function(value) {
     this.rawNode.nodeValue = value;
   }, {
     request: { value: Arg(0) },
     response: {}
   }),
 
   /**
+   * Get the node's image data if any (for canvas and img nodes).
+   * Returns a LongStringActor with the image or canvas' image data as png
+   * a data:image/png;base64,.... string
+   * A null return value means the node isn't an image
+   * An empty string return value means the node is an image but image data
+   * could not be retrieved (missing/broken image).
+   */
+  getImageData: method(function() {
+    let isImg = this.rawNode.tagName.toLowerCase() === "img";
+    let isCanvas = this.rawNode.tagName.toLowerCase() === "canvas";
+
+    if (!isImg && !isCanvas) {
+      return null;
+    }
+
+    let imageData;
+    if (isImg) {
+      let canvas = this.rawNode.ownerDocument.createElement("canvas");
+      canvas.width = this.rawNode.naturalWidth;
+      canvas.height = this.rawNode.naturalHeight;
+      let ctx = canvas.getContext("2d");
+      try {
+        // This will fail if the image is missing
+        ctx.drawImage(this.rawNode, 0, 0);
+        imageData = canvas.toDataURL("image/png");
+      } catch (e) {
+        imageData = "";
+      }
+    } else if (isCanvas) {
+      imageData = this.rawNode.toDataURL("image/png");
+    }
+
+    return LongStringActor(this.conn, imageData);
+  }, {
+    request: {},
+    response: {
+      data: RetVal("nullable:longstring")
+    }
+  }),
+
+  /**
    * Modify a node's attributes.  Passed an array of modifications
    * similar in format to "attributes" mutations.
    * {
    *   attributeName: <string>
    *   attributeNamespace: <optional string>
    *   newValue: <optional string> - If null or undefined, the attribute
    *     will be removed.
    * }
@@ -278,18 +319,17 @@ var NodeActor = protocol.ActorClass({
         }
       }
     }
   }, {
     request: {
       modifications: Arg(0, "array:json")
     },
     response: {}
-  }),
-
+  })
 });
 
 /**
  * Client side of the node actor.
  *
  * Node fronts are strored in a tree that mirrors the DOM tree on the
  * server, but with a few key differences:
  *  - Not all children will be necessary loaded for each node.