Merge latest green inbound changeset to mozilla-central
authorEd Morley <emorley@mozilla.com>
Thu, 21 Mar 2013 11:49:34 +0000
changeset 125733 a73a2b5c423b
parent 125732 88bf3f7c0e2b (current diff)
parent 125586 4caaa0bc587d (diff)
child 125734 d6a51ac10751
child 125741 be6da6dbf632
push id24461
push useremorley@mozilla.com
push dateThu, 21 Mar 2013 11:51:51 +0000
treeherdermozilla-central@a73a2b5c423b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone22.0a1
first release with
nightly linux32
a73a2b5c423b / 22.0a1 / 20130321090706 / files
nightly linux64
a73a2b5c423b / 22.0a1 / 20130321090706 / files
nightly mac
a73a2b5c423b / 22.0a1 / 20130321090706 / files
nightly win32
a73a2b5c423b / 22.0a1 / 20130321090706 / files
nightly win64
a73a2b5c423b / 22.0a1 / 20130321090706 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge latest green inbound changeset to mozilla-central
browser/base/content/newtab/batch.js
deleted file mode 100644
--- a/browser/base/content/newtab/batch.js
+++ /dev/null
@@ -1,76 +0,0 @@
-#ifdef 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/. */
-#endif
-
-/**
- * This class makes it easy to wait until a batch of callbacks has finished.
- *
- * Example:
- *
- * let batch = new Batch(function () alert("finished"));
- * let pop = batch.pop.bind(batch);
- *
- * for (let i = 0; i < 5; i++) {
- *   batch.push();
- *   setTimeout(pop, i * 1000);
- * }
- *
- * batch.close();
- */
-function Batch(aCallback) {
-  this._callback = aCallback;
-}
-
-Batch.prototype = {
-  /**
-   * The number of batch entries.
-   */
-  _count: 0,
-
-  /**
-   * Whether this batch is closed.
-   */
-  _closed: false,
-
-  /**
-   * Increases the number of batch entries by one.
-   */
-  push: function Batch_push() {
-    if (!this._closed)
-      this._count++;
-  },
-
-  /**
-   * Decreases the number of batch entries by one.
-   */
-  pop: function Batch_pop() {
-    if (this._count)
-      this._count--;
-
-    if (this._closed)
-      this._check();
-  },
-
-  /**
-   * Closes the batch so that no new entries can be added.
-   */
-  close: function Batch_close() {
-    if (this._closed)
-      return;
-
-    this._closed = true;
-    this._check();
-  },
-
-  /**
-   * Checks if the batch has finished.
-   */
-  _check: function Batch_check() {
-    if (this._count == 0 && this._callback) {
-      this._callback();
-      this._callback = null;
-    }
-  }
-};
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -6,16 +6,17 @@
 
 let Cu = Components.utils;
 let Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PageThumbs.jsm");
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
+Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 let {
   links: gLinks,
@@ -34,17 +35,16 @@ XPCOMUtils.defineLazyGetter(this, "gStri
 function newTabString(name) gStringBundle.GetStringFromName('newtab.' + name);
 
 function inPrivateBrowsingMode() {
   return PrivateBrowsingUtils.isWindowPrivate(window);
 }
 
 const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
 
-#include batch.js
 #include transformations.js
 #include page.js
 #include grid.js
 #include cells.js
 #include sites.js
 #include drag.js
 #include dragDataHelper.js
 #include drop.js
--- a/browser/base/content/newtab/transformations.js
+++ b/browser/base/content/newtab/transformations.js
@@ -164,47 +164,43 @@ let gTransformation = {
    * Rearranges a given array of sites and moves them to their new positions or
    * fades in/out new/removed sites.
    * @param aSites An array of sites to rearrange.
    * @param aOptions Set of options (see below).
    *        unfreeze - unfreeze the site after rearranging
    *        callback - the callback to call when finished
    */
   rearrangeSites: function Transformation_rearrangeSites(aSites, aOptions) {
-    let batch;
+    let batch = [];
     let cells = gGrid.cells;
     let callback = aOptions && aOptions.callback;
     let unfreeze = aOptions && aOptions.unfreeze;
 
-    if (callback) {
-      batch = new Batch(callback);
-      callback = function () batch.pop();
-    }
-
     aSites.forEach(function (aSite, aIndex) {
       // Do not re-arrange empty cells or the dragged site.
       if (!aSite || aSite == gDrag.draggedSite)
         return;
 
-      if (batch)
-        batch.push();
+      let deferred = Promise.defer();
+      batch.push(deferred.promise);
+      let cb = function () deferred.resolve();
 
       if (!cells[aIndex])
         // The site disappeared from the grid, hide it.
-        this.hideSite(aSite, callback);
+        this.hideSite(aSite, cb);
       else if (this._getNodeOpacity(aSite.node) != 1)
         // The site disappeared before but is now back, show it.
-        this.showSite(aSite, callback);
+        this.showSite(aSite, cb);
       else
         // The site's position has changed, move it around.
-        this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: callback});
+        this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb});
     }, this);
 
-    if (batch)
-      batch.close();
+    let wait = Promise.promised(function () callback && callback());
+    wait.apply(null, batch);
   },
 
   /**
    * Listens for the 'transitionend' event on a given node and calls the given
    * callback.
    * @param aNode The node that is transitioned.
    * @param aCallback The callback to call when finished.
    */
--- a/browser/base/content/newtab/updater.js
+++ b/browser/base/content/newtab/updater.js
@@ -121,62 +121,66 @@ let gUpdater = {
 
   /**
    * Removes all sites from the grid that are not in the given links array or
    * exceed the grid.
    * @param aSites The array of sites remaining in the grid.
    * @param aCallback The callback to call when finished.
    */
   _removeLegacySites: function Updater_removeLegacySites(aSites, aCallback) {
-    let batch = new Batch(aCallback);
+    let batch = [];
 
     // Delete sites that were removed from the grid.
     gGrid.sites.forEach(function (aSite) {
       // The site must be valid and not in the current grid.
       if (!aSite || aSites.indexOf(aSite) != -1)
         return;
 
-      batch.push();
+      let deferred = Promise.defer();
+      batch.push(deferred.promise);
 
       // Fade out the to-be-removed site.
       gTransformation.hideSite(aSite, function () {
         let node = aSite.node;
 
         // Remove the site from the DOM.
         node.parentNode.removeChild(node);
-        batch.pop();
+        deferred.resolve();
       });
     });
 
-    batch.close();
+    let wait = Promise.promised(aCallback);
+    wait.apply(null, batch);
   },
 
   /**
    * Tries to fill empty cells with new links if available.
    * @param aLinks The array of links.
    * @param aCallback The callback to call when finished.
    */
   _fillEmptyCells: function Updater_fillEmptyCells(aLinks, aCallback) {
     let {cells, sites} = gGrid;
-    let batch = new Batch(aCallback);
+    let batch = [];
 
     // Find empty cells and fill them.
     sites.forEach(function (aSite, aIndex) {
       if (aSite || !aLinks[aIndex])
         return;
 
-      batch.push();
+      let deferred = Promise.defer();
+      batch.push(deferred.promise);
 
       // Create the new site and fade it in.
       let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
 
       // Set the site's initial opacity to zero.
       site.node.style.opacity = 0;
 
       // Flush all style changes for the dynamically inserted site to make
       // the fade-in transition work.
       window.getComputedStyle(site.node).opacity;
-      gTransformation.showSite(site, function () batch.pop());
+      gTransformation.showSite(site, function () deferred.resolve());
     });
 
-    batch.close();
+    let wait = Promise.promised(aCallback);
+    wait.apply(null, batch);
   }
 };
--- a/browser/components/sessionstore/src/_SessionFile.jsm
+++ b/browser/components/sessionstore/src/_SessionFile.jsm
@@ -199,23 +199,30 @@ let SessionFileInternal = {
     });
   },
 
   write: function ssfi_write(aData) {
     let refObj = {};
     let self = this;
     return TaskUtils.spawn(function task() {
       TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
+      TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
 
       let bytes = gEncoder.encode(aData);
 
       try {
-        yield OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
+        let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"});
+        // At this point, we measure how long we stop the main thread
+        TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
+
+        // Now wait for the result and measure how long we had to wait for the result
+        yield promise;
         TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
       } catch (ex) {
+        TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
         TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj);
         Cu.reportError("Could not write session state file " + self.path
                        + ": " + aReason);
       }
     });
   },
 
   createBackupCopy: function ssfi_createBackupCopy() {
--- a/browser/devtools/markupview/MarkupView.jsm
+++ b/browser/devtools/markupview/MarkupView.jsm
@@ -13,16 +13,17 @@ const PAGE_SIZE = 10;
 
 const PREVIEW_AREA = 700;
 const DEFAULT_MAX_CHILDREN = 100;
 
 this.EXPORTED_SYMBOLS = ["MarkupView"];
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
 Cu.import("resource:///modules/devtools/Templater.jsm");
 Cu.import("resource:///modules/devtools/Undo.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * Vocabulary for the purposes of this file:
  *
@@ -957,17 +958,17 @@ function DoctypeEditor(aContainer, aNode
  * @param string aTemplate The template id to use to build the editor.
  */
 function TextEditor(aContainer, aNode, aTemplate)
 {
   this.node = aNode;
 
   aContainer.markup.template(aTemplate, this);
 
-  _editableField({
+  editableField({
     element: this.value,
     stopOnReturn: true,
     trigger: "dblclick",
     multiline: true,
     done: function TE_done(aVal, aCommit) {
       if (!aCommit) {
         return;
       }
@@ -1026,26 +1027,26 @@ function ElementEditor(aContainer, aNode
   }
 
   // Create the closing tag
   this.template("elementClose", this);
 
   // Make the tag name editable (unless this is a document element)
   if (aNode != aNode.ownerDocument.documentElement) {
     this.tag.setAttribute("tabindex", "0");
-    _editableField({
+    editableField({
       element: this.tag,
       trigger: "dblclick",
       stopOnReturn: true,
       done: this.onTagEdit.bind(this),
     });
   }
 
   // Make the new attribute space editable.
-  _editableField({
+  editableField({
     element: this.newAttr,
     trigger: "dblclick",
     stopOnReturn: true,
     done: function EE_onNew(aVal, aCommit) {
       if (!aCommit) {
         return;
       }
 
@@ -1115,17 +1116,17 @@ ElementEditor.prototype = {
         before = this.attrList.firstChild;
       } else if (aAttr.name == "class") {
         let idNode = this.attrs["id"];
         before = idNode ? idNode.nextSibling : this.attrList.firstChild;
       }
       this.attrList.insertBefore(attr, before);
 
       // Make the attribute editable.
-      _editableField({
+      editableField({
         element: inner,
         trigger: "dblclick",
         stopOnReturn: true,
         selectAll: false,
         start: function EE_editAttribute_start(aEditor, aEvent) {
           // If the editing was started inside the name or value areas,
           // select accordingly.
           if (aEvent && aEvent.target === name) {
--- a/browser/devtools/markupview/test/browser_inspector_markup_edit.js
+++ b/browser/devtools/markupview/test/browser_inspector_markup_edit.js
@@ -13,20 +13,19 @@ http://creativecommons.org/publicdomain/
  *
  * This test mostly tries to verify that the editor makes changes to the
  * underlying DOM, not that the UI updates - UI updates are based on
  * underlying DOM changes, and the mutation tests should cover those cases.
  */
 
 function test() {
   let inspector;
-  let tempScope = {}
-  Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-
-  let inplaceEditor = tempScope._getInplaceEditorForSpan;
+  let {
+    getInplaceEditorForSpan: inplaceEditor
+  } = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {});
 
   waitForExplicitFinish();
 
   // Will hold the doc we're viewing
   let doc;
 
   // Holds the MarkupTool object we're testing.
   let markup;
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/InplaceEditor.jsm
@@ -0,0 +1,849 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+
+/**
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * Basic use:
+ * let spanToEdit = document.getElementById("somespan");
+ *
+ * editableField({
+ *   element: spanToEdit,
+ *   done: function(value, commit) {
+ *     if (commit) {
+ *       spanToEdit.textContent = value;
+ *     }
+ *   },
+ *   trigger: "dblclick"
+ * });
+ *
+ * See editableField() for more options.
+ */
+
+"use strict";
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["editableItem",
+                         "editableField",
+                         "getInplaceEditorForSpan",
+                         "InplaceEditor"];
+
+/**
+ * Mark a span editable.  |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {object} aOptions
+ *    Options for the editable field, including:
+ *    {Element} element:
+ *      (required) The span to be edited on focus.
+ *    {function} canEdit:
+ *       Will be called before creating the inplace editor.  Editor
+ *       won't be created if canEdit returns false.
+ *    {function} start:
+ *       Will be called when the inplace editor is initialized.
+ *    {function} change:
+ *       Will be called when the text input changes.  Will be called
+ *       with the current value of the text input.
+ *    {function} done:
+ *       Called when input is committed or blurred.  Called with
+ *       current value and a boolean telling the caller whether to
+ *       commit the change.  This function is called before the editor
+ *       has been torn down.
+ *    {function} destroy:
+ *       Called when the editor is destroyed and has been torn down.
+ *    {string} advanceChars:
+ *       If any characters in advanceChars are typed, focus will advance
+ *       to the next element.
+ *    {boolean} stopOnReturn:
+ *       If true, the return key will not advance the editor to the next
+ *       focusable element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
+ */
+function editableField(aOptions)
+{
+  return editableItem(aOptions, function(aElement, aEvent) {
+    new InplaceEditor(aOptions, aEvent);
+  });
+}
+
+/**
+ * Handle events for an element that should respond to
+ * clicks and sit in the editing tab order, and call
+ * a callback when it is activated.
+ *
+ * @param {object} aOptions
+ *    The options for this editor, including:
+ *    {Element} element: The DOM element.
+ *    {string} trigger: The DOM event that should trigger editing,
+ *      defaults to "click"
+ * @param {function} aCallback
+ *        Called when the editor is activated.
+ */
+this.editableItem = function editableItem(aOptions, aCallback)
+{
+  let trigger = aOptions.trigger || "click"
+  let element = aOptions.element;
+  element.addEventListener(trigger, function(evt) {
+    let win = this.ownerDocument.defaultView;
+    let selection = win.getSelection();
+    if (trigger != "click" || selection.isCollapsed) {
+      aCallback(element, evt);
+    }
+    evt.stopPropagation();
+  }, false);
+
+  // If focused by means other than a click, start editing by
+  // pressing enter or space.
+  element.addEventListener("keypress", function(evt) {
+    if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
+        evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+      aCallback(element);
+    }
+  }, true);
+
+  // Ugly workaround - the element is focused on mousedown but
+  // the editor is activated on click/mouseup.  This leads
+  // to an ugly flash of the focus ring before showing the editor.
+  // So hide the focus ring while the mouse is down.
+  element.addEventListener("mousedown", function(evt) {
+    let cleanup = function() {
+      element.style.removeProperty("outline-style");
+      element.removeEventListener("mouseup", cleanup, false);
+      element.removeEventListener("mouseout", cleanup, false);
+    };
+    element.style.setProperty("outline-style", "none");
+    element.addEventListener("mouseup", cleanup, false);
+    element.addEventListener("mouseout", cleanup, false);
+  }, false);
+
+  // Mark the element editable field for tab
+  // navigation while editing.
+  element._editable = true;
+}
+
+/*
+ * Various API consumers (especially tests) sometimes want to grab the
+ * inplaceEditor expando off span elements. However, when each global has its
+ * own compartment, those expandos live on Xray wrappers that are only visible
+ * within this JSM. So we provide a little workaround here.
+ */
+this.getInplaceEditorForSpan = function getInplaceEditorForSpan(aSpan)
+{
+  return aSpan.inplaceEditor;
+};
+
+function InplaceEditor(aOptions, aEvent)
+{
+  this.elt = aOptions.element;
+  let doc = this.elt.ownerDocument;
+  this.doc = doc;
+  this.elt.inplaceEditor = this;
+
+  this.change = aOptions.change;
+  this.done = aOptions.done;
+  this.destroy = aOptions.destroy;
+  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+  this.multiline = aOptions.multiline || false;
+  this.stopOnReturn = !!aOptions.stopOnReturn;
+
+  this._onBlur = this._onBlur.bind(this);
+  this._onKeyPress = this._onKeyPress.bind(this);
+  this._onInput = this._onInput.bind(this);
+  this._onKeyup = this._onKeyup.bind(this);
+
+  this._createInput();
+  this._autosize();
+
+  // Pull out character codes for advanceChars, listing the
+  // characters that should trigger a blur.
+  this._advanceCharCodes = {};
+  let advanceChars = aOptions.advanceChars || '';
+  for (let i = 0; i < advanceChars.length; i++) {
+    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
+  }
+
+  // Hide the provided element and add our editor.
+  this.originalDisplay = this.elt.style.display;
+  this.elt.style.display = "none";
+  this.elt.parentNode.insertBefore(this.input, this.elt);
+
+  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
+    this.input.select();
+  }
+  this.input.focus();
+
+  this.input.addEventListener("blur", this._onBlur, false);
+  this.input.addEventListener("keypress", this._onKeyPress, false);
+  this.input.addEventListener("input", this._onInput, false);
+  this.input.addEventListener("mousedown", function(aEvt) {
+                                             aEvt.stopPropagation();
+                                           }, false);
+
+  this.warning = aOptions.warning;
+  this.validate = aOptions.validate;
+
+  if (this.warning && this.validate) {
+    this.input.addEventListener("keyup", this._onKeyup, false);
+  }
+
+  if (aOptions.start) {
+    aOptions.start(this, aEvent);
+  }
+}
+
+InplaceEditor.prototype = {
+  _createInput: function InplaceEditor_createEditor()
+  {
+    this.input =
+      this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
+    this.input.inplaceEditor = this;
+    this.input.classList.add("styleinspector-propertyeditor");
+    this.input.value = this.initial;
+
+    copyTextStyles(this.elt, this.input);
+  },
+
+  /**
+   * Get rid of the editor.
+   */
+  _clear: function InplaceEditor_clear()
+  {
+    if (!this.input) {
+      // Already cleared.
+      return;
+    }
+
+    this.input.removeEventListener("blur", this._onBlur, false);
+    this.input.removeEventListener("keypress", this._onKeyPress, false);
+    this.input.removeEventListener("keyup", this._onKeyup, false);
+    this.input.removeEventListener("oninput", this._onInput, false);
+    this._stopAutosize();
+
+    this.elt.style.display = this.originalDisplay;
+    this.elt.focus();
+
+    if (this.destroy) {
+      this.destroy();
+    }
+
+    this.elt.parentNode.removeChild(this.input);
+    this.input = null;
+
+    delete this.elt.inplaceEditor;
+    delete this.elt;
+  },
+
+  /**
+   * Keeps the editor close to the size of its input string.  This is pretty
+   * crappy, suggestions for improvement welcome.
+   */
+  _autosize: function InplaceEditor_autosize()
+  {
+    // Create a hidden, absolutely-positioned span to measure the text
+    // in the input.  Boo.
+
+    // We can't just measure the original element because a) we don't
+    // change the underlying element's text ourselves (we leave that
+    // up to the client), and b) without tweaking the style of the
+    // original element, it might wrap differently or something.
+    this._measurement =
+      this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
+    this._measurement.className = "autosizer";
+    this.elt.parentNode.appendChild(this._measurement);
+    let style = this._measurement.style;
+    style.visibility = "hidden";
+    style.position = "absolute";
+    style.top = "0";
+    style.left = "0";
+    copyTextStyles(this.input, this._measurement);
+    this._updateSize();
+  },
+
+  /**
+   * Clean up the mess created by _autosize().
+   */
+  _stopAutosize: function InplaceEditor_stopAutosize()
+  {
+    if (!this._measurement) {
+      return;
+    }
+    this._measurement.parentNode.removeChild(this._measurement);
+    delete this._measurement;
+  },
+
+  /**
+   * Size the editor to fit its current contents.
+   */
+  _updateSize: function InplaceEditor_updateSize()
+  {
+    // Replace spaces with non-breaking spaces.  Otherwise setting
+    // the span's textContent will collapse spaces and the measurement
+    // will be wrong.
+    this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
+
+    // We add a bit of padding to the end.  Should be enough to fit
+    // any letter that could be typed, otherwise we'll scroll before
+    // we get a chance to resize.  Yuck.
+    let width = this._measurement.offsetWidth + 10;
+
+    if (this.multiline) {
+      // Make sure there's some content in the current line.  This is a hack to
+      // account for the fact that after adding a newline the <pre> doesn't grow
+      // unless there's text content on the line.
+      width += 15;
+      this._measurement.textContent += "M";
+      this.input.style.height = this._measurement.offsetHeight + "px";
+    }
+
+    this.input.style.width = width + "px";
+  },
+
+   /**
+   * Increment property values in rule view.
+   *
+   * @param {number} increment
+   *        The amount to increase/decrease the property value.
+   * @return {bool} true if value has been incremented.
+   */
+  _incrementValue: function InplaceEditor_incrementValue(increment)
+  {
+    let value = this.input.value;
+    let selectionStart = this.input.selectionStart;
+    let selectionEnd = this.input.selectionEnd;
+
+    let newValue = this._incrementCSSValue(value, increment, selectionStart,
+                                           selectionEnd);
+
+    if (!newValue) {
+      return false;
+    }
+
+    this.input.value = newValue.value;
+    this.input.setSelectionRange(newValue.start, newValue.end);
+
+    return true;
+  },
+
+  /**
+   * Increment the property value based on the property type.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increase/decrease the property value.
+   * @param {number} selStart
+   *        Starting index of the value.
+   * @param {number} selEnd
+   *        Ending index of the value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
+                                                               selStart, selEnd)
+  {
+    let range = this._parseCSSValue(value, selStart);
+    let type = (range && range.type) || "";
+    let rawValue = (range ? value.substring(range.start, range.end) : "");
+    let incrementedValue = null, selection;
+
+    if (type === "num") {
+      let newValue = this._incrementRawValue(rawValue, increment);
+      if (newValue !== null) {
+        incrementedValue = newValue;
+        selection = [0, incrementedValue.length];
+      }
+    } else if (type === "hex") {
+      let exprOffset = selStart - range.start;
+      let exprOffsetEnd = selEnd - range.start;
+      let newValue = this._incHexColor(rawValue, increment, exprOffset,
+                                       exprOffsetEnd);
+      if (newValue) {
+        incrementedValue = newValue.value;
+        selection = newValue.selection;
+      }
+    } else {
+      let info;
+      if (type === "rgb" || type === "hsl") {
+        info = {};
+        let part = value.substring(range.start, selStart).split(",").length - 1;
+        if (part === 3) { // alpha
+          info.minValue = 0;
+          info.maxValue = 1;
+        } else if (type === "rgb") {
+          info.minValue = 0;
+          info.maxValue = 255;
+        } else if (part !== 0) { // hsl percentage
+          info.minValue = 0;
+          info.maxValue = 100;
+
+          // select the previous number if the selection is at the end of a
+          // percentage sign.
+          if (value.charAt(selStart - 1) === "%") {
+            --selStart;
+          }
+        }
+      }
+      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
+    }
+
+    if (incrementedValue === null) {
+      return;
+    }
+
+    let preRawValue = value.substr(0, range.start);
+    let postRawValue = value.substr(range.end);
+
+    return {
+      value: preRawValue + incrementedValue + postRawValue,
+      start: range.start + selection[0],
+      end: range.start + selection[1]
+    };
+  },
+
+  /**
+   * Parses the property value and type.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} offset
+   *        Starting index of value.
+   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
+   */
+   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
+  {
+    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
+    let start = 0;
+    let m;
+
+    // retreive values from left to right until we find the one at our offset
+    while ((m = reSplitCSS.exec(value)) &&
+          (m.index + m[0].length < offset)) {
+      value = value.substr(m.index + m[0].length);
+      start += m.index + m[0].length;
+      offset -= m.index + m[0].length;
+    }
+
+    if (!m) {
+      return;
+    }
+
+    let type;
+    if (m[1]) {
+      type = "url";
+    } else if (m[2]) {
+      type = "rgb";
+    } else if (m[3]) {
+      type = "hsl";
+    } else if (m[4]) {
+      type = "hex";
+    } else if (m[5]) {
+      type = "num";
+    }
+
+    return {
+      value: m[0],
+      start: start + m.index,
+      end: start + m.index + m[0].length,
+      type: type
+    };
+  },
+
+  /**
+   * Increment the property value for types other than
+   * number or hex, such as rgb, hsl, and file names.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increment/decrement.
+   * @param {number} offset
+   *        Starting index of the property value.
+   * @param {number} offsetEnd
+   *        Ending index of the property value.
+   * @param {object} info
+   *        Object with details about the property value.
+   * @return {object} object with properties 'value', 'start', and 'end'.
+   */
+  _incrementGenericValue:
+  function InplaceEditor_incrementGenericValue(value, increment, offset,
+                                               offsetEnd, info)
+  {
+    // Try to find a number around the cursor to increment.
+    let start, end;
+    // Check if we are incrementing in a non-number context (such as a URL)
+    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
+      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
+      // We have a number selected, possibly with a suffix, and we are not in
+      // the disallowed case of just part of a known number being selected.
+      // Use that number.
+      start = offset;
+      end = offsetEnd;
+    } else {
+      // Parse periods as belonging to the number only if we are in a known number
+      // context. (This makes incrementing the 1 in 'image1.gif' work.)
+      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
+      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
+      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
+
+      start = offset - before;
+      end = offset + after;
+
+      // Expand the number to contain an initial minus sign if it seems
+      // free-standing.
+      if (value.charAt(start - 1) === "-" &&
+         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
+        --start;
+      }
+    }
+
+    if (start !== end)
+    {
+      // Include percentages as part of the incremented number (they are
+      // common enough).
+      if (value.charAt(end) === "%") {
+        ++end;
+      }
+
+      let first = value.substr(0, start);
+      let mid = value.substring(start, end);
+      let last = value.substr(end);
+
+      mid = this._incrementRawValue(mid, increment, info);
+
+      if (mid !== null) {
+        return {
+          value: first + mid + last,
+          start: start,
+          end: start + mid.length
+        };
+      }
+    }
+  },
+
+  /**
+   * Increment the property value for numbers.
+   *
+   * @param {string} rawValue
+   *        Raw value to increment.
+   * @param {number} increment
+   *        Amount to increase/decrease the raw value.
+   * @param {object} info
+   *        Object with info about the property value.
+   * @return {string} the incremented value.
+   */
+  _incrementRawValue:
+  function InplaceEditor_incrementRawValue(rawValue, increment, info)
+  {
+    let num = parseFloat(rawValue);
+
+    if (isNaN(num)) {
+      return null;
+    }
+
+    let number = /\d+(\.\d+)?/.exec(rawValue);
+    let units = rawValue.substr(number.index + number[0].length);
+
+    // avoid rounding errors
+    let newValue = Math.round((num + increment) * 1000) / 1000;
+
+    if (info && "minValue" in info) {
+      newValue = Math.max(newValue, info.minValue);
+    }
+    if (info && "maxValue" in info) {
+      newValue = Math.min(newValue, info.maxValue);
+    }
+
+    newValue = newValue.toString();
+
+    return newValue + units;
+  },
+
+  /**
+   * Increment the property value for hex.
+   *
+   * @param {string} value
+   *        Property value.
+   * @param {number} increment
+   *        Amount to increase/decrease the property value.
+   * @param {number} offset
+   *        Starting index of the property value.
+   * @param {number} offsetEnd
+   *        Ending index of the property value.
+   * @return {object} object with properties 'value' and 'selection'.
+   */
+  _incHexColor:
+  function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
+  {
+    // Return early if no part of the rawValue is selected.
+    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
+      return;
+    }
+    if (offset < 1 && offsetEnd <= 1) {
+      return;
+    }
+    // Ignore the leading #.
+    rawValue = rawValue.substr(1);
+    --offset;
+    --offsetEnd;
+
+    // Clamp the selection to within the actual value.
+    offset = Math.max(offset, 0);
+    offsetEnd = Math.min(offsetEnd, rawValue.length);
+    offsetEnd = Math.max(offsetEnd, offset);
+
+    // Normalize #ABC -> #AABBCC.
+    if (rawValue.length === 3) {
+      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
+                 rawValue.charAt(1) + rawValue.charAt(1) +
+                 rawValue.charAt(2) + rawValue.charAt(2);
+      offset *= 2;
+      offsetEnd *= 2;
+    }
+
+    if (rawValue.length !== 6) {
+      return;
+    }
+
+    // If no selection, increment an adjacent color, preferably one to the left.
+    if (offset === offsetEnd) {
+      if (offset === 0) {
+        offsetEnd = 1;
+      } else {
+        offset = offsetEnd - 1;
+      }
+    }
+
+    // Make the selection cover entire parts.
+    offset -= offset % 2;
+    offsetEnd += offsetEnd % 2;
+
+    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
+    if (-1 < increment && increment < 1) {
+      increment = (increment < 0 ? -1 : 1);
+    }
+    if (Math.abs(increment) === 10) {
+      increment = (increment < 0 ? -16 : 16);
+    }
+
+    let isUpper = (rawValue.toUpperCase() === rawValue);
+
+    for (let pos = offset; pos < offsetEnd; pos += 2) {
+      // Increment the part in [pos, pos+2).
+      let mid = rawValue.substr(pos, 2);
+      let value = parseInt(mid, 16);
+
+      if (isNaN(value)) {
+        return;
+      }
+
+      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
+
+      while (mid.length < 2) {
+        mid = "0" + mid;
+      }
+      if (isUpper) {
+        mid = mid.toUpperCase();
+      }
+
+      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
+    }
+
+    return {
+      value: "#" + rawValue,
+      selection: [offset + 1, offsetEnd + 1]
+    };
+  },
+
+  /**
+   * Call the client's done handler and clear out.
+   */
+  _apply: function InplaceEditor_apply(aEvent)
+  {
+    if (this._applied) {
+      return;
+    }
+
+    this._applied = true;
+
+    if (this.done) {
+      let val = this.input.value.trim();
+      return this.done(this.cancelled ? this.initial : val, !this.cancelled);
+    }
+
+    return null;
+  },
+
+  /**
+   * Handle loss of focus by calling done if it hasn't been called yet.
+   */
+  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
+  {
+    this._apply();
+    if (!aDoNotClear) {
+      this._clear();
+    }
+  },
+
+  /**
+   * Handle the input field's keypress event.
+   */
+  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
+  {
+    let prevent = false;
+
+    const largeIncrement = 100;
+    const mediumIncrement = 10;
+    const smallIncrement = 0.1;
+
+    let increment = 0;
+
+    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
+      increment = 1;
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+      increment = -1;
+    }
+
+    if (aEvent.shiftKey && !aEvent.altKey) {
+      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
+           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
+        increment *= largeIncrement;
+      } else {
+        increment *= mediumIncrement;
+      }
+    } else if (aEvent.altKey && !aEvent.shiftKey) {
+      increment *= smallIncrement;
+    }
+
+    if (increment && this._incrementValue(increment) ) {
+      this._updateSize();
+      prevent = true;
+    }
+
+    if (this.multiline &&
+        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
+        aEvent.shiftKey) {
+      prevent = false;
+    } else if (aEvent.charCode in this._advanceCharCodes
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
+      prevent = true;
+
+      let direction = FOCUS_FORWARD;
+      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
+          aEvent.shiftKey) {
+        this.cancelled = true;
+        direction = FOCUS_BACKWARD;
+      }
+      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+        direction = null;
+      }
+
+      let input = this.input;
+
+      this._apply();
+
+      if (direction !== null && focusManager.focusedElement === input) {
+        // If the focused element wasn't changed by the done callback,
+        // move the focus as requested.
+        let next = moveFocus(this.doc.defaultView, direction);
+
+        // If the next node to be focused has been tagged as an editable
+        // node, send it a click event to trigger
+        if (next && next.ownerDocument === this.doc && next._editable) {
+          next.click();
+        }
+      }
+
+      this._clear();
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
+      // Cancel and blur ourselves.
+      prevent = true;
+      this.cancelled = true;
+      this._apply();
+      this._clear();
+      aEvent.stopPropagation();
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+      // No need for leading spaces here.  This is particularly
+      // noticable when adding a property: it's very natural to type
+      // <name>: (which advances to the next property) then spacebar.
+      prevent = !this.input.value;
+    }
+
+    if (prevent) {
+      aEvent.preventDefault();
+    }
+  },
+
+  /**
+   * Handle the input field's keyup event.
+   */
+  _onKeyup: function(aEvent) {
+    // Validate the entered value.
+    this.warning.hidden = this.validate(this.input.value);
+    this._applied = false;
+    this._onBlur(null, true);
+  },
+
+  /**
+   * Handle changes to the input text.
+   */
+  _onInput: function InplaceEditor_onInput(aEvent)
+  {
+    // Validate the entered value.
+    if (this.warning && this.validate) {
+      this.warning.hidden = this.validate(this.input.value);
+    }
+
+    // Update size if we're autosizing.
+    if (this._measurement) {
+      this._updateSize();
+    }
+
+    // Call the user's change handler if available.
+    if (this.change) {
+      this.change(this.input.value.trim());
+    }
+  }
+};
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(aFrom, aTo)
+{
+  let win = aFrom.ownerDocument.defaultView;
+  let style = win.getComputedStyle(aFrom);
+  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(aWin, aDirection)
+{
+  return focusManager.moveFocus(aWin, null, aDirection, 0);
+}
+
+
+XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
+  return Services.focus;
+});
--- a/browser/devtools/styleeditor/StyleEditor.jsm
+++ b/browser/devtools/styleeditor/StyleEditor.jsm
@@ -117,29 +117,76 @@ StyleEditor.prototype = {
    */
   get styleSheet()
   {
     assert(this._styleSheet, "StyleSheet must be loaded first.");
     return this._styleSheet;
   },
 
   /**
+   * Recursively traverse imported stylesheets to find the index
+   *
+   * @param number aIndex
+   *        The index of the current sheet in the document.
+   * @param CSSStyleSheet aSheet
+   *        A stylesheet we're going to browse to look for all imported sheets.
+   */
+  _getImportedStyleSheetIndex: function SE__getImportedStyleSheetIndex(aIndex, aSheet)
+  {
+    let index = aIndex;
+    for (let j = 0; j < aSheet.cssRules.length; j++) {
+      let rule = aSheet.cssRules.item(j);
+      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
+        // Associated styleSheet may be null if it has already been seen due to
+        // duplicate @imports for the same URL.
+        if (!rule.styleSheet) {
+          continue;
+        }
+
+        if (rule.styleSheet == this.styleSheet) {
+          this._styleSheetIndex = index;
+          return index;
+        }
+        index++;
+        index = this._getImportedStyleSheetIndex(index, rule.styleSheet);
+
+        if (this._styleSheetIndex != -1) {
+          return index;
+        }
+      } else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
+        // @import rules must precede all others except @charset
+        return index;
+      }
+    }
+    return index;
+  },
+
+  /**
    * Retrieve the index (order) of stylesheet in the document.
    *
    * @return number
    */
   get styleSheetIndex()
   {
     let document = this.contentDocument;
     if (this._styleSheetIndex == -1) {
-      for (let i = 0; i < document.styleSheets.length; i++) {
-        if (document.styleSheets[i] == this.styleSheet) {
-          this._styleSheetIndex = i;
+      let index = 0;
+      let sheetIndex = 0;
+      while (sheetIndex <= document.styleSheets.length) {
+        let sheet = document.styleSheets[sheetIndex];
+        if (sheet == this.styleSheet) {
+          this._styleSheetIndex = index;
           break;
         }
+        index++;
+        index = this._getImportedStyleSheetIndex(index, sheet);
+        if (this._styleSheetIndex != -1) {
+          break;
+        }
+        sheetIndex++;
       }
     }
     return this._styleSheetIndex;
   },
 
   /**
    * Retrieve the input element that handles display and input for this editor.
    * Can be null if the editor is detached/headless, which means that this
--- a/browser/devtools/styleeditor/StyleEditorChrome.jsm
+++ b/browser/devtools/styleeditor/StyleEditorChrome.jsm
@@ -263,32 +263,44 @@ StyleEditorChrome.prototype = {
       let handler = listener["on" + aName];
       if (handler) {
         handler.apply(listener, aArgs);
       }
     }
   },
 
   /**
+   * Create a new style editor, add to the list of editors, and bind this
+   * object as an action listener.
+   * @param DOMDocument aDocument
+   *        The document that the stylesheet is being referenced in.
+   * @param CSSStyleSheet aSheet
+   *        Optional stylesheet to edit from the document.
+   * @return StyleEditor
+   */
+  _createStyleEditor: function SEC__createStyleEditor(aDocument, aSheet) {
+    let editor = new StyleEditor(aDocument, aSheet);
+    this._editors.push(editor);
+    editor.addActionListener(this);
+    return editor;
+  },
+
+  /**
    * Set up the chrome UI. Install event listeners and so on.
    */
   _setupChrome: function SEC__setupChrome()
   {
     // wire up UI elements
     wire(this._view.rootElement, ".style-editor-newButton", function onNewButton() {
-      let editor = new StyleEditor(this.contentDocument);
-      this._editors.push(editor);
-      editor.addActionListener(this);
+      let editor = this._createStyleEditor(this.contentDocument);
       editor.load();
     }.bind(this));
 
     wire(this._view.rootElement, ".style-editor-importButton", function onImportButton() {
-      let editor = new StyleEditor(this.contentDocument);
-      this._editors.push(editor);
-      editor.addActionListener(this);
+      let editor = this._createStyleEditor(this.contentDocument);
       editor.importFromFile(this._mockImportFile || null, this._window);
     }.bind(this));
   },
 
   /**
    * Reset the chrome UI to an empty and ready state.
    */
   resetChrome: function SEC__resetChrome()
@@ -303,34 +315,62 @@ StyleEditorChrome.prototype = {
     // (re)enable UI
     let matches = this._root.querySelectorAll("toolbarbutton,input,select");
     for (let i = 0; i < matches.length; i++) {
       matches[i].removeAttribute("disabled");
     }
   },
 
   /**
+   * Add all imported stylesheets to chrome UI, recursively
+   *
+   * @param CSSStyleSheet aSheet
+   *        A stylesheet we're going to browse to look for all imported sheets.
+   */
+  _showImportedStyleSheets: function SEC__showImportedStyleSheets(aSheet)
+  {
+    let document = this.contentDocument;
+    for (let j = 0; j < aSheet.cssRules.length; j++) {
+      let rule = aSheet.cssRules.item(j);
+      if (rule.type == Ci.nsIDOMCSSRule.IMPORT_RULE) {
+        // Associated styleSheet may be null if it has already been seen due to
+        // duplicate @imports for the same URL.
+        if (!rule.styleSheet) {
+          continue;
+        }
+
+        this._createStyleEditor(document, rule.styleSheet);
+
+        this._showImportedStyleSheets(rule.styleSheet);
+      } else if (rule.type != Ci.nsIDOMCSSRule.CHARSET_RULE) {
+        // @import rules must precede all others except @charset
+        return;
+      }
+    }
+  },
+
+  /**
    * Populate the chrome UI according to the content document.
    *
    * @see StyleEditor._setupShadowStyleSheet
    */
   _populateChrome: function SEC__populateChrome()
   {
     this.resetChrome();
 
     let document = this.contentDocument;
     this._document.title = _("chromeWindowTitle",
       document.title || document.location.href);
 
     for (let i = 0; i < document.styleSheets.length; i++) {
       let styleSheet = document.styleSheets[i];
 
-      let editor = new StyleEditor(document, styleSheet);
-      editor.addActionListener(this);
-      this._editors.push(editor);
+      this._createStyleEditor(document, styleSheet);
+
+      this._showImportedStyleSheets(styleSheet);
     }
 
     // Queue editors loading so that ContentAttach is consistently triggered
     // right after all editor instances are available (this.editors) but are
     // NOT loaded/ready yet. This also helps responsivity during loading when
     // there are many heavy stylesheets.
     this._editors.forEach(function (aEditor) {
       this._window.setTimeout(aEditor.load.bind(aEditor), 0);
--- a/browser/devtools/styleeditor/test/Makefile.in
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -12,32 +12,36 @@ include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
                  browser_styleeditor_enabled.js \
                  browser_styleeditor_filesave.js \
                  browser_styleeditor_cmd_edit.js \
                  browser_styleeditor_cmd_edit.html \
                  browser_styleeditor_import.js \
+                 browser_styleeditor_import_rule.js \
                  browser_styleeditor_init.js \
                  browser_styleeditor_loading.js \
                  browser_styleeditor_new.js \
                  browser_styleeditor_passedinsheet.js \
                  browser_styleeditor_pretty.js \
                  browser_styleeditor_private_perwindowpb.js \
                  browser_styleeditor_readonly.js \
                  browser_styleeditor_reopen.js \
                  browser_styleeditor_sv_keynav.js \
                  browser_styleeditor_sv_resize.js \
                  browser_styleeditor_bug_826982_location_changed.js \
                  head.js \
                  helpers.js \
                  four.html \
                  head.js \
                  helpers.js \
+                 import.css \
+                 import.html \
+                 import2.css \
                  longload.html \
                  media.html \
                  media-small.css \
                  minified.html \
                  resources_inpage.jsi \
                  resources_inpage1.css \
                  resources_inpage2.css \
                  simple.css \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_import_rule.js
@@ -0,0 +1,34 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
+
+function test()
+{
+  waitForExplicitFinish();
+
+  addTabAndLaunchStyleEditorChromeWhenLoaded(function (aChrome) {
+    run(aChrome);
+  });
+
+  content.location = TESTCASE_URI;
+}
+
+function run(aChrome)
+{
+  is(aChrome.editors.length, 3,
+    "there are 3 stylesheets after loading @imports");
+
+  is(aChrome.editors[0]._styleSheet.href, TEST_BASE_HTTP + "simple.css",
+    "stylesheet 1 is simple.css");
+
+  is(aChrome.editors[1]._styleSheet.href, TEST_BASE_HTTP + "import.css",
+    "stylesheet 2 is import.css");
+
+  is(aChrome.editors[2]._styleSheet.href, TEST_BASE_HTTP + "import2.css",
+    "stylesheet 3 is import2.css");
+
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/import.css
@@ -0,0 +1,10 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import2.css);
+
+body {
+  margin: 0;
+}
+
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/import.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+  <title>import testcase</title>
+  <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+  <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="import.css"/>
+</head>
+<body>
+  <div>import <span>testcase</span></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/import2.css
@@ -0,0 +1,10 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import.css);
+
+p {
+  padding: 5px;
+}
+
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -8,39 +8,34 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
-const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
-const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
-
 /**
  * These regular expressions are adapted from firebug's css.js, and are
  * used to parse CSSStyleDeclaration's cssText attribute.
  */
 
 // Used to split on css line separators
 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
 
 // Used to parse a single property line.
 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/CssLogic.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource:///modules/devtools/InplaceEditor.jsm");
 
 this.EXPORTED_SYMBOLS = ["CssRuleView",
-                         "_ElementStyle",
-                         "editableItem",
-                         "_editableField",
-                         "_getInplaceEditorForSpan"];
+                         "_ElementStyle"];
 
 /**
  * Our model looks like this:
  *
  * ElementStyle:
  *   Responsible for keeping track of which properties are overridden.
  *   Maintains a list of Rule objects that apply to the element.
  * Rule:
@@ -1906,791 +1901,16 @@ TextPropertyEditor.prototype = {
     } finally {
       prefs.setBoolPref("layout.css.report_errors", prefVal);
     }
     return !!style.getPropertyValue(name);
   },
 };
 
 /**
- * Mark a span editable.  |editableField| will listen for the span to
- * be focused and create an InlineEditor to handle text input.
- * Changes will be committed when the InlineEditor's input is blurred
- * or dropped when the user presses escape.
- *
- * @param {object} aOptions
- *    Options for the editable field, including:
- *    {Element} element:
- *      (required) The span to be edited on focus.
- *    {function} canEdit:
- *       Will be called before creating the inplace editor.  Editor
- *       won't be created if canEdit returns false.
- *    {function} start:
- *       Will be called when the inplace editor is initialized.
- *    {function} change:
- *       Will be called when the text input changes.  Will be called
- *       with the current value of the text input.
- *    {function} done:
- *       Called when input is committed or blurred.  Called with
- *       current value and a boolean telling the caller whether to
- *       commit the change.  This function is called before the editor
- *       has been torn down.
- *    {function} destroy:
- *       Called when the editor is destroyed and has been torn down.
- *    {string} advanceChars:
- *       If any characters in advanceChars are typed, focus will advance
- *       to the next element.
- *    {boolean} stopOnReturn:
- *       If true, the return key will not advance the editor to the next
- *       focusable element.
- *    {string} trigger: The DOM event that should trigger editing,
- *      defaults to "click"
- */
-function editableField(aOptions)
-{
-  return editableItem(aOptions, function(aElement, aEvent) {
-    new InplaceEditor(aOptions, aEvent);
-  });
-}
-
-/**
- * Handle events for an element that should respond to
- * clicks and sit in the editing tab order, and call
- * a callback when it is activated.
- *
- * @param {object} aOptions
- *    The options for this editor, including:
- *    {Element} element: The DOM element.
- *    {string} trigger: The DOM event that should trigger editing,
- *      defaults to "click"
- * @param {function} aCallback
- *        Called when the editor is activated.
- */
-this.editableItem = function editableItem(aOptions, aCallback)
-{
-  let trigger = aOptions.trigger || "click"
-  let element = aOptions.element;
-  element.addEventListener(trigger, function(evt) {
-    let win = this.ownerDocument.defaultView;
-    let selection = win.getSelection();
-    if (trigger != "click" || selection.isCollapsed) {
-      aCallback(element, evt);
-    }
-    evt.stopPropagation();
-  }, false);
-
-  // If focused by means other than a click, start editing by
-  // pressing enter or space.
-  element.addEventListener("keypress", function(evt) {
-    if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
-        evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
-      aCallback(element);
-    }
-  }, true);
-
-  // Ugly workaround - the element is focused on mousedown but
-  // the editor is activated on click/mouseup.  This leads
-  // to an ugly flash of the focus ring before showing the editor.
-  // So hide the focus ring while the mouse is down.
-  element.addEventListener("mousedown", function(evt) {
-    let cleanup = function() {
-      element.style.removeProperty("outline-style");
-      element.removeEventListener("mouseup", cleanup, false);
-      element.removeEventListener("mouseout", cleanup, false);
-    };
-    element.style.setProperty("outline-style", "none");
-    element.addEventListener("mouseup", cleanup, false);
-    element.addEventListener("mouseout", cleanup, false);
-  }, false);
-
-  // Mark the element editable field for tab
-  // navigation while editing.
-  element._editable = true;
-}
-
-this._editableField = editableField;
-
-function InplaceEditor(aOptions, aEvent)
-{
-  this.elt = aOptions.element;
-  let doc = this.elt.ownerDocument;
-  this.doc = doc;
-  this.elt.inplaceEditor = this;
-
-  this.change = aOptions.change;
-  this.done = aOptions.done;
-  this.destroy = aOptions.destroy;
-  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
-  this.multiline = aOptions.multiline || false;
-  this.stopOnReturn = !!aOptions.stopOnReturn;
-
-  this._onBlur = this._onBlur.bind(this);
-  this._onKeyPress = this._onKeyPress.bind(this);
-  this._onInput = this._onInput.bind(this);
-  this._onKeyup = this._onKeyup.bind(this);
-
-  this._createInput();
-  this._autosize();
-
-  // Pull out character codes for advanceChars, listing the
-  // characters that should trigger a blur.
-  this._advanceCharCodes = {};
-  let advanceChars = aOptions.advanceChars || '';
-  for (let i = 0; i < advanceChars.length; i++) {
-    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
-  }
-
-  // Hide the provided element and add our editor.
-  this.originalDisplay = this.elt.style.display;
-  this.elt.style.display = "none";
-  this.elt.parentNode.insertBefore(this.input, this.elt);
-
-  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
-    this.input.select();
-  }
-  this.input.focus();
-
-  this.input.addEventListener("blur", this._onBlur, false);
-  this.input.addEventListener("keypress", this._onKeyPress, false);
-  this.input.addEventListener("input", this._onInput, false);
-  this.input.addEventListener("mousedown", function(aEvt) { aEvt.stopPropagation(); }, false);
-
-  this.warning = aOptions.warning;
-  this.validate = aOptions.validate;
-
-  if (this.warning && this.validate) {
-    this.input.addEventListener("keyup", this._onKeyup, false);
-  }
-
-  if (aOptions.start) {
-    aOptions.start(this, aEvent);
-  }
-}
-
-InplaceEditor.prototype = {
-  _createInput: function InplaceEditor_createEditor()
-  {
-    this.input = this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
-    this.input.inplaceEditor = this;
-    this.input.classList.add("styleinspector-propertyeditor");
-    this.input.value = this.initial;
-
-    copyTextStyles(this.elt, this.input);
-  },
-
-  /**
-   * Get rid of the editor.
-   */
-  _clear: function InplaceEditor_clear()
-  {
-    if (!this.input) {
-      // Already cleared.
-      return;
-    }
-
-    this.input.removeEventListener("blur", this._onBlur, false);
-    this.input.removeEventListener("keypress", this._onKeyPress, false);
-    this.input.removeEventListener("keyup", this._onKeyup, false);
-    this.input.removeEventListener("oninput", this._onInput, false);
-    this._stopAutosize();
-
-    this.elt.style.display = this.originalDisplay;
-    this.elt.focus();
-
-    if (this.destroy) {
-      this.destroy();
-    }
-
-    this.elt.parentNode.removeChild(this.input);
-    this.input = null;
-
-    delete this.elt.inplaceEditor;
-    delete this.elt;
-  },
-
-  /**
-   * Keeps the editor close to the size of its input string.  This is pretty
-   * crappy, suggestions for improvement welcome.
-   */
-  _autosize: function InplaceEditor_autosize()
-  {
-    // Create a hidden, absolutely-positioned span to measure the text
-    // in the input.  Boo.
-
-    // We can't just measure the original element because a) we don't
-    // change the underlying element's text ourselves (we leave that
-    // up to the client), and b) without tweaking the style of the
-    // original element, it might wrap differently or something.
-    this._measurement = this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
-    this._measurement.className = "autosizer";
-    this.elt.parentNode.appendChild(this._measurement);
-    let style = this._measurement.style;
-    style.visibility = "hidden";
-    style.position = "absolute";
-    style.top = "0";
-    style.left = "0";
-    copyTextStyles(this.input, this._measurement);
-    this._updateSize();
-  },
-
-  /**
-   * Clean up the mess created by _autosize().
-   */
-  _stopAutosize: function InplaceEditor_stopAutosize()
-  {
-    if (!this._measurement) {
-      return;
-    }
-    this._measurement.parentNode.removeChild(this._measurement);
-    delete this._measurement;
-  },
-
-  /**
-   * Size the editor to fit its current contents.
-   */
-  _updateSize: function InplaceEditor_updateSize()
-  {
-    // Replace spaces with non-breaking spaces.  Otherwise setting
-    // the span's textContent will collapse spaces and the measurement
-    // will be wrong.
-    this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
-
-    // We add a bit of padding to the end.  Should be enough to fit
-    // any letter that could be typed, otherwise we'll scroll before
-    // we get a chance to resize.  Yuck.
-    let width = this._measurement.offsetWidth + 10;
-
-    if (this.multiline) {
-      // Make sure there's some content in the current line.  This is a hack to account
-      // for the fact that after adding a newline the <pre> doesn't grow unless there's
-      // text content on the line.
-      width += 15;
-      this._measurement.textContent += "M";
-      this.input.style.height = this._measurement.offsetHeight + "px";
-    }
-
-    this.input.style.width = width + "px";
-  },
-
-   /**
-   * Increment property values in rule view.
-   *
-   * @param {number} increment 
-   *        The amount to increase/decrease the property value.
-   * @return {bool} true if value has been incremented.
-   */
-  _incrementValue: function InplaceEditor_incrementValue(increment)
-  {
-    let value = this.input.value;
-    let selectionStart = this.input.selectionStart;
-    let selectionEnd = this.input.selectionEnd;
-
-    let newValue = this._incrementCSSValue(value, increment, selectionStart, selectionEnd);
-
-    if (!newValue) {
-      return false;
-    }
-
-    this.input.value = newValue.value;
-    this.input.setSelectionRange(newValue.start, newValue.end);
-
-    return true;
-  },
-
-  /**
-   * Increment the property value based on the property type.
-   *
-   * @param {string} value
-   *        Property value.
-   * @param {number} increment
-   *        Amount to increase/decrease the property value.
-   * @param {number} selStart
-   *        Starting index of the value.
-   * @param {number} selEnd
-   *        Ending index of the value.
-   * @return {object} object with properties 'value', 'start', and 'end'.
-   */
-  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment, selStart, 
-                                                               selEnd)
-  {
-    let range = this._parseCSSValue(value, selStart);
-    let type = (range && range.type) || "";
-    let rawValue = (range ? value.substring(range.start, range.end) : "");
-    let incrementedValue = null, selection;
-
-    if (type === "num") {
-      let newValue = this._incrementRawValue(rawValue, increment);
-      if (newValue !== null) {
-        incrementedValue = newValue;
-        selection = [0, incrementedValue.length];
-      }
-    } else if (type === "hex") {
-      let exprOffset = selStart - range.start;
-      let exprOffsetEnd = selEnd - range.start;
-      let newValue = this._incHexColor(rawValue, increment, exprOffset, exprOffsetEnd);
-      if (newValue) {
-        incrementedValue = newValue.value;
-        selection = newValue.selection;
-      }
-    } else {
-      let info;
-      if (type === "rgb" || type === "hsl") {
-        info = {};
-        let part = value.substring(range.start, selStart).split(",").length - 1;
-        if (part === 3) { // alpha
-          info.minValue = 0;
-          info.maxValue = 1;
-        } else if (type === "rgb") {
-          info.minValue = 0;
-          info.maxValue = 255;
-        } else if (part !== 0) { // hsl percentage
-          info.minValue = 0;
-          info.maxValue = 100;
-
-          // select the previous number if the selection is at the end of a percentage sign
-          if (value.charAt(selStart - 1) === "%") {
-            --selStart;
-          }
-        }
-      }
-      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
-    }
-
-    if (incrementedValue === null) {
-      return;
-    }
-
-    let preRawValue = value.substr(0, range.start);
-    let postRawValue = value.substr(range.end);
-
-    return {
-      value: preRawValue + incrementedValue + postRawValue,
-      start: range.start + selection[0],
-      end: range.start + selection[1]
-    };
-  },
-
-  /**
-   * Parses the property value and type.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} offset 
-   *        Starting index of value.
-   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
-   */
-   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
-  {
-    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
-    let start = 0;
-    let m;
-
-    // retreive values from left to right until we find the one at our offset
-    while ((m = reSplitCSS.exec(value)) &&
-          (m.index + m[0].length < offset)) {
-      value = value.substr(m.index + m[0].length);
-      start += m.index + m[0].length;
-      offset -= m.index + m[0].length;
-    }
-
-    if (!m) {
-      return;
-    }
-
-    let type;
-    if (m[1]) {
-      type = "url";
-    } else if (m[2]) {
-      type = "rgb";
-    } else if (m[3]) {
-      type = "hsl";
-    } else if (m[4]) {
-      type = "hex";
-    } else if (m[5]) {
-      type = "num";
-    }
-
-    return {
-      value: m[0],
-      start: start + m.index,
-      end: start + m.index + m[0].length,
-      type: type
-    };
-  },
-
-  /**
-   * Increment the property value for types other than
-   * number or hex, such as rgb, hsl, and file names.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} increment 
-   *        Amount to increment/decrement.
-   * @param {number} offset 
-   *        Starting index of the property value.
-   * @param {number} offsetEnd 
-   *        Ending index of the property value.
-   * @param {object} info 
-   *        Object with details about the property value.
-   * @return {object} object with properties 'value', 'start', and 'end'.
-   */
-  _incrementGenericValue: function InplaceEditor_incrementGenericValue(value, increment, offset,
-                                                                       offsetEnd, info)
-  {
-    // Try to find a number around the cursor to increment.
-    let start, end;
-    // Check if we are incrementing in a non-number context (such as a URL)
-    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
-      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
-      // We have a number selected, possibly with a suffix, and we are not in
-      // the disallowed case of just part of a known number being selected.
-      // Use that number.
-      start = offset;
-      end = offsetEnd;
-    } else {
-      // Parse periods as belonging to the number only if we are in a known number
-      // context. (This makes incrementing the 1 in 'image1.gif' work.)
-      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
-      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
-      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
-
-      start = offset - before;
-      end = offset + after;
-
-      // Expand the number to contain an initial minus sign if it seems
-      // free-standing.
-      if (value.charAt(start - 1) === "-" &&
-         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
-        --start;
-      }
-    }
-
-    if (start !== end)
-    {
-      // Include percentages as part of the incremented number (they are
-      // common enough).
-      if (value.charAt(end) === "%") {
-        ++end;
-      }
-
-      let first = value.substr(0, start);
-      let mid = value.substring(start, end);
-      let last = value.substr(end);
-
-      mid = this._incrementRawValue(mid, increment, info);
-
-      if (mid !== null) {
-        return {
-          value: first + mid + last,
-          start: start,
-          end: start + mid.length
-        };
-      }
-    }
-  },
-
-  /**
-   * Increment the property value for numbers.
-   *
-   * @param {string} rawValue 
-   *        Raw value to increment.
-   * @param {number} increment 
-   *        Amount to increase/decrease the raw value.
-   * @param {object} info 
-   *        Object with info about the property value.
-   * @return {string} the incremented value.
-   */
-  _incrementRawValue: function InplaceEditor_incrementRawValue(rawValue, increment, info)
-  {
-    let num = parseFloat(rawValue);
-
-    if (isNaN(num)) {
-      return null;
-    }
-
-    let number = /\d+(\.\d+)?/.exec(rawValue);
-    let units = rawValue.substr(number.index + number[0].length);
-
-    // avoid rounding errors
-    let newValue = Math.round((num + increment) * 1000) / 1000;
-
-    if (info && "minValue" in info) {
-      newValue = Math.max(newValue, info.minValue);
-    }
-    if (info && "maxValue" in info) {
-      newValue = Math.min(newValue, info.maxValue);
-    }
-
-    newValue = newValue.toString();
-
-    return newValue + units;
-  },
-
-  /**
-   * Increment the property value for hex.
-   *
-   * @param {string} value 
-   *        Property value.
-   * @param {number} increment 
-   *        Amount to increase/decrease the property value.
-   * @param {number} offset 
-   *        Starting index of the property value.
-   * @param {number} offsetEnd 
-   *        Ending index of the property value.
-   * @return {object} object with properties 'value' and 'selection'.
-   */
-  _incHexColor: function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
-  {
-    // Return early if no part of the rawValue is selected.
-    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
-      return;
-    }
-    if (offset < 1 && offsetEnd <= 1) {
-      return;
-    }
-    // Ignore the leading #.
-    rawValue = rawValue.substr(1);
-    --offset;
-    --offsetEnd;
-
-    // Clamp the selection to within the actual value.
-    offset = Math.max(offset, 0);
-    offsetEnd = Math.min(offsetEnd, rawValue.length);
-    offsetEnd = Math.max(offsetEnd, offset);
-
-    // Normalize #ABC -> #AABBCC.
-    if (rawValue.length === 3) {
-      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
-                 rawValue.charAt(1) + rawValue.charAt(1) +
-                 rawValue.charAt(2) + rawValue.charAt(2);
-      offset *= 2;
-      offsetEnd *= 2;
-    }
-
-    if (rawValue.length !== 6) {
-      return;
-    }
-
-    // If no selection, increment an adjacent color, preferably one to the left.
-    if (offset === offsetEnd) {
-      if (offset === 0) {
-        offsetEnd = 1;
-      } else {
-        offset = offsetEnd - 1;
-      }
-    }
-
-    // Make the selection cover entire parts.
-    offset -= offset % 2;
-    offsetEnd += offsetEnd % 2;
-
-    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
-    if (-1 < increment && increment < 1) {
-      increment = (increment < 0 ? -1 : 1);
-    }
-    if (Math.abs(increment) === 10) {
-      increment = (increment < 0 ? -16 : 16);
-    }
-
-    let isUpper = (rawValue.toUpperCase() === rawValue);
-
-    for (let pos = offset; pos < offsetEnd; pos += 2) {
-      // Increment the part in [pos, pos+2).
-      let mid = rawValue.substr(pos, 2);
-      let value = parseInt(mid, 16);
-
-      if (isNaN(value)) {
-        return;
-      }
-
-      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
-
-      while (mid.length < 2) {
-        mid = "0" + mid;
-      }
-      if (isUpper) {
-        mid = mid.toUpperCase();
-      }
-
-      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
-    }
-
-    return {
-      value: "#" + rawValue,
-      selection: [offset + 1, offsetEnd + 1]
-    };
-  },
-
-  /**
-   * Call the client's done handler and clear out.
-   */
-  _apply: function InplaceEditor_apply(aEvent)
-  {
-    if (this._applied) {
-      return;
-    }
-
-    this._applied = true;
-
-    if (this.done) {
-      let val = this.input.value.trim();
-      return this.done(this.cancelled ? this.initial : val, !this.cancelled);
-    }
-
-    return null;
-  },
-
-  /**
-   * Handle loss of focus by calling done if it hasn't been called yet.
-   */
-  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
-  {
-    this._apply();
-    if (!aDoNotClear) {
-      this._clear();
-    }
-  },
-
-  /**
-   * Handle the input field's keypress event.
-   */
-  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
-  {
-    let prevent = false;
-
-    const largeIncrement = 100;
-    const mediumIncrement = 10;
-    const smallIncrement = 0.1;
-
-    let increment = 0;
-
-    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
-      increment = 1;
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
-      increment = -1;
-    }
-
-    if (aEvent.shiftKey && !aEvent.altKey) {
-      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
-           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
-        increment *= largeIncrement;
-      } else {
-        increment *= mediumIncrement;
-      }
-    } else if (aEvent.altKey && !aEvent.shiftKey) {
-      increment *= smallIncrement;
-    }
-
-    if (increment && this._incrementValue(increment) ) {
-      this._updateSize();
-      prevent = true;
-    }
-
-    if (this.multiline &&
-        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
-        aEvent.shiftKey) {
-      prevent = false;
-    } else if (aEvent.charCode in this._advanceCharCodes
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
-       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
-      prevent = true;
-
-      let direction = FOCUS_FORWARD;
-      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
-          aEvent.shiftKey) {
-        this.cancelled = true;
-        direction = FOCUS_BACKWARD;
-      }
-      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
-        direction = null;
-      }
-
-      let input = this.input;
-
-      this._apply();
-
-      let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-      if (direction !== null && fm.focusedElement === input) {
-        // If the focused element wasn't changed by the done callback,
-        // move the focus as requested.
-        let next = moveFocus(this.doc.defaultView, direction);
-
-        // If the next node to be focused has been tagged as an editable
-        // node, send it a click event to trigger
-        if (next && next.ownerDocument === this.doc && next._editable) {
-          next.click();
-        }
-      }
-
-      this._clear();
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
-      // Cancel and blur ourselves.
-      prevent = true;
-      this.cancelled = true;
-      this._apply();
-      this._clear();
-      aEvent.stopPropagation();
-    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
-      // No need for leading spaces here.  This is particularly
-      // noticable when adding a property: it's very natural to type
-      // <name>: (which advances to the next property) then spacebar.
-      prevent = !this.input.value;
-    }
-
-    if (prevent) {
-      aEvent.preventDefault();
-    }
-  },
-
-  /**
-   * Handle the input field's keyup event.
-   */
-  _onKeyup: function(aEvent) {
-    // Validate the entered value.
-    this.warning.hidden = this.validate(this.input.value);
-    this._applied = false;
-    this._onBlur(null, true);
-  },
-
-  /**
-   * Handle changes to the input text.
-   */
-  _onInput: function InplaceEditor_onInput(aEvent)
-  {
-    // Validate the entered value.
-    if (this.warning && this.validate) {
-      this.warning.hidden = this.validate(this.input.value);
-    }
-
-    // Update size if we're autosizing.
-    if (this._measurement) {
-      this._updateSize();
-    }
-
-    // Call the user's change handler if available.
-    if (this.change) {
-      this.change(this.input.value.trim());
-    }
-  }
-};
-
-/*
- * Various API consumers (especially tests) sometimes want to grab the
- * inplaceEditor expando off span elements. However, when each global has its
- * own compartment, those expandos live on Xray wrappers that are only visible
- * within this JSM. So we provide a little workaround here.
- */
-this._getInplaceEditorForSpan = function _getInplaceEditorForSpan(aSpan)
-{
-  return aSpan.inplaceEditor;
-};
-
-/**
  * Store of CSSStyleDeclarations mapped to properties that have been changed by
  * the user.
  */
 function UserProperties()
 {
   // FIXME: This should be a WeakMap once bug 753517 is fixed.
   // See Bug 777373 for details.
   this.map = new Map();
@@ -2811,38 +2031,16 @@ function createMenuItem(aMenu, aAttribut
 /**
  * Append a text node to an element.
  */
 function appendText(aParent, aText)
 {
   aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
 }
 
-/**
- * Copy text-related styles from one element to another.
- */
-function copyTextStyles(aFrom, aTo)
-{
-  let win = aFrom.ownerDocument.defaultView;
-  let style = win.getComputedStyle(aFrom);
-  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
-  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
-  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
-  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
-}
-
-/**
- * Trigger a focus change similar to pressing tab/shift-tab.
- */
-function moveFocus(aWin, aDirection)
-{
-  let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
-  return fm.moveFocus(aWin, null, aDirection, 0);
-}
-
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
 
 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
   return Services.strings.createBundle(
     "chrome://browser/locale/devtools/styleinspector.properties");
--- a/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
+++ b/browser/devtools/styleinspector/test/browser_bug722691_rule_view_increment.js
@@ -4,18 +4,16 @@
 
 // Test that increasing/decreasing values in rule view using
 // arrow keys works correctly.
 
 let tempScope = {};
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 function setUpTests()
 {
   doc.body.innerHTML = '<div id="test" style="' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
@@ -1,16 +1,13 @@
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 let doc;
-let tempScope = {};
-Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 let inspector;
 let win;
 
 XPCOMUtils.defineLazyGetter(this, "osString", function() {
   return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
 });
 
 function createDocument()
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor.js
@@ -1,18 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc = content.document;
 
 function expectDone(aValue, aCommit, aNext)
 {
   return function(aDoneValue, aDoneCommit) {
     dump("aDoneValue: " + aDoneValue + " commit: " + aDoneCommit + "\n");
 
@@ -35,50 +33,50 @@ function createSpan()
   doc.body.appendChild(span);
   return span;
 }
 
 function testReturnCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     initial: "explicit initial",
     start: function() {
       is(inplaceEditor(span).input.value, "explicit initial", "Explicit initial value should be used.");
       inplaceEditor(span).input.value = "Test Value";
       EventUtils.sendKey("return");
     },
     done: expectDone("Test Value", true, testBlurCommit)
   });
   span.click();
 }
 
 function testBlurCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     start: function() {
       is(inplaceEditor(span).input.value, "Edit Me!", "textContent of the span used.");
       inplaceEditor(span).input.value = "Test Value";
       inplaceEditor(span).input.blur();
     },
     done: expectDone("Test Value", true, testAdvanceCharCommit)
   });
   span.click();
 }
 
 function testAdvanceCharCommit()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     advanceChars: ":",
     start: function() {
       let input = inplaceEditor(span).input;
       for each (let ch in "Test:") {
         EventUtils.sendChar(ch);
       }
     },
@@ -86,17 +84,17 @@ function testAdvanceCharCommit()
   });
   span.click();
 }
 
 function testEscapeCancel()
 {
   clearBody();
   let span = createSpan();
-  _editableField({
+  editableField({
     element: span,
     initial: "initial text",
     start: function() {
       inplaceEditor(span).input.value = "Test Value";
       EventUtils.sendKey("escape");
     },
     done: expectDone("initial text", false, finishTest)
   });
--- a/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_editor_changedvalues.js
@@ -1,18 +1,16 @@
 /* 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 tempScope = {};
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 var gRuleViewChanged = false;
 function ruleViewChanged()
 {
@@ -52,30 +50,28 @@ function startTest()
     waitForFocus(testCancelNew, ruleDialog);
   }, true);
 }
 
 function testCancelNew()
 {
   // Start at the beginning: start to add a rule to the element's style
   // declaration, but leave it empty.
-
   let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
   waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
     is(inplaceEditor(elementRuleEditor.newPropSpan), aEditor, "Next focused editor should be the new property editor.");
     let input = aEditor.input;
     waitForEditorBlur(aEditor, function () {
       ok(!gRuleViewChanged, "Shouldn't get a change event after a cancel.");
       is(elementRuleEditor.rule.textProps.length,  0, "Should have canceled creating a new text property.");
       ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
       testCreateNew();
     });
     aEditor.input.blur();
   });
-
   EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
                              { },
                              ruleDialog);
 }
 
 function testCreateNew()
 {
   // Create a new property.
--- a/browser/devtools/styleinspector/test/browser_ruleview_focus.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_focus.js
@@ -1,18 +1,15 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that focus doesn't leave the style editor when adding a property
 // (bug 719916)
 
-let tempScope = {};
-Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 let doc;
 let inspector;
 let stylePanel;
 
 function openRuleView()
 {
   var target = TargetFactory.forTab(gBrowser.selectedTab);
   gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
--- a/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_inherit.js
@@ -1,17 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleInherit()
 {
   let style = '' +
     '#test2 {' +
     '  background-color: green;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_manipulation.js
@@ -1,17 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleOverride()
 {
   doc.body.innerHTML = '<div id="testid">Styled Node</div>';
   let element = doc.getElementById("testid");
   let elementStyle = new _ElementStyle(element);
--- a/browser/devtools/styleinspector/test/browser_ruleview_override.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_override.js
@@ -1,17 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
 
 let doc;
 
 function simpleOverride()
 {
   let style = '' +
     '#testid {' +
     '  background-color: blue;' +
--- a/browser/devtools/styleinspector/test/browser_ruleview_ui.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_ui.js
@@ -1,18 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 
 var gRuleViewChanged = false;
 function ruleViewChanged()
 {
--- a/browser/devtools/styleinspector/test/browser_ruleview_update.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_update.js
@@ -1,18 +1,16 @@
 /* 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 tempScope = {}
 Cu.import("resource:///modules/devtools/CssRuleView.jsm", tempScope);
 let CssRuleView = tempScope.CssRuleView;
 let _ElementStyle = tempScope._ElementStyle;
-let _editableField = tempScope._editableField;
-let inplaceEditor = tempScope._getInplaceEditorForSpan;
 
 let doc;
 let ruleDialog;
 let ruleView;
 let testElement;
 
 function startTest()
 {
--- a/browser/devtools/styleinspector/test/head.js
+++ b/browser/devtools/styleinspector/test/head.js
@@ -8,16 +8,20 @@ Cu.import("resource:///modules/devtools/
 Cu.import("resource:///modules/devtools/CssHtmlTree.jsm", tempScope);
 Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope);
 let ConsoleUtils = tempScope.ConsoleUtils;
 let CssLogic = tempScope.CssLogic;
 let CssHtmlTree = tempScope.CssHtmlTree;
 let gDevTools = tempScope.gDevTools;
 Cu.import("resource:///modules/devtools/Target.jsm", tempScope);
 let TargetFactory = tempScope.TargetFactory;
+let {
+  editableField,
+  getInplaceEditorForSpan: inplaceEditor
+} = Cu.import("resource:///modules/devtools/InplaceEditor.jsm", {});
 Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope);
 let console = tempScope.console;
 
 let browser, hudId, hud, hudBox, filterBox, outputNode, cs;
 
 function addTab(aURL)
 {
   gBrowser.selectedTab = gBrowser.addTab();
--- a/browser/devtools/tilt/TiltUtils.jsm
+++ b/browser/devtools/tilt/TiltUtils.jsm
@@ -8,16 +8,18 @@
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 
+const STACK_THICKNESS = 15;
+
 this.EXPORTED_SYMBOLS = ["TiltUtils"];
 
 /**
  * Module containing various helper functions used throughout Tilt.
  */
 this.TiltUtils = {};
 
 /**
@@ -373,85 +375,124 @@ TiltUtils.DOM = {
   {
     return {
       width: aContentWindow.innerWidth + aContentWindow.scrollMaxX,
       height: aContentWindow.innerHeight + aContentWindow.scrollMaxY
     };
   },
 
   /**
+   * Calculates the position and depth to display a node, this can be overriden
+   * to change the visualization.
+   *
+   * @param {Window} aContentWindow
+   *                 the window content holding the document
+   * @param {Node}   aNode
+   *                 the node to get the position for
+   * @param {Object} aParentPosition
+   *                 the position of the parent node, as returned by this
+   *                 function
+   *
+   * @return {Object} an object describing the node's position in 3D space
+   *                  containing the following properties:
+   *         {Number} top
+   *                  distance along the x axis
+   *         {Number} left
+   *                  distance along the y axis
+   *         {Number} depth
+   *                  distance along the z axis
+   *         {Number} width
+   *                  width of the node
+   *         {Number} height
+   *                  height of the node
+   *         {Number} thickness
+   *                  thickness of the node
+   */
+  getNodePosition: function TUD_getNodePosition(aContentWindow, aNode,
+                                                aParentPosition) {
+    // get the x, y, width and height coordinates of the node
+    let coord = LayoutHelpers.getRect(aNode, aContentWindow);
+    if (!coord) {
+      return null;
+    }
+
+    coord.depth = aParentPosition ? (aParentPosition.depth + aParentPosition.thickness) : 0;
+    coord.thickness = STACK_THICKNESS;
+
+    return coord;
+  },
+
+  /**
    * Traverses a document object model & calculates useful info for each node.
    *
    * @param {Window} aContentWindow
    *                 the window content holding the document
    * @param {Object} aProperties
    *                 optional, an object containing the following properties:
    *        {Object} invisibleElements
    *                 elements which should be ignored
    *        {Number} minSize
    *                 the minimum dimensions needed for a node to be traversed
    *        {Number} maxX
    *                 the maximum left position of an element
    *        {Number} maxY
    *                 the maximum top position of an element
    *
-   * @return {Array} list containing nodes depths, coordinates and local names
+   * @return {Array} list containing nodes positions and local names
    */
   traverse: function TUD_traverse(aContentWindow, aProperties)
   {
     // make sure the properties parameter is a valid object
     aProperties = aProperties || {};
 
     let aInvisibleElements = aProperties.invisibleElements || {};
     let aMinSize = aProperties.minSize || -1;
     let aMaxX = aProperties.maxX || Number.MAX_VALUE;
     let aMaxY = aProperties.maxY || Number.MAX_VALUE;
 
     let nodes = aContentWindow.document.childNodes;
     let store = { info: [], nodes: [] };
     let depth = 0;
 
-    while (nodes.length) {
-      let queue = [];
+    let queue = [
+      { parentPosition: null, nodes: aContentWindow.document.childNodes }
+    ]
 
-      for (let i = 0, len = nodes.length; i < len; i++) {
-        let node = nodes[i];
+    while (queue.length) {
+      let { nodes, parentPosition } = queue.shift();
 
+      for (let node of nodes) {
         // skip some nodes to avoid visualization meshes that are too bloated
         let name = node.localName;
         if (!name || aInvisibleElements[name]) {
           continue;
         }
 
-        // get the x, y, width and height coordinates of the node
-        let coord = LayoutHelpers.getRect(node, aContentWindow);
+        let coord = this.getNodePosition(aContentWindow, node, parentPosition);
         if (!coord) {
           continue;
         }
 
         // the maximum size slices the traversal where needed
         if (coord.left > aMaxX || coord.top > aMaxY) {
           continue;
         }
 
         // use this node only if it actually has visible dimensions
         if (coord.width > aMinSize && coord.height > aMinSize) {
 
           // save the necessary details into a list to be returned later
-          store.info.push({ depth: depth, coord: coord, name: name });
+          store.info.push({ coord: coord, name: name });
           store.nodes.push(node);
         }
 
-        // prepare the queue array
-        Array.prototype.push.apply(queue, name === "iframe" || name === "frame" ?
-                                          node.contentDocument.childNodes :
-                                          node.childNodes);
+        let childNodes = (name === "iframe" || name === "frame") ? node.contentDocument.childNodes : node.childNodes;
+        if (childNodes.length > 0)
+          queue.push({ parentPosition: coord, nodes: childNodes });
       }
-      nodes = queue;
-      depth++;
     }
 
     return store;
   }
 };
 
 /**
  * Binds a new owner object to the child functions.
--- a/browser/devtools/tilt/TiltVisualizer.jsm
+++ b/browser/devtools/tilt/TiltVisualizer.jsm
@@ -24,17 +24,16 @@ const INVISIBLE_ELEMENTS = {
 
 // a node is represented in the visualization mesh as a rectangular stack
 // of 5 quads composed of 12 vertices; we draw these as triangles using an
 // index buffer of 12 unsigned int elements, obviously one for each vertex;
 // if a webpage has enough nodes to overflow the index buffer elements size,
 // weird things may happen; thus, when necessary, we'll split into groups
 const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
 
-const STACK_THICKNESS = 15;
 const WIREFRAME_COLOR = [0, 0, 0, 0.25];
 const INTRO_TRANSITION_DURATION = 1000;
 const OUTRO_TRANSITION_DURATION = 800;
 const INITIAL_Z_TRANSLATION = 400;
 const MOVE_INTO_VIEW_ACCURACY = 50;
 
 const MOUSE_CLICK_THRESHOLD = 10;
 const MOUSE_INTRO_DELAY = 200;
@@ -730,17 +729,16 @@ TiltVisualizer.Presenter.prototype = {
     worker.addEventListener("message", function TVP_onMessage(event) {
       this._setupMesh(event.data);
     }.bind(this), false);
 
     // calculate necessary information regarding vertices, texture coordinates
     // etc. in a separate thread, as this process may take a while
     worker.postMessage({
       maxGroupNodes: MAX_GROUP_NODES,
-      thickness: STACK_THICKNESS,
       style: TiltVisualizerStyle.nodes,
       texWidth: this._texture.width,
       texHeight: this._texture.height,
       nodesInfo: this._traverseData.info
     });
   },
 
   /**
@@ -865,22 +863,22 @@ TiltVisualizer.Presenter.prototype = {
     highlight.fill = style[info.name] || style.highlight.defaultFill;
     highlight.stroke = style.highlight.defaultStroke;
     highlight.strokeWeight = style.highlight.defaultStrokeWeight;
 
     let x = info.coord.left;
     let y = info.coord.top;
     let w = info.coord.width;
     let h = info.coord.height;
-    let z = info.depth;
+    let z = info.coord.depth + info.coord.thickness;
 
-    vec3.set([x,     y,     z * STACK_THICKNESS], highlight.v0);
-    vec3.set([x + w, y,     z * STACK_THICKNESS], highlight.v1);
-    vec3.set([x + w, y + h, z * STACK_THICKNESS], highlight.v2);
-    vec3.set([x,     y + h, z * STACK_THICKNESS], highlight.v3);
+    vec3.set([x,     y,     z], highlight.v0);
+    vec3.set([x + w, y,     z], highlight.v1);
+    vec3.set([x + w, y + h, z], highlight.v2);
+    vec3.set([x,     y + h, z], highlight.v3);
 
     this._currentSelection = aNodeIndex;
 
     // if something is highlighted, make sure it's inside the current viewport;
     // the point which should be moved into view is considered the center [x, y]
     // position along the top edge of the currently selected node
 
     if (aFlags && aFlags.indexOf("moveIntoView") !== -1)
@@ -967,17 +965,16 @@ TiltVisualizer.Presenter.prototype = {
     let height = this._renderer.height * zoom;
     x *= zoom;
     y *= zoom;
 
     // create a ray following the mouse direction from the near clipping plane
     // to the far clipping plane, to check for intersections with the mesh,
     // and do all the heavy lifting in a separate thread
     worker.postMessage({
-      thickness: STACK_THICKNESS,
       vertices: this._meshData.allVertices,
 
       // create the ray destined for 3D picking
       ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height],
         this._meshStacks.mvMatrix,
         this._meshStacks.projMatrix)
     });
   },
--- a/browser/devtools/tilt/TiltWorkerCrafter.js
+++ b/browser/devtools/tilt/TiltWorkerCrafter.js
@@ -1,28 +1,27 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 /**
- * Given the initialization data (thickness, sizes and information about
+ * Given the initialization data (sizes and information about
  * each DOM node) this worker sends back the arrays representing
  * vertices, texture coords, colors, indices and all the needed data for
  * rendering the DOM visualization mesh.
  *
  * Used in the TiltVisualization.Presenter object.
  */
 self.onmessage = function TWC_onMessage(event)
 {
   let data = event.data;
   let maxGroupNodes = parseInt(data.maxGroupNodes);
-  let thickness = data.thickness;
   let style = data.style;
   let texWidth = data.texWidth;
   let texHeight = data.texHeight;
   let nodesInfo = data.nodesInfo;
 
   let mesh = {
     allVertices: [],
     groups: [],
@@ -50,21 +49,20 @@ self.onmessage = function TWC_onMessage(
       texCoord = [];
       color = [];
       stacksIndices = [];
       wireframeIndices = [];
       index = 0;
     }
 
     let info = nodesInfo[n];
-    let depth = info.depth;
     let coord = info.coord;
 
     // calculate the stack x, y, z, width and height coordinates
-    let z = depth * thickness;
+    let z = coord.depth + coord.thickness;
     let y = coord.top;
     let x = coord.left;
     let w = coord.width;
     let h = coord.height;
 
     // the maximum texture size slices the visualization mesh where needed
     if (x + w > texWidth) {
       w = texWidth - x;
@@ -75,17 +73,17 @@ self.onmessage = function TWC_onMessage(
 
     x += self.random.next();
     y += self.random.next();
     w -= self.random.next() * 0.1;
     h -= self.random.next() * 0.1;
 
     let xpw = x + w;
     let yph = y + h;
-    let zmt = z - thickness;
+    let zmt = coord.depth;
 
     let xotw = x / texWidth;
     let yoth = y / texHeight;
     let xpwotw = xpw / texWidth;
     let yphoth = yph / texHeight;
 
     // calculate the margin fill color
     let fill = style[info.name] || style.highlight.defaultFill;
@@ -152,17 +150,17 @@ self.onmessage = function TWC_onMessage(
     // compute the stack indices
     stacksIndices.unshift(i,    ip1,  ip2,  i,    ip2,  ip3,
                           ip8,  ip9,  ip5,  ip8,  ip5,  ip4,
                           ip7,  ip6,  ip10, ip7,  ip10, ip11,
                           ip8,  ip4,  ip7,  ip8,  ip7,  ip11,
                           ip5,  ip9,  ip10, ip5,  ip10, ip6);
 
     // compute the wireframe indices
-    if (depth !== 0) {
+    if (coord.thickness !== 0) {
       wireframeIndices.unshift(i,    ip1, ip1,  ip2,
                                ip2,  ip3, ip3,  i,
                                ip8,  i,   ip9,  ip1,
                                ip11, ip3, ip10, ip2);
     }
 
     // there are 12 vertices in a stack representing a node
     index += 12;
--- a/browser/devtools/tilt/TiltWorkerPicker.js
+++ b/browser/devtools/tilt/TiltWorkerPicker.js
@@ -9,17 +9,16 @@
  * This worker handles picking, given a set of vertices and a ray (calculates
  * the intersection points and offers back information about the closest hit).
  *
  * Used in the TiltVisualization.Presenter object.
  */
 self.onmessage = function TWP_onMessage(event)
 {
   let data = event.data;
-  let thickness = data.thickness;
   let vertices = data.vertices;
   let ray = data.ray;
 
   let intersection = null;
   let hit = [];
 
   // calculates the squared distance between two points
   function dsq(p1, p2) {
@@ -30,26 +29,26 @@ self.onmessage = function TWP_onMessage(
     return xd * xd + yd * yd + zd * zd;
   }
 
   // check each stack face in the visualization mesh for intersections with
   // the mouse ray (using a ray picking algorithm)
   for (let i = 0, len = vertices.length; i < len; i += 36) {
 
     // the front quad
-    let v0f = [vertices[i],     vertices[i + 1],  vertices[i + 2]];
-    let v1f = [vertices[i + 3], vertices[i + 4],  vertices[i + 5]];
-    let v2f = [vertices[i + 6], vertices[i + 7],  vertices[i + 8]];
-    let v3f = [vertices[i + 9], vertices[i + 10], vertices[i + 11]];
+    let v0f = [vertices[i],      vertices[i + 1],  vertices[i + 2]];
+    let v1f = [vertices[i + 3],  vertices[i + 4],  vertices[i + 5]];
+    let v2f = [vertices[i + 6],  vertices[i + 7],  vertices[i + 8]];
+    let v3f = [vertices[i + 9],  vertices[i + 10], vertices[i + 11]];
 
     // the back quad
-    let v0b = [v0f[0], v0f[1], v0f[2] - thickness];
-    let v1b = [v1f[0], v1f[1], v1f[2] - thickness];
-    let v2b = [v2f[0], v2f[1], v2f[2] - thickness];
-    let v3b = [v3f[0], v3f[1], v3f[2] - thickness];
+    let v0b = [vertices[i + 24], vertices[i + 25], vertices[i + 26]];
+    let v1b = [vertices[i + 27], vertices[i + 28], vertices[i + 29]];
+    let v2b = [vertices[i + 30], vertices[i + 31], vertices[i + 32]];
+    let v3b = [vertices[i + 33], vertices[i + 34], vertices[i + 35]];
 
     // don't do anything with degenerate quads
     if (!v0f[0] && !v1f[0] && !v2f[0] && !v3f[0]) {
       continue;
     }
 
     // for each triangle in the stack box, check for the intersections
     if (self.intersect(v0f, v1f, v2f, ray, hit) || // front left
--- a/browser/devtools/tilt/test/browser_tilt_utils05.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+const STACK_THICKNESS = 15;
+
 function init(callback) {
   let iframe = gBrowser.ownerDocument.createElement("iframe");
 
   iframe.addEventListener("load", function onLoad() {
     iframe.removeEventListener("load", onLoad, true);
     callback(iframe);
 
     gBrowser.parentNode.removeChild(iframe);
@@ -66,42 +68,31 @@ function test() {
     is(nodeCoordinates.width, 123,
       "The node coordinates width value wasn't calculated correctly.");
     is(nodeCoordinates.height, 456,
       "The node coordinates height value wasn't calculated correctly.");
 
 
     let store = dom.traverse(iframe.contentWindow);
 
-    is(store.nodes.length, 7,
+    let expected = [
+      { name: "html",   depth: 0 * STACK_THICKNESS },
+      { name: "head",   depth: 1 * STACK_THICKNESS },
+      { name: "body",   depth: 1 * STACK_THICKNESS },
+      { name: "style",  depth: 2 * STACK_THICKNESS },
+      { name: "script", depth: 2 * STACK_THICKNESS },
+      { name: "div",    depth: 2 * STACK_THICKNESS },
+      { name: "span",   depth: 3 * STACK_THICKNESS },
+    ];
+
+    is(store.nodes.length, expected.length,
       "The traverse() function didn't walk the correct number of nodes.");
-    is(store.info.length, 7,
+    is(store.info.length, expected.length,
       "The traverse() function didn't examine the correct number of nodes.");
-    is(store.info[0].name, "html",
-      "the 1st traversed node isn't the expected one.");
-    is(store.info[0].depth, 0,
-      "the 1st traversed node doesn't have the expected depth.");
-    is(store.info[1].name, "head",
-      "the 2nd traversed node isn't the expected one.");
-    is(store.info[1].depth, 1,
-      "the 2nd traversed node doesn't have the expected depth.");
-    is(store.info[2].name, "body",
-      "the 3rd traversed node isn't the expected one.");
-    is(store.info[2].depth, 1,
-      "the 3rd traversed node doesn't have the expected depth.");
-    is(store.info[3].name, "style",
-      "the 4th traversed node isn't the expected one.");
-    is(store.info[3].depth, 2,
-      "the 4th traversed node doesn't have the expected depth.");
-    is(store.info[4].name, "script",
-      "the 5th traversed node isn't the expected one.");
-    is(store.info[4].depth, 2,
-      "the 5th traversed node doesn't have the expected depth.");
-    is(store.info[5].name, "div",
-      "the 6th traversed node isn't the expected one.");
-    is(store.info[5].depth, 2,
-      "the 6th traversed node doesn't have the expected depth.");
-    is(store.info[6].name, "span",
-      "the 7th traversed node isn't the expected one.");
-    is(store.info[6].depth, 3,
-      "the 7th traversed node doesn't have the expected depth.");
+
+    for (let i = 0; i < expected.length; i++) {
+      is(store.info[i].name, expected[i].name,
+        "traversed node " + (i + 1) + " isn't the expected one.");
+      is(store.info[i].coord.depth, expected[i].depth,
+        "traversed node " + (i + 1) + " doesn't have the expected depth.");
+    }
   });
 }
--- a/browser/devtools/tilt/test/browser_tilt_utils07.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils07.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
+const STACK_THICKNESS = 15;
+
 function init(callback) {
   let iframe = gBrowser.ownerDocument.createElement("iframe");
 
   iframe.addEventListener("load", function onLoad() {
     iframe.removeEventListener("load", onLoad, true);
     callback(iframe);
 
     gBrowser.parentNode.removeChild(iframe);
@@ -115,78 +117,40 @@ function test() {
     is(nodeCoordinates.width, 123,
       "The node coordinates width value wasn't calculated correctly.");
     is(nodeCoordinates.height, 456,
       "The node coordinates height value wasn't calculated correctly.");
 
 
     let store = dom.traverse(iframe.contentWindow);
 
-    is(store.nodes.length, 16,
+    let expected = [
+      { name: "html",   depth: 0 * STACK_THICKNESS },
+      { name: "head",   depth: 1 * STACK_THICKNESS },
+      { name: "body",   depth: 1 * STACK_THICKNESS },
+      { name: "div",    depth: 2 * STACK_THICKNESS },
+      { name: "span",   depth: 2 * STACK_THICKNESS },
+      { name: "iframe", depth: 2 * STACK_THICKNESS },
+      { name: "span",   depth: 2 * STACK_THICKNESS },
+      { name: "iframe", depth: 2 * STACK_THICKNESS },
+      { name: "html",   depth: 3 * STACK_THICKNESS },
+      { name: "html",   depth: 3 * STACK_THICKNESS },
+      { name: "head",   depth: 4 * STACK_THICKNESS },
+      { name: "body",   depth: 4 * STACK_THICKNESS },
+      { name: "head",   depth: 4 * STACK_THICKNESS },
+      { name: "body",   depth: 4 * STACK_THICKNESS },
+      { name: "span",   depth: 5 * STACK_THICKNESS },
+      { name: "div",    depth: 5 * STACK_THICKNESS },
+    ];
+
+    is(store.nodes.length, expected.length,
       "The traverse() function didn't walk the correct number of nodes.");
-    is(store.info.length, 16,
+    is(store.info.length, expected.length,
       "The traverse() function didn't examine the correct number of nodes.");
-    is(store.info[0].name, "html",
-      "the 1st traversed node isn't the expected one.");
-    is(store.info[0].depth, 0,
-      "the 1st traversed node doesn't have the expected depth.");
-    is(store.info[1].name, "head",
-      "the 2nd traversed node isn't the expected one.");
-    is(store.info[1].depth, 1,
-      "the 2nd traversed node doesn't have the expected depth.");
-    is(store.info[2].name, "body",
-      "the 3rd traversed node isn't the expected one.");
-    is(store.info[2].depth, 1,
-      "the 3rd traversed node doesn't have the expected depth.");
-    is(store.info[3].name, "div",
-      "the 4th traversed node isn't the expected one.");
-    is(store.info[3].depth, 2,
-      "the 4th traversed node doesn't have the expected depth.");
-    is(store.info[4].name, "span",
-      "the 5th traversed node isn't the expected one.");
-    is(store.info[4].depth, 2,
-      "the 5th traversed node doesn't have the expected depth.");
-    is(store.info[5].name, "iframe",
-      "the 6th traversed node isn't the expected one.");
-    is(store.info[5].depth, 2,
-      "the 6th traversed node doesn't have the expected depth.");
-    is(store.info[6].name, "span",
-      "the 7th traversed node isn't the expected one.");
-    is(store.info[6].depth, 2,
-      "the 7th traversed node doesn't have the expected depth.");
-    is(store.info[7].name, "iframe",
-      "the 8th traversed node isn't the expected one.");
-    is(store.info[7].depth, 2,
-      "the 8th traversed node doesn't have the expected depth.");
-    is(store.info[8].name, "html",
-      "the 9th traversed node isn't the expected one.");
-    is(store.info[8].depth, 3,
-      "the 9th traversed node doesn't have the expected depth.");
-    is(store.info[9].name, "html",
-      "the 10th traversed node isn't the expected one.");
-    is(store.info[9].depth, 3,
-      "the 10th traversed node doesn't have the expected depth.");
-    is(store.info[10].name, "head",
-      "the 11th traversed node isn't the expected one.");
-    is(store.info[10].depth, 4,
-      "the 11th traversed node doesn't have the expected depth.");
-    is(store.info[11].name, "body",
-      "the 12th traversed node isn't the expected one.");
-    is(store.info[11].depth, 4,
-      "the 12th traversed node doesn't have the expected depth.");
-    is(store.info[12].name, "head",
-      "the 13th traversed node isn't the expected one.");
-    is(store.info[12].depth, 4,
-      "the 13th traversed node doesn't have the expected depth.");
-    is(store.info[13].name, "body",
-      "the 14th traversed node isn't the expected one.");
-    is(store.info[13].depth, 4,
-      "the 14th traversed node doesn't have the expected depth.");
-    is(store.info[14].name, "span",
-      "the 15th traversed node isn't the expected one.");
-    is(store.info[14].depth, 5,
-      "the 15th traversed node doesn't have the expected depth.");
-    is(store.info[15].name, "div",
-      "the 16th traversed node isn't the expected one.");
-    is(store.info[15].depth, 5,
-      "the 16th traversed node doesn't have the expected depth.");
+
+    for (let i = 0; i < expected.length; i++) {
+      is(store.info[i].name, expected[i].name,
+        "traversed node " + (i + 1) + " isn't the expected one.");
+      is(store.info[i].coord.depth, expected[i].depth,
+        "traversed node " + (i + 1) + " doesn't have the expected depth.");
+    }
   });
 }
--- a/browser/themes/linux/devtools/toolbox.css
+++ b/browser/themes/linux/devtools/toolbox.css
@@ -145,36 +145,37 @@
 }
 
 #toolbox-tabs {
   margin: 0;
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  width: 47px;
-  min-width: 47px;
+  min-width: 32px;
   min-height: 32px;
-  max-width: 137px;
+  max-width: 127px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-right: 1px solid hsla(206,37%,4%,.45);
+  -moz-box-align: center;
 }
 
 .devtools-tab > image {
   border: none;
-  -moz-margin-end: 6px;
-  -moz-margin-start: 16px;
+  -moz-margin-end: 0;
+  -moz-margin-start: 8px;
   opacity: 0.6;
+  max-height: 16px;
 }
 
 .devtools-tab > label {
   white-space: nowrap;
 }
 
 .devtools-tab:hover > image {
   opacity: 0.8;
--- a/browser/themes/linux/devtools/widgets.css
+++ b/browser/themes/linux/devtools/widgets.css
@@ -97,16 +97,17 @@
   transform: scaleX(-1);
 }
 
 .breadcrumbs-widget-item {
   background-color: transparent;
   -moz-appearance: none;
   overflow: hidden;
   min-width: 85px;
+  max-width: 250px;
   min-height: 25px;
   border-style: solid;
   border-width: 1px 13px 2px 13px;
   margin: 0 -11px 0 0;
   padding: 0 9px;
   outline: none;
   color: hsl(210,30%,85%);
 }
--- a/browser/themes/osx/devtools/toolbox.css
+++ b/browser/themes/osx/devtools/toolbox.css
@@ -134,34 +134,34 @@
 
 #toolbox-tabs {
   margin: 0;
   border-left: 1px solid hsla(206,37%,4%,.45);
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  width: 47px;
-  min-width: 47px;
+  min-width: 32px;
   min-height: 32px;
-  max-width: 137px;
+  max-width: 110px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-right: 1px solid hsla(206,37%,4%,.45);
+  -moz-box-align: center;
 }
 
 .devtools-tab > image {
-  -moz-margin-end: 6px;
-  -moz-margin-start: 16px;
+  -moz-margin-end: 0;
+  -moz-margin-start: 8px;
   opacity: 0.6;
 }
 
 .devtools-tab > label {
   white-space: nowrap;
 }
 
 .devtools-tab:hover > image {
--- a/browser/themes/osx/devtools/widgets.css
+++ b/browser/themes/osx/devtools/widgets.css
@@ -97,16 +97,17 @@
   transform: scaleX(-1);
 }
 
 .breadcrumbs-widget-item {
   background-color: transparent;
   -moz-appearance: none;
   overflow: hidden;
   min-width: 85px;
+  max-width: 250px;
   min-height: 25px;
   border-style: solid;
   border-width: 1px 13px 2px 13px;
   margin: 0 -11px 0 0;
   padding: 0 9px;
   outline: none;
   color: hsl(210,30%,85%);
 }
--- a/browser/themes/windows/devtools/toolbox.css
+++ b/browser/themes/windows/devtools/toolbox.css
@@ -146,35 +146,35 @@
 }
 
 #toolbox-tabs {
   margin: 0;
 }
 
 .devtools-tab {
   -moz-appearance: none;
-  width: 47px;
-  min-width: 47px;
+  min-width: 32px;
   min-height: 32px;
-  max-width: 137px;
+  max-width: 110px;
   color: #b6babf;
   margin: 0;
   padding: 0;
   background-image: linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1)),
                     linear-gradient(hsla(204,45%,98%,.05), hsla(204,45%,98%,.1));
   background-size: 1px 100%;
   background-repeat: no-repeat;
   background-position: left, right;
   border-top: 1px solid #060a0d;
   border-right: 1px solid hsla(206,37%,4%,.45);
+  -moz-box-align: center;
 }
 
 .devtools-tab > image {
-  -moz-margin-end: 6px;
-  -moz-margin-start: 16px;
+  -moz-margin-end: 0;
+  -moz-margin-start: 8px;
   opacity: 0.6;
 }
 
 .devtools-tab:hover > image {
   opacity: 0.8;
 }
 
 .devtools-tab:active > image,
--- a/browser/themes/windows/devtools/widgets.css
+++ b/browser/themes/windows/devtools/widgets.css
@@ -97,16 +97,17 @@
   transform: scaleX(-1);
 }
 
 .breadcrumbs-widget-item {
   background-color: transparent;
   -moz-appearance: none;
   overflow: hidden;
   min-width: 85px;
+  max-width: 250px;
   min-height: 25px;
   border-style: solid;
   border-width: 2px 13px;
   margin: 0 -11px 0 0;
   padding: 0 9px;
   outline: none;
   color: hsl(210,30%,85%);
 }
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -2484,16 +2484,23 @@
   },
   "FX_SESSION_RESTORE_WRITE_FILE_MS": {
     "kind": "exponential",
     "high": "3000",
     "n_buckets": 10,
     "extended_statistics_ok": true,
     "description": "Session restore: Time to write the session data to the file on disk (ms)"
   },
+  "FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS": {
+    "kind": "exponential",
+    "high": "3000",
+    "n_buckets": 10,
+    "extended_statistics_ok": true,
+    "description": "Session restore: Duration of the longest uninterruptible operation while writing session data (ms)"
+  },
   "FX_SESSION_RESTORE_CORRUPT_FILE": {
     "kind": "boolean",
     "description": "Session restore: Whether the file read on startup contained parse-able JSON"
   },
   "FX_SESSION_RESTORE_BACKUP_FILE_MS": {
     "kind": "exponential",
     "high": "30000",
     "n_buckets": 10,
--- a/toolkit/components/thumbnails/PageThumbs.jsm
+++ b/toolkit/components/thumbnails/PageThumbs.jsm
@@ -239,17 +239,24 @@ this.PageThumbs = {
    * @param aWindow The content window.
    * @param aCanvas The target canvas.
    * @return An array containing width, height and scale.
    */
   _determineCropSize: function PageThumbs_determineCropSize(aWindow, aCanvas) {
     let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDOMWindowUtils);
     let sbWidth = {}, sbHeight = {};
-    utils.getScrollbarSize(false, sbWidth, sbHeight);
+
+    try {
+      utils.getScrollbarSize(false, sbWidth, sbHeight);
+    } catch (e) {
+      // This might fail if the window does not have a presShell.
+      Cu.reportError("Unable to get scrollbar size in _determineCropSize.");
+      sbWidth.value = sbHeight.value = 0;
+    }
 
     // Even in RTL mode, scrollbars are always on the right.
     // So there's no need to determine a left offset.
     let sw = aWindow.innerWidth - sbWidth.value;
     let sh = aWindow.innerHeight - sbHeight.value;
 
     let {width: thumbnailWidth, height: thumbnailHeight} = aCanvas;
     let scale = Math.min(Math.max(thumbnailWidth / sw, thumbnailHeight / sh), 1);
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -28,36 +28,16 @@ function ThreadActor(aHooks, aGlobal)
 {
   this._state = "detached";
   this._frameActors = [];
   this._environmentActors = [];
   this._hooks = aHooks;
   this._sources = {};
   this.global = aGlobal;
 
-  /**
-   * A script cache that maps script URLs to arrays of different Debugger.Script
-   * instances that have the same URL. For example, when an inline <script> tag
-   * in a web page contains a function declaration, the JS engine creates two
-   * Debugger.Script objects, one for the function and one for the script tag
-   * as a whole. The two objects will usually have different startLine and/or
-   * lineCount properties. For the edge case where two scripts are contained in
-   * the same line we need column support.
-   *
-   * The sparse array that is mapped to each URL serves as an additional mapping
-   * from startLine numbers to Debugger.Script objects, facilitating retrieval
-   * of the scripts that contain a particular line number. For example, if a
-   * cache holds two scripts with the URL http://foo.com/ starting at lines 4
-   * and 10, then the corresponding cache will be:
-   * this._scripts: {
-   *   'http://foo.com/': [,,,,[Debugger.Script],,,,,,[Debugger.Script]]
-   * }
-   */
-  this._scripts = {};
-
   // A cache of prototype chains for objects that have received a
   // prototypeAndProperties request. Due to the way the debugger frontend works,
   // this corresponds to a cache of prototype chains that the user has been
   // inspecting in the variables tree view. This allows the debugger to evaluate
   // native getter methods for WebIDL attributes that are meant to be called on
   // the instace and not on the prototype.
   //
   // The map keys are Debugger.Object instances requested by the client and the
@@ -94,17 +74,16 @@ ThreadActor.prototype = {
   },
 
   clearDebuggees: function TA_clearDebuggees() {
     if (this.dbg) {
       this.dbg.removeAllDebuggees();
     }
     this.conn.removeActorPool(this._threadLifetimePool || undefined);
     this._threadLifetimePool = null;
-    this._scripts = {};
     this._sources = {};
   },
 
   /**
    * Add a debuggee global to the Debugger object.
    */
   addDebuggee: function TA_addDebuggee(aGlobal) {
     try {
@@ -485,17 +464,17 @@ ThreadActor.prototype = {
   onSetBreakpoint: function TA_onSetBreakpoint(aRequest) {
     if (this.state !== "paused") {
       return { error: "wrongState",
                message: "Breakpoints can only be set while the debuggee is paused."};
     }
 
     let location = aRequest.location;
     let line = location.line;
-    if (!this._scripts[location.url] || line < 0) {
+    if (this.dbg.findScripts({ url: location.url }).length == 0 || line < 0) {
       return { error: "noScript" };
     }
 
     // Add the breakpoint to the store for later reuse, in case it belongs to a
     // script that hasn't appeared yet.
     if (!this._breakpointStore[location.url]) {
       this._breakpointStore[location.url] = [];
     }
@@ -609,45 +588,16 @@ ThreadActor.prototype = {
 
     return {
       error: "noCodeAtLineColumn",
       actor: actor.actorID
     };
   },
 
   /**
-   * A recursive generator function for iterating over the scripts that contain
-   * the specified line, by looking through child scripts of the supplied
-   * script. As an example, an inline <script> tag has the top-level functions
-   * declared in it as its children.
-   *
-   * @param aScript Debugger.Script
-   *        The source script.
-   * @param aLine number
-   *        The line number.
-   */
-  _getContainers: function TA__getContainers(aScript, aLine) {
-    let children = aScript.getChildScripts();
-    if (children.length > 0) {
-      for (let i = 0; i < children.length; i++) {
-        let child = children[i];
-        // Iterate over the children that contain this location.
-        if (child.startLine <= aLine &&
-            child.startLine + child.lineCount > aLine) {
-          for (let j of this._getContainers(child, aLine)) {
-            yield j;
-          }
-        }
-      }
-    }
-    // Include this script in the iteration, too.
-    yield aScript;
-  },
-
-  /**
    * Get the script and source lists from the debugger.
    */
   _discoverScriptsAndSources: function TA__discoverScriptsAndSources() {
     for (let s of this.dbg.findScripts()) {
       this._addScript(s);
     }
   },
 
@@ -1183,23 +1133,16 @@ ThreadActor.prototype = {
     if (!this._allowSource(aScript.url)) {
       return false;
     }
 
     // TODO bug 637572: we should be dealing with sources directly, not
     // inferring them through scripts.
     this._addSource(aScript.url);
 
-    // Use a sparse array for storing the scripts for each URL in order to
-    // optimize retrieval.
-    if (!this._scripts[aScript.url]) {
-      this._scripts[aScript.url] = [];
-    }
-    this._scripts[aScript.url][aScript.startLine] = aScript;
-
     // Set any stored breakpoints.
     let existing = this._breakpointStore[aScript.url];
     if (existing) {
       let endLine = aScript.startLine + aScript.lineCount - 1;
       // Iterate over the lines backwards, so that sliding breakpoints don't
       // affect the loop.
       for (let line = existing.length - 1; line >= 0; line--) {
         let bp = existing[line];
--- a/toolkit/devtools/debugger/tests/unit/testcompatactors.js
+++ b/toolkit/devtools/debugger/tests/unit/testcompatactors.js
@@ -28,30 +28,27 @@ function createRootActor()
             threadActor: actor.thread.actorID
           };
         };
 
         actor.thread.requestTypes["scripts"] = function (aRequest) {
           this._discoverScriptsAndSources();
 
           let scripts = [];
-          for (let url in this._scripts) {
-            for (let i = 0; i < this._scripts[url].length; i++) {
-              if (!this._scripts[url][i]) {
-                continue;
-              }
-
-              let script = {
-                url: url,
-                startLine: i,
-                lineCount: this._scripts[url][i].lineCount,
-                source: this._getSource(url).form()
-              };
-              scripts.push(script);
+          for (let s of this.dbg.findScripts()) {
+            if (!s.url) {
+              continue;
             }
+            let script = {
+              url: s.url,
+              startLine: s.startLine,
+              lineCount: s.lineCount,
+              source: this._getSource(s.url).form()
+            };
+            scripts.push(script);
           }
 
           return {
             from: this.actorID,
             scripts: scripts
           };
         };