Merge f-t to m-c
authorPhil Ringnalda <philringnalda@gmail.com>
Fri, 25 Oct 2013 19:24:55 -0700
changeset 167050 0280ef5d685c3a8f72a80ed6df62aad8b9cc709a
parent 167041 ac49a4f5bf04831bd94a92ade36105f6f7e1d884 (current diff)
parent 167049 954741a373f33169f3253e0477ae2a5ddd825284 (diff)
child 167059 ef3f5669b53e4b3e85ca33622108ede08d5a608a
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)
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
Merge f-t to m-c
toolkit/components/downloads/test/unit/bug_401582_downloads.sqlite
toolkit/components/downloads/test/unit/bug_409179_downloads.sqlite
toolkit/components/downloads/test/unit/empty_downloads.rdf
toolkit/components/downloads/test/unit/test_bug_401582.js
toolkit/components/downloads/test/unit/test_bug_409179.js
toolkit/components/downloads/test/unit/test_old_download_files_removed.js
toolkit/forgetaboutsite/test/unit/downloads.empty.sqlite
toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain_activeDownloads.js
--- a/browser/base/content/test/general/browser_bug822367.js
+++ b/browser/base/content/test/general/browser_bug822367.js
@@ -174,23 +174,8 @@ function MixedTest6B() {
 function MixedTest6C() {
   gTestBrowser.removeEventListener("load", MixedTest6C, true);
   waitForCondition(function() content.document.getElementById('f1').contentDocument.getElementById('p1').innerHTML == "hello", MixedTest6D, "Waited too long for mixed script to run in Test 6");
 }
 function MixedTest6D() {
   ok(content.document.getElementById('f1').contentDocument.getElementById('p1').innerHTML == "hello","Mixed script didn't load in Test 6");
   MixedTestsCompleted();
 }
-
-function waitForCondition(condition, nextTest, errorMsg) {
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
-    }
-    if (condition()) {
-      moveOn();
-    }
-    tries++;
-  }, 100);
-  var moveOn = function() { clearInterval(interval); nextTest(); };
-}
--- a/browser/base/content/test/general/browser_bug902156.js
+++ b/browser/base/content/test/general/browser_bug902156.js
@@ -36,36 +36,16 @@ registerCleanupFunction(function() {
   Services.prefs.setBoolPref(PREF_ACTIVE, origBlockActive);
 });
 
 function cleanUpAfterTests() {
   gBrowser.removeCurrentTab();
   window.focus();
   finish();
 }
-/*
- * Whenever we disable the Mixed Content Blocker of the page
- * we have to make sure that our condition is properly loaded.
- */
-function waitForCondition(condition, nextTest, errorMsg) {
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
-    }
-    if (condition()) {
-      moveOn();
-    }
-    tries++;
-  }, 100);
-  var moveOn = function() {
-    clearInterval(interval); nextTest();
-  };
-}
 
 //------------------------ Test 1 ------------------------------
 
 function test1A() {
   // Removing EventListener because we have to register a new
   // one once the page is loaded with mixed content blocker disabled
   gTestBrowser.removeEventListener("load", test1A, true);
   gTestBrowser.addEventListener("load", test1B, true);
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -84,17 +84,24 @@ function closeToolbarCustomizationUI(aCa
 
 function waitForCondition(condition, nextTest, errorMsg) {
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= 30) {
       ok(false, errorMsg);
       moveOn();
     }
-    if (condition()) {
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
       moveOn();
     }
     tries++;
   }, 100);
   var moveOn = function() { clearInterval(interval); nextTest(); };
 }
 
 function getTestPlugin(aName) {
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -13,17 +13,24 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 function waitForCondition(condition, nextTest, errorMsg) {
   var tries = 0;
   var interval = setInterval(function() {
     if (tries >= 30) {
       ok(false, errorMsg);
       moveOn();
     }
-    if (condition()) {
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
       moveOn();
     }
     tries++;
   }, 100);
   var moveOn = function() { clearInterval(interval); nextTest(); };
 }
 
 // Check that a specified (string) URL hasn't been "remembered" (ie, is not
--- a/browser/devtools/app-manager/content/connection-footer.xhtml
+++ b/browser/devtools/app-manager/content/connection-footer.xhtml
@@ -97,16 +97,17 @@
           <div class="connected-indicator"></div>
           <div class="banner-box">
             <div class="banner-content">
               <div class="no-simulator">
                 <span>&connection.noSimulatorInstalled;</span>
                 <button class="action-primary" onclick="UI.installSimulator()" title="&connection.installOneSimulatorTooltip;">&connection.installOneSimulator;</button>
               </div>
               <div class="found-simulator">
+                <span>&connection.startRegisteredSimulator;</span>
                 <span template-loop='{"arrayPath":"simulators.versions","childSelector":"#simulator-item-template"}'></span>
                 <button class="action-primary" onclick="UI.installSimulator()" title="&connection.installAnotherSimulatorTooltip;">&connection.installAnotherSimulator;</button>
               </div>
               <button class="action-cancel" onclick="UI.cancelShowSimulatorList()" title="&connection.cancelShowSimulatorTooltip;">&connection.cancel;</button>
             </div>
           </div>
         </div>
 
@@ -119,17 +120,17 @@
         </div>
 
       </div>
     </div>
   </body>
 
   <template id="simulator-item-template">
   <span>
-    <button class="simulator-item" onclick="UI.startSimulator(this.dataset.version)" template='{"type":"attribute","path":"version","name":"data-version"}' title="&connection.startSimulatorTooltip;">
+    <button class="simulator-item action-primary" onclick="UI.startSimulator(this.dataset.version)" template='{"type":"attribute","path":"version","name":"data-version"}' title="&connection.startSimulatorTooltip;">
       <span template='{"type":"textContent", "path":"version"}'></span>
     </button>
   </span>
   </template>
 
   <template id="adb-devices-template">
   <span>
     <button class="adb-device action-primary" onclick="UI.connectToAdbDevice(this.dataset.name)" template='{"type":"attribute","path":"name","name":"data-name"}'>
--- 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/app-manager.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
@@ -52,16 +52,17 @@
 <!ENTITY connection.cancelConnectTooltip "Cancel the connection in progress.">
 <!ENTITY connection.cancelShowSimulatorTooltip "Exit the Simulator connection mode and return to the initial prompt.">
 <!ENTITY connection.or "or">
 <!ENTITY connection.noSimulatorInstalled "No simulator installed.">
 <!ENTITY connection.installOneSimulator "Install Simulator">
 <!ENTITY connection.installOneSimulatorTooltip "Install a version of the Simulator by downloading the relevant add-on.">
 <!ENTITY connection.installAnotherSimulator "Add">
 <!ENTITY connection.installAnotherSimulatorTooltip "Install an additional version of the Simulator by downloading the relevant add-on.">
+<!ENTITY connection.startRegisteredSimulator "Start:">
 
 <!ENTITY projects.localApps "Local Apps">
 <!ENTITY projects.addApp "Add">
 <!ENTITY projects.addPackaged "Add Packaged App">
 <!ENTITY projects.addPackagedTooltip "Add a new packaged app (a directory) from your computer.">
 <!ENTITY projects.addHosted "Add Hosted App">
 <!ENTITY projects.addHostedTooltip "Add a new hosted app (link to a manifest.webapp file) from a remote website.">
 <!ENTITY projects.title "Local Apps">
--- 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/linux/devtools/debugger.css
+++ b/browser/themes/linux/devtools/debugger.css
@@ -12,16 +12,23 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
+/* Sources toolbar */
+
+#sources-toolbar {
+  border: none; /* Remove the devtools-toolbar's black bottom border. */
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #pretty-print {
   min-width: 0;
   font-weight: bold;
 }
 
 #black-box {
   list-style-image: url(debugger-blackbox.png);
   -moz-image-region: rect(0px,16px,16px,0px);
@@ -430,8 +437,13 @@
 
 #body[layout=vertical] .side-menu-widget-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
+
+#body[layout=vertical] .side-menu-widget-group,
+#body[layout=vertical] .side-menu-widget-item {
+  -moz-margin-end: 0;
+}
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -274,24 +274,38 @@
   color: #fff;
 }
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
+/* SideMenuWidget container */
+
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+  /* To allow visibility of the dark margin shadow. */
+  -moz-margin-end: 1px;
+}
+
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
+  /* To compensate for the arrow image's dark margin. */
+  -moz-margin-end: -1px;
+}
+
+/* SideMenuWidget groups */
+
 .side-menu-widget-group-title {
   padding: 4px;
 }
 
 .side-menu-widget-group-title[theme="dark"] {
   background-image: linear-gradient(#1f3e4f, #1b3243);
   text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -303,16 +317,18 @@
   background-image: linear-gradient(#fff, #eee);
 }
 
 .side-menu-widget-group-checkbox {
   margin: 0;
   padding: 0;
 }
 
+/* SideMenuWidget items */
+
 .side-menu-widget-item[theme="dark"] {
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
@@ -338,25 +354,27 @@
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
-  background-image: url(itemArrow-ltr.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(rtl) {
-  background-image: url(itemArrow-rtl.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-rtl.png), linear-gradient(to right, #222426, #222426);
   background-position: center left, top left;
 }
 
+/* SideMenuWidget items contents */
+
 .side-menu-widget-item-label {
   padding: 4px 0px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
   width: 8px;
 }
@@ -380,16 +398,18 @@
   border-top-left-radius: 4px;
 }
 
 .side-menu-widget-item-other > label {
   color: #f5f7fa;
   text-shadow: 0 1px 1px #111;
 }
 
+/* SideMenuWidget misc */
+
 .side-menu-widget-empty-notice-container {
   padding: 12px;
 }
 
 .side-menu-widget-empty-notice-container[theme="dark"] {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   font-weight: 600;
   color: #fff;
--- a/browser/themes/osx/devtools/debugger.css
+++ b/browser/themes/osx/devtools/debugger.css
@@ -14,16 +14,23 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
+/* Sources toolbar */
+
+#sources-toolbar {
+  border: none; /* Remove the devtools-toolbar's black bottom border. */
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #pretty-print {
   min-width: 0;
   font-weight: bold;
 }
 
 #black-box {
   list-style-image: url(debugger-blackbox.png);
   -moz-image-region: rect(0px,16px,16px,0px);
@@ -432,8 +439,13 @@
 
 #body[layout=vertical] .side-menu-widget-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
+
+#body[layout=vertical] .side-menu-widget-group,
+#body[layout=vertical] .side-menu-widget-item {
+  -moz-margin-end: 0;
+}
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -274,24 +274,38 @@
   color: #fff;
 }
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
+/* SideMenuWidget container */
+
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+  /* To allow visibility of the dark margin shadow. */
+  -moz-margin-end: 1px;
+}
+
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
+  /* To compensate for the arrow image's dark margin. */
+  -moz-margin-end: -1px;
+}
+
+/* SideMenuWidget groups */
+
 .side-menu-widget-group-title {
   padding: 4px;
 }
 
 .side-menu-widget-group-title[theme="dark"] {
   background-image: linear-gradient(#1f3e4f, #1b3243);
   text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -303,16 +317,18 @@
   background-image: linear-gradient(#fff, #eee);
 }
 
 .side-menu-widget-group-checkbox {
   margin: 0;
   padding: 0;
 }
 
+/* SideMenuWidget items */
+
 .side-menu-widget-item[theme="dark"] {
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
@@ -338,25 +354,27 @@
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
-  background-image: url(itemArrow-ltr.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(rtl) {
-  background-image: url(itemArrow-rtl.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-rtl.png), linear-gradient(to right, #222426, #222426);
   background-position: center left, top left;
 }
 
+/* SideMenuWidget items contents */
+
 .side-menu-widget-item-label {
   padding: 4px 0px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
   width: 8px;
 }
@@ -380,16 +398,18 @@
   border-top-left-radius: 4px;
 }
 
 .side-menu-widget-item-other > label {
   color: #f5f7fa;
   text-shadow: 0 1px 1px #111;
 }
 
+/* SideMenuWidget misc */
+
 .side-menu-widget-empty-notice-container {
   padding: 12px;
 }
 
 .side-menu-widget-empty-notice-container[theme="dark"] {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   font-weight: 600;
   color: #fff;
--- 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/browser/themes/windows/devtools/debugger.css
+++ b/browser/themes/windows/devtools/debugger.css
@@ -12,16 +12,23 @@
 #sources-pane {
   min-width: 50px;
 }
 
 #sources-and-editor-splitter {
   -moz-border-start-color: transparent;
 }
 
+/* Sources toolbar */
+
+#sources-toolbar {
+  border: none; /* Remove the devtools-toolbar's black bottom border. */
+  -moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
+}
+
 #pretty-print {
   min-width: 0;
   font-weight: bold;
 }
 
 #black-box {
   list-style-image: url(debugger-blackbox.png);
   -moz-image-region: rect(0px,16px,16px,0px);
@@ -435,8 +442,13 @@
 
 #body[layout=vertical] .side-menu-widget-container {
   box-shadow: none !important;
 }
 
 #body[layout=vertical] .side-menu-widget-item-arrow {
   background-image: none !important;
 }
+
+#body[layout=vertical] .side-menu-widget-group,
+#body[layout=vertical] .side-menu-widget-item {
+  -moz-margin-end: 0;
+}
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -278,24 +278,38 @@
   color: #fff;
 }
 
 .side-menu-widget-container[theme="light"] {
   background: #fff;
   color: #000;
 }
 
+/* SideMenuWidget container */
+
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
   box-shadow: inset -1px 0 0 #222426;
 }
 
 .side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
   box-shadow: inset 1px 0 0 #222426;
 }
 
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
+  /* To allow visibility of the dark margin shadow. */
+  -moz-margin-end: 1px;
+}
+
+.side-menu-widget-container[with-arrows=true] .side-menu-widget-item {
+  /* To compensate for the arrow image's dark margin. */
+  -moz-margin-end: -1px;
+}
+
+/* SideMenuWidget groups */
+
 .side-menu-widget-group-title {
   padding: 4px;
 }
 
 .side-menu-widget-group-title[theme="dark"] {
   background-image: linear-gradient(#1f3e4f, #1b3243);
   text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.05) inset,
@@ -307,16 +321,18 @@
   background-image: linear-gradient(#fff, #eee);
 }
 
 .side-menu-widget-group-checkbox {
   margin: 0;
   padding: 0;
 }
 
+/* SideMenuWidget items */
+
 .side-menu-widget-item[theme="dark"] {
   border-top: 1px solid hsla(210,8%,5%,.25);
   border-bottom: 1px solid hsla(210,16%,76%,.1);
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
 .side-menu-widget-item[theme="light"] {
@@ -342,25 +358,27 @@
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow {
   background-size: auto, 1px 100%;
   background-repeat: no-repeat;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(ltr) {
-  background-image: url(itemArrow-ltr.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-ltr.png), linear-gradient(to right, #222426, #222426);
   background-position: center right, top right;
 }
 
 .side-menu-widget-item.selected > .side-menu-widget-item-arrow:-moz-locale-dir(rtl) {
-  background-image: url(itemArrow-rtl.png), linear-gradient(to right, black, black);
+  background-image: url(itemArrow-rtl.png), linear-gradient(to right, #222426, #222426);
   background-position: center left, top left;
 }
 
+/* SideMenuWidget items contents */
+
 .side-menu-widget-item-label {
   padding: 4px 0px;
 }
 
 .side-menu-widget-item-arrow {
   -moz-margin-start: -8px;
   width: 8px;
 }
@@ -383,16 +401,18 @@
 .side-menu-widget-item-other:first-of-type {
   border-top-left-radius: 4px;
 }
 
 .side-menu-widget-item-other > label {
   color: #f5f7fa;
 }
 
+/* SideMenuWidget misc */
+
 .side-menu-widget-empty-notice-container {
   padding: 12px;
 }
 
 .side-menu-widget-empty-notice-container[theme="dark"] {
   background: url(background-noise-toolbar.png), hsl(208,11%,27%);
   font-weight: 600;
   color: #fff;
--- a/mobile/android/base/favicons/cache/FaviconCache.java
+++ b/mobile/android/base/favicons/cache/FaviconCache.java
@@ -626,15 +626,16 @@ public class FaviconCache {
 
     /**
      * Purge all elements from the FaviconCache. Handy if you want to reclaim some memory.
      */
     public void evictAll() {
         startWrite();
 
         try {
+            mCurrentSize.set(0);
             mBackingMap.clear();
             mOrdering.clear();
         } finally {
             finishWrite();
         }
     }
 }
deleted file mode 100644
index a81e9d65a873f270189d2813225feac925b5f450..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 324e4aaba1de1e8e5fa75dbbf8702e4c7de1e035..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/toolkit/components/downloads/test/unit/head_download_manager.js
+++ b/toolkit/components/downloads/test/unit/head_download_manager.js
@@ -51,35 +51,16 @@ var provider = {
         iid.equals(Ci.nsISupports)) {
       return this;
     }
     throw Cr.NS_ERROR_NO_INTERFACE;
   }
 };
 dirSvc.QueryInterface(Ci.nsIDirectoryService).registerProvider(provider);
 
-/**
- * Imports a download test file to use.  Works with rdf and sqlite files.
- *
- * @param aFName
- *        The name of the file to import.  This file should be located in the
- *        same directory as this file.
- */
-function importDownloadsFile(aFName)
-{
-  var file = do_get_file(aFName);
-  var newFile = dirSvc.get("ProfD", Ci.nsIFile);
-  if (/\.rdf$/i.test(aFName))
-    file.copyTo(newFile, "downloads.rdf");
-  else if (/\.sqlite$/i.test(aFName))
-    file.copyTo(newFile, "downloads.sqlite");
-  else
-    do_throw("Unexpected filename!");
-}
-
 var gDownloadCount = 0;
 /**
  * Adds a download to the DM, and starts it.
  * @param server: a HttpServer used to serve the sourceURI
  * @param aParams (optional): an optional object which contains the function
  *                            parameters:
  *                              resultFileName: leaf node for the target file
  *                              targetFile: nsIFile for the target (overrides resultFileName)
deleted file mode 100644
--- a/toolkit/components/downloads/test/unit/test_bug_401582.js
+++ /dev/null
@@ -1,36 +0,0 @@
-/* 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/. */
-
-// This tests that downloads in the scanning state are set to a completed state
-// upon service initialization.
-
-importDownloadsFile("bug_401582_downloads.sqlite");
-
-const nsIDownloadManager = Ci.nsIDownloadManager;
-const dm = Cc["@mozilla.org/download-manager;1"].getService(nsIDownloadManager);
-
-function test_noScanningDownloads()
-{
-  var stmt = dm.DBConnection.createStatement(
-    "SELECT * " +
-    "FROM moz_downloads " +
-    "WHERE state = ?1");
-  stmt.bindByIndex(0, nsIDownloadManager.DOWNLOAD_SCANNING);
-
-  do_check_false(stmt.executeStep());
-  stmt.reset();
-  stmt.finalize();
-}
-
-var tests = [test_noScanningDownloads];
-
-function run_test()
-{
-  if (oldDownloadManagerDisabled()) {
-    return;
-  }
-
-  for (var i = 0; i < tests.length; i++)
-    tests[i]();
-}
deleted file mode 100644
--- a/toolkit/components/downloads/test/unit/test_bug_409179.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* 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/. */
-
-// This file ensures that the download manager service can be instantiated with
-// a certain downloads.sqlite file that had incorrect data.
-
-importDownloadsFile("bug_409179_downloads.sqlite");
-
-function run_test()
-{
-  if (oldDownloadManagerDisabled()) {
-    return;
-  }
-
-  var caughtException = false;
-  try {
-    var dm = Cc["@mozilla.org/download-manager;1"].
-             getService(Ci.nsIDownloadManager);
-  } catch (e) {
-    caughtException = true;
-  }
-  do_check_false(caughtException);
-}
deleted file mode 100644
--- a/toolkit/components/downloads/test/unit/test_old_download_files_removed.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* 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/. */
-
-// Make sure we remove old, now-unused downloads.rdf (pre-Firefox 3 storage)
-// when starting the download manager.
-
-function run_test()
-{
-  if (oldDownloadManagerDisabled()) {
-    return;
-  }
-
-  // Create the downloads.rdf file
-  importDownloadsFile("empty_downloads.rdf");
-
-  // Make sure it got created
-  let rdfFile = dirSvc.get("DLoads", Ci.nsIFile);
-  do_check_true(rdfFile.exists());
-
-  // Initialize the download manager, which will delete downloads.rdf
-  Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
-  do_check_false(rdfFile.exists());
-}
--- a/toolkit/components/downloads/test/unit/xpcshell.ini
+++ b/toolkit/components/downloads/test/unit/xpcshell.ini
@@ -1,38 +1,32 @@
 [DEFAULT]
 head = head_download_manager.js
 tail = tail_download_manager.js
 firefox-appdir = browser
 support-files =
-  bug_401582_downloads.sqlite
-  bug_409179_downloads.sqlite
   downloads_manifest.js
-  empty_downloads.rdf
   test_downloads.manifest
   data/digest.chunk
 
 [test_app_rep.js]
 [test_bug_382825.js]
 [test_bug_384744.js]
 [test_bug_395092.js]
 [test_bug_401430.js]
-[test_bug_401582.js]
 [test_bug_406857.js]
-[test_bug_409179.js]
 [test_bug_420230.js]
 [test_cancel_download_files_removed.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_download_manager.js]
 [test_download_samename.js]
 # Bug 676989: test hangs consistently on Android
 skip-if = os == "android"
 [test_guid.js]
 [test_history_expiration.js]
 [test_offline_support.js]
-[test_old_download_files_removed.js]
 [test_private_resume.js]
 [test_privatebrowsing.js]
 [test_privatebrowsing_cancel.js]
 [test_removeDownloadsByTimeframe.js]
 [test_resume.js]
 [test_sleep_wake.js]
--- a/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadIntegration.jsm
@@ -357,17 +357,17 @@ this.DownloadIntegration = {
       let directoryPath = null;
 #ifdef XP_MACOSX
       directoryPath = yield this.getPreferredDownloadsDirectory();
 #elifdef ANDROID
       directoryPath = yield this.getSystemDownloadsDirectory();
 #else
       // For Metro mode on Windows 8,  we want searchability for documents
       // that the user chose to open with an external application.
-      if (this._isImmersiveProcess()) {
+      if (Services.metro && Services.metro.immersive) {
         directoryPath = yield this.getSystemDownloadsDirectory();
       } else {
         directoryPath = this._getDirectory("TmpD");
       }
 #endif
       throw new Task.Result(directoryPath);
     }.bind(this));
   },
@@ -449,24 +449,16 @@ this.DownloadIntegration = {
       gDownloadPlatform.downloadDone(NetUtil.newURI(aDownload.source.url),
                                      new FileUtils.File(aDownload.target.path),
                                      aDownload.contentType,
                                      aDownload.source.isPrivate);
       this.downloadDoneCalled = true;
     }.bind(this));
   },
 
-  /**
-   * Determines whether it's a Windows Metro app.
-   */
-  _isImmersiveProcess: function() {
-    // TODO: to be implemented
-    return false;
-  },
-
   /*
    * Launches a file represented by the target of a download. This can
    * open the file with the default application for the target MIME type
    * or file extension, or with a custom application if
    * aDownload.launcherPath is set.
    *
    * @param    aDownload
    *           A Download object that contains the necessary information
--- 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.
deleted file mode 100644
index c6f03e6a08ae14cad5bd47cecd6514b102b71ee1..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
--- a/toolkit/forgetaboutsite/test/unit/test_removeDataFromDomain_activeDownloads.js
+++ /dev/null
@@ -1,137 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- * vim: sw=2 ts=2 sts=2
- * 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/. */
-
-/**
- * Test added with bug 460086 to test the behavior of the new API that was added
- * to remove all traces of visiting a site.
- */
-
-Components.utils.import("resource://gre/modules/Services.jsm");
-Components.utils.import("resource://gre/modules/ForgetAboutSite.jsm");
-
-////////////////////////////////////////////////////////////////////////////////
-//// Utility Functions
-
-/**
- * Creates an nsIURI object for the given file.
- *
- * @param aFile
- *        The nsIFile of the URI to create.
- * @returns an nsIURI representing aFile.
- */
-function uri(aFile)
-{
-  return Cc["@mozilla.org/network/io-service;1"].
-         getService(Ci.nsIIOService).
-         newFileURI(aFile);
-}
-
-/**
- * Checks to ensure a URI string is in download history or not.
- *
- * @param aURIString
- *        The string of the URI to check.
- * @param aIsActive
- *        True if the URI should be actively downloaded, false otherwise.
- */
-function check_active_download(aURIString, aIsActive)
-{
-  let dm = Cc["@mozilla.org/download-manager;1"].
-           getService(Ci.nsIDownloadManager);
-  let enumerator = dm.activeDownloads;
-  let found = false;
-  while (enumerator.hasMoreElements()) {
-    let dl = enumerator.getNext().QueryInterface(Ci.nsIDownload);
-    if (dl.source.spec == aURIString)
-      found = true;
-  }
-  let checker = aIsActive ? do_check_true : do_check_false;
-  checker(found);
-}
-
-////////////////////////////////////////////////////////////////////////////////
-//// Test Functions
-
-let destFile = dirSvc.get("TmpD", Ci.nsIFile);
-destFile.append("dm-test-file");
-destFile = uri(destFile);
-let data = [
-  { source: "http://mozilla.org/direct_match",
-    target: destFile.spec,
-    removed: true
-  },
-  { source: "http://www.mozilla.org/subdomain",
-    target: destFile.spec,
-    removed: true
-  },
-  { source: "http://ilovemozilla.org/contains_domain",
-    target: destFile.spec,
-    removed: false
-  },
-];
-
-function makeGUID() {
-  let guid = "";
-  for (var i = 0; i < 12; i++)
-    guid += Math.floor(Math.random() * 10);
-  return guid;
-}
-
-function run_test()
-{
-  if (oldDownloadManagerDisabled()) {
-    return;
-  }
-
-  // We add this data to the database first, but we cannot instantiate the
-  // download manager service, otherwise these downloads will not be placed in
-  // the active downloads array.
-
-  // Copy the empty downloads database to our profile directory
-  let downloads = do_get_file("downloads.empty.sqlite");
-  downloads.copyTo(dirSvc.get("ProfD", Ci.nsIFile), "downloads.sqlite");
-
-  // Open the database
-  let ss = Cc["@mozilla.org/storage/service;1"].
-           getService(Ci.mozIStorageService);
-  let file = dirSvc.get("ProfD", Ci.nsIFile);
-  file.append("downloads.sqlite");
-  let db = ss.openDatabase(file);
-
-  // Insert the data
-  let stmt = db.createStatement(
-    "INSERT INTO moz_downloads (source, target, state, autoResume, entityID, guid) " +
-    "VALUES (:source, :target, :state, :autoResume, :entityID, :guid)"
-  );
-  for (let i = 0; i < data.length; i++) {
-    stmt.params.source = data[i].source;
-    stmt.params.target = data[i].target;
-    stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_PAUSED;
-    stmt.params.autoResume = 0; // DONT_RESUME is 0
-    stmt.params.entityID = "foo" // just has to be non-null for our test
-    stmt.params.guid = makeGUID();
-    stmt.execute();
-    stmt.reset();
-  }
-  stmt.finalize();
-  stmt = null;
-  db.close();
-  db = null;
-
-  // Check to make sure it's all there
-  for (let i = 0; i < data.length; i++)
-    check_active_download(data[i].source, true);
-
-  // Dispatch the remove call
-  ForgetAboutSite.removeDataFromDomain("mozilla.org");
-
-  // And check our data
-  for (let i = 0; i < data.length; i++)
-    check_active_download(data[i].source, !data[i].removed);
-
-  // Shutdown the download manager.
-  Services.obs.notifyObservers(null, "quit-application", null);
-}
--- a/toolkit/forgetaboutsite/test/unit/xpcshell.ini
+++ b/toolkit/forgetaboutsite/test/unit/xpcshell.ini
@@ -1,7 +1,5 @@
 [DEFAULT]
 head = head_forgetaboutsite.js
 tail =
-support-files = downloads.empty.sqlite
 
 [test_removeDataFromDomain.js]
-[test_removeDataFromDomain_activeDownloads.js]
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -1236,17 +1236,24 @@ MockInstall.prototype = {
 
 function waitForCondition(condition, nextTest, errorMsg) {
   let tries = 0;
   let interval = setInterval(function() {
     if (tries >= 30) {
       ok(false, errorMsg);
       moveOn();
     }
-    if (condition()) {
+    var conditionPassed;
+    try {
+      conditionPassed = condition();
+    } catch (e) {
+      ok(false, e + "\n" + e.stack);
+      conditionPassed = false;
+    }
+    if (conditionPassed) {
       moveOn();
     }
     tries++;
   }, 100);
   let moveOn = function() { clearInterval(interval); nextTest(); };
 }
 
 function getTestPluginTag() {