Merge mozilla-central to mozilla-inbound
authorEd Morley <bmo@edmorley.co.uk>
Sun, 22 Jan 2012 15:24:29 +0000
changeset 86302 a6a127333270e53134f2c058597a2b267c710112
parent 86301 ca7d87ab38b6e7ef55edd6ac2f300b8c1fcbfc01 (current diff)
parent 86300 d7f703ae1c8c7014be939f31244ddde5422f6691 (diff)
child 86303 14810f6dd6a78b6d5eaa41fac21180ab25ea6461
push id805
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 18:17:35 +0000
treeherdermozilla-aurora@6fb3bf232436 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone12.0a1
Merge mozilla-central to mozilla-inbound
browser/devtools/styleeditor/SplitView.jsm
browser/devtools/styleeditor/splitview.css
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1325894427000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1327073984000">
   <emItems>
       <emItem  blockID="i41" id="{99079a25-328f-4bd4-be04-00955acaa0a7}">
                         <versionRange  minVersion="0.1" maxVersion="4.3.1.00" severity="1">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i8" id="{B13721C7-F507-4982-B2E5-502A71474FED}">
                         <versionRange  minVersion=" " severity="1">
                     </versionRange>
@@ -34,16 +34,20 @@
       <emItem  blockID="i39" id="{c2d64ff7-0ab8-4263-89c9-ea3b0f8f050c}">
                         <versionRange  minVersion="0.1" maxVersion="4.3.1.00" severity="1">
                     </versionRange>
                   </emItem>
       <emItem  blockID="i42" id="{D19CA586-DD6C-4a0a-96F8-14644F340D60}">
                         <versionRange  minVersion="0.1" maxVersion="14.4.0" severity="1">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i53" id="{a3a5c777-f583-4fef-9380-ab4add1bc2a8}">
+                        <versionRange  minVersion="2.0.3" maxVersion="2.0.3">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i10" id="{8CE11043-9A15-4207-A565-0C94C42D590D}">
                         </emItem>
       <emItem  blockID="i1" id="mozilla_cc@internetdownloadmanager.com">
                         <versionRange  minVersion="2.1" maxVersion="3.3">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="3.0a1" maxVersion="*" />
                           </targetApplication>
                     </versionRange>
@@ -61,20 +65,31 @@
                         </emItem>
       <emItem  blockID="i4" id="{4B3803EA-5230-4DC3-A7FC-33638F3D3542}">
                         <versionRange  minVersion="1.2" maxVersion="1.2">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="3.0a1" maxVersion="*" />
                           </targetApplication>
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i50" id="firebug@software.joehewitt.com">
+                        <versionRange  minVersion="0" maxVersion="0">
+                      <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+                              <versionRange  minVersion="9.0a1" maxVersion="9.*" />
+                          </targetApplication>
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i40" id="{28387537-e3f9-4ed7-860c-11e69af4a8a0}">
                         <versionRange  minVersion="0.1" maxVersion="4.3.1.00" severity="1">
                     </versionRange>
                   </emItem>
+      <emItem  blockID="i51" id="admin@youtubeplayer.com">
+                        <versionRange  minVersion="0" maxVersion="*">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i46" id="{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}">
                         <versionRange  minVersion="0.1" maxVersion="*">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="9.0a1" maxVersion="9.0" />
                           </targetApplication>
                     </versionRange>
                   </emItem>
       <emItem  blockID="i23" id="firefox@bandoo.com">
@@ -134,20 +149,22 @@
                               <versionRange  minVersion="8.0a1" maxVersion="*" />
                           </targetApplication>
                     </versionRange>
                   </emItem>
       <emItem  blockID="i3" id="langpack-vi-VN@firefox.mozilla.org">
                         <versionRange  minVersion="2.0" maxVersion="2.0">
                     </versionRange>
                   </emItem>
-      <emItem  blockID="i49" id="{ADFA33FD-16F5-4355-8504-DF4D664CFE63}">
-                        </emItem>
       <emItem  blockID="i7" id="{2224e955-00e9-4613-a844-ce69fccaae91}">
                         </emItem>
+      <emItem  blockID="i52" id="ff-ext@youtube">
+                        <versionRange  minVersion="0" maxVersion="*">
+                    </versionRange>
+                  </emItem>
       <emItem  blockID="i24" id="{6E19037A-12E3-4295-8915-ED48BC341614}">
                         <versionRange  minVersion="0.1" maxVersion="1.3.328.4" severity="1">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="3.7a1pre" maxVersion="*" />
                           </targetApplication>
                     </versionRange>
                   </emItem>
       <emItem  blockID="i15" id="personas@christopher.beard">
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1031,32 +1031,34 @@ pref("devtools.errorconsole.enabled", fa
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.htmlHeight", 112);
 
 // Enable the style inspector
 pref("devtools.styleinspector.enabled", true);
 
 // Enable the Tilt inspector
 pref("devtools.tilt.enabled", true);
+pref("devtools.tilt.intro_transition", true);
+pref("devtools.tilt.outro_transition", true);
 
 // Enable the rules view
 pref("devtools.ruleview.enabled", true);
 
 // Enable the Scratchpad tool.
 pref("devtools.scratchpad.enabled", true);
 
 // Enable the Style Editor.
 pref("devtools.styleeditor.enabled", true);
 pref("devtools.styleeditor.transitions", true);
 
 // Enable tools for Chrome development.
 pref("devtools.chrome.enabled", false);
 
 // Disable the GCLI enhanced command line.
-pref("devtools.gcli.enable", true);
+pref("devtools.gcli.enable", false);
 
 // The last Web Console height. This is initially 0 which means that the Web
 // Console will use the default height next time it shows.
 // Change to -1 if you do not want the Web Console to remember its last height.
 pref("devtools.hud.height", 0);
 
 // Remember the Web Console position. Possible values:
 //   above - above the web page,
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -988,17 +988,16 @@
 
   <vbox id="browser-bottombox" layer="true">
     <toolbar id="inspector-toolbar"
              class="devtools-toolbar"
              nowindowdrag="true"
              hidden="true">
       <vbox flex="1">
         <resizer id="inspector-top-resizer" flex="1" 
-                 class="inspector-resizer"
                  dir="top" disabled="true"
                  element="inspector-tree-box"/>
         <hbox>
 #ifdef XP_MACOSX
           <toolbarbutton id="highlighter-closebutton"
                          oncommand="InspectorUI.closeInspectorUI(false);"
                          tooltiptext="&inspectCloseButton.tooltiptext;"/>
 #endif
@@ -1024,20 +1023,16 @@
                            command="Inspector:Sidebar"/>
             <!-- registered tools go here -->
           </hbox>
 #ifndef XP_MACOSX
           <toolbarbutton id="highlighter-closebutton"
                          oncommand="InspectorUI.closeInspectorUI(false);"
                          tooltiptext="&inspectCloseButton.tooltiptext;"/>
 #endif
-          <resizer id="inspector-end-resizer"
-                   class="inspector-resizer"
-                   dir="top" disabled="true"
-                   element="inspector-tree-box"/>
         </hbox>
       </vbox>
     </toolbar>
     <toolbar id="addon-bar"
              toolbarname="&addonBarCmd.label;" accesskey="&addonBarCmd.accesskey;"
              collapsed="true"
              class="toolbar-primary chromeclass-toolbar"
              context="toolbar-context-menu" toolboxid="navigator-toolbox"
--- a/browser/base/content/highlighter.css
+++ b/browser/base/content/highlighter.css
@@ -29,22 +29,21 @@
 #highlighter-veil-middlebox:-moz-locale-dir(rtl) {
   -moz-box-direction: reverse;
 }
 
 .inspector-breadcrumbs-button {
   direction: ltr;
 }
 
-.inspector-resizer {
+#inspector-top-resizer {
   display: none;
 }
 
-#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer,
-#inspector-toolbar[treepanel-open] > vbox > hbox > #inspector-end-resizer {
+#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer {
   display: -moz-box;
 }
 
 /*
  * Node Infobar
  */
 
 #highlighter-nodeinfobar-container {
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,15 +1,15 @@
 browser.jar:
 *   content/browser/inspector.html                (highlighter/inspector.html)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
 *   content/browser/scratchpad.xul                (scratchpad/scratchpad.xul)
     content/browser/scratchpad.js                 (scratchpad/scratchpad.js)
+    content/browser/splitview.css                 (shared/splitview.css)
 *   content/browser/styleeditor.xul               (styleeditor/styleeditor.xul)
-    content/browser/splitview.css                 (styleeditor/splitview.css)
     content/browser/styleeditor.css               (styleeditor/styleeditor.css)
     content/browser/devtools/csshtmltree.xul      (styleinspector/csshtmltree.xul)
     content/browser/devtools/cssruleview.xul      (styleinspector/cssruleview.xul)
     content/browser/devtools/styleinspector.css   (styleinspector/styleinspector.css)
     content/browser/orion.js                      (sourceeditor/orion/orion.js)
     content/browser/orion.css                     (sourceeditor/orion/orion.css)
     content/browser/orion-mozilla.css             (sourceeditor/orion/mozilla.css)
     content/browser/source-editor-overlay.xul     (sourceeditor/source-editor-overlay.xul)
--- a/browser/devtools/shared/Makefile.in
+++ b/browser/devtools/shared/Makefile.in
@@ -47,11 +47,9 @@ include $(DEPTH)/config/autoconf.mk
 
 ifdef ENABLE_TESTS
 	DIRS += test
 endif
 
 include $(topsrcdir)/config/rules.mk
 
 libs::
-	$(NSINSTALL) $(srcdir)/Templater.jsm $(FINAL_TARGET)/modules/devtools
-	$(NSINSTALL) $(srcdir)/Promise.jsm $(FINAL_TARGET)/modules/devtools
-	$(NSINSTALL) $(srcdir)/LayoutHelpers.jsm $(FINAL_TARGET)/modules/devtools
+	$(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/SplitView.jsm
@@ -0,0 +1,453 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Style Editor code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Cedric Vivier <cedricv@neonux.com> (original author)
+ *   Paul Rouget <paul@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["SplitView"];
+
+/* this must be kept in sync with CSS (ie. splitview.css) */
+const LANDSCAPE_MEDIA_QUERY = "(min-aspect-ratio: 5/3)";
+
+const BINDING_USERDATA = "splitview-binding";
+
+
+/**
+ * SplitView constructor
+ *
+ * Initialize the split view UI on an existing DOM element.
+ *
+ * A split view contains items, each of those having one summary and one details
+ * elements.
+ * It is adaptive as it behaves similarly to a richlistbox when there the aspect
+ * ratio is narrow or as a pair listbox-box otherwise.
+ *
+ * @param DOMElement aRoot
+ * @see appendItem
+ */
+function SplitView(aRoot)
+{
+  this._root = aRoot;
+  this._controller = aRoot.querySelector(".splitview-controller");
+  this._nav = aRoot.querySelector(".splitview-nav");
+  this._side = aRoot.querySelector(".splitview-side-details");
+  this._activeSummary = null
+
+  this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
+
+  this._filter = aRoot.querySelector(".splitview-filter");
+  if (this._filter) {
+    this._setupFilterBox();
+  }
+
+  // items list focus and search-on-type handling
+  this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) {
+    function getFocusedItemWithin(nav) {
+      let node = nav.ownerDocument.activeElement;
+      while (node && node.parentNode != nav) {
+        node = node.parentNode;
+      }
+      return node;
+    }
+
+    // do not steal focus from inside iframes or textboxes
+    if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
+        aEvent.target.tagName == "input" ||
+        aEvent.target.tagName == "textbox" ||
+        aEvent.target.tagName == "textarea" ||
+        aEvent.target.classList.contains("textbox")) {
+      return false;
+    }
+
+    // handle keyboard navigation within the items list
+    let newFocusOrdinal;
+    if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP ||
+        aEvent.keyCode == aEvent.DOM_VK_HOME) {
+      newFocusOrdinal = 0;
+    } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
+               aEvent.keyCode == aEvent.DOM_VK_END) {
+      newFocusOrdinal = this._nav.childNodes.length - 1;
+    } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
+      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+      newFocusOrdinal--;
+    } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
+      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
+      newFocusOrdinal++;
+    }
+    if (newFocusOrdinal !== undefined) {
+      aEvent.stopPropagation();
+      let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
+      if (el) {
+        el.focus();
+      }
+      return false;
+    }
+
+    // search-on-type when any non-whitespace character is pressed while list
+    // has the focus
+    if (this._filter &&
+        !/\s/.test(String.fromCharCode(aEvent.which))) {
+      this._filter.focus();
+    }
+  }.bind(this), false);
+}
+
+SplitView.prototype = {
+  /**
+    * Retrieve whether the UI currently has a landscape orientation.
+    *
+    * @return boolean
+    */
+  get isLandscape() this._mql.matches,
+
+  /**
+    * Retrieve the root element.
+    *
+    * @return DOMElement
+    */
+  get rootElement() this._root,
+
+  /**
+    * Retrieve the active item's summary element or null if there is none.
+    *
+    * @return DOMElement
+    */
+  get activeSummary() this._activeSummary,
+
+  /**
+    * Set the active item's summary element.
+    *
+    * @param DOMElement aSummary
+    */
+  set activeSummary(aSummary)
+  {
+    if (aSummary == this._activeSummary) {
+      return;
+    }
+
+    if (this._activeSummary) {
+      let binding = this._activeSummary.getUserData(BINDING_USERDATA);
+
+      if (binding.onHide) {
+        binding.onHide(this._activeSummary, binding._details, binding.data);
+      }
+
+      this._activeSummary.classList.remove("splitview-active");
+      binding._details.classList.remove("splitview-active");
+    }
+
+    if (!aSummary) {
+      return;
+    }
+
+    let binding = aSummary.getUserData(BINDING_USERDATA);
+    aSummary.classList.add("splitview-active");
+    binding._details.classList.add("splitview-active");
+
+    this._activeSummary = aSummary;
+
+    if (binding.onShow) {
+      binding.onShow(aSummary, binding._details, binding.data);
+    }
+    aSummary.scrollIntoView();
+  },
+
+  /**
+    * Retrieve the active item's details element or null if there is none.
+    * @return DOMElement
+    */
+  get activeDetails()
+  {
+    let summary = this.activeSummary;
+    return summary ? summary.getUserData(BINDING_USERDATA)._details : null;
+  },
+
+  /**
+   * Retrieve the summary element for a given ordinal.
+   *
+   * @param number aOrdinal
+   * @return DOMElement
+   *         Summary element with given ordinal or null if not found.
+   * @see appendItem
+   */
+  getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
+  {
+    return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
+  },
+
+  /**
+   * Append an item to the split view.
+   *
+   * @param DOMElement aSummary
+   *        The summary element for the item.
+   * @param DOMElement aDetails
+   *        The details element for the item.
+   * @param object aOptions
+   *     Optional object that defines custom behavior and data for the item.
+   *     All properties are optional :
+   *     - function(DOMElement summary, DOMElement details, object data) onCreate
+   *         Called when the item has been added.
+   *     - function(summary, details, data) onShow
+   *         Called when the item is shown/active.
+   *     - function(summary, details, data) onHide
+   *         Called when the item is hidden/inactive.
+   *     - function(summary, details, data) onDestroy
+   *         Called when the item has been removed.
+   *     - function(summary, details, data, query) onFilterBy
+   *         Called when the user performs a filtering search.
+   *         If the function returns false, the item does not match query
+   *         string and will be hidden.
+   *     - object data
+   *         Object to pass to the callbacks above.
+   *     - number ordinal
+   *         Items with a lower ordinal are displayed before those with a
+   *         higher ordinal.
+   */
+  appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
+  {
+    let binding = aOptions || {};
+
+    binding._summary = aSummary;
+    binding._details = aDetails;
+    aSummary.setUserData(BINDING_USERDATA, binding, null);
+
+    this._nav.appendChild(aSummary);
+
+    aSummary.addEventListener("click", function onSummaryClick(aEvent) {
+      aEvent.stopPropagation();
+      this.activeSummary = aSummary;
+    }.bind(this), false);
+
+    this._side.appendChild(aDetails);
+
+    if (binding.onCreate) {
+      // queue onCreate handler
+      this._root.ownerDocument.defaultView.setTimeout(function () {
+        binding.onCreate(aSummary, aDetails, binding.data);
+      }, 0);
+    }
+  },
+
+  /**
+   * Append an item to the split view according to two template elements
+   * (one for the item's summary and the other for the item's details).
+   *
+   * @param string aName
+   *        Name of the template elements to instantiate.
+   *        Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
+   *        and "splitview-tpl-details-" suffixed with aName.
+   * @param object aOptions
+   *        Optional object that defines custom behavior and data for the item.
+   *        See appendItem for full description.
+   * @return object{summary:,details:}
+   *         Object with the new DOM elements created for summary and details.
+   * @see appendItem
+   */
+  appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
+  {
+    aOptions = aOptions || {};
+    let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
+    let details = this._root.querySelector("#splitview-tpl-details-" + aName);
+
+    summary = summary.cloneNode(true);
+    summary.id = "";
+    if (aOptions.ordinal !== undefined) { // can be zero
+      summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
+      summary.setAttribute("data-ordinal", aOptions.ordinal);
+    }
+    details = details.cloneNode(true);
+    details.id = "";
+
+    this.appendItem(summary, details, aOptions);
+    return {summary: summary, details: details};
+  },
+
+  /**
+    * Remove an item from the split view.
+    *
+    * @param DOMElement aSummary
+    *        Summary element of the item to remove.
+    */
+  removeItem: function ASV_removeItem(aSummary)
+  {
+    if (aSummary == this._activeSummary) {
+      this.activeSummary = null;
+    }
+
+    let binding = aSummary.getUserData(BINDING_USERDATA);
+    aSummary.parentNode.removeChild(aSummary);
+    binding._details.parentNode.removeChild(binding._details);
+
+    if (binding.onDestroy) {
+      binding.onDestroy(aSummary, binding._details, binding.data);
+    }
+  },
+
+  /**
+   * Remove all items from the split view.
+   */
+  removeAll: function ASV_removeAll()
+  {
+    while (this._nav.hasChildNodes()) {
+      this.removeItem(this._nav.firstChild);
+    }
+  },
+
+  /**
+    * Filter items by given string.
+    * Matching is performed on every item by calling onFilterBy when defined
+    * and then by searching aQuery in the summary element's text item.
+    * Non-matching item is hidden.
+    *
+    * If no item matches, 'splitview-all-filtered' class is set on the filter
+    * input element and the splitview-nav element.
+    *
+    * @param string aQuery
+    *        The query string. Use null to reset (no filter).
+    * @return number
+    *         The number of filtered (non-matching) item.
+    */
+  filterItemsBy: function ASV_filterItemsBy(aQuery)
+  {
+    if (!this._nav.hasChildNodes()) {
+      return 0;
+    }
+    if (aQuery) {
+      aQuery = aQuery.trim();
+    }
+    if (!aQuery) {
+      for (let i = 0; i < this._nav.childNodes.length; ++i) {
+        this._nav.childNodes[i].classList.remove("splitview-filtered");
+      }
+      this._filter.classList.remove("splitview-all-filtered");
+      this._nav.classList.remove("splitview-all-filtered");
+      return 0;
+    }
+
+    let count = 0;
+    let filteredCount = 0;
+    for (let i = 0; i < this._nav.childNodes.length; ++i) {
+      let summary = this._nav.childNodes[i];
+
+      let matches = false;
+      let binding = summary.getUserData(BINDING_USERDATA);
+      if (binding.onFilterBy) {
+        matches = binding.onFilterBy(summary, binding._details, binding.data, aQuery);
+      }
+      if (!matches) { // try text content
+        let content = summary.textContent.toUpperCase();
+        matches = (content.indexOf(aQuery.toUpperCase()) > -1);
+      }
+
+      count++;
+      if (!matches) {
+        summary.classList.add("splitview-filtered");
+        filteredCount++;
+      } else {
+        summary.classList.remove("splitview-filtered");
+      }
+    }
+
+    if (count > 0 && filteredCount == count) {
+      this._filter.classList.add("splitview-all-filtered");
+      this._nav.classList.add("splitview-all-filtered");
+    } else {
+      this._filter.classList.remove("splitview-all-filtered");
+      this._nav.classList.remove("splitview-all-filtered");
+    }
+    return filteredCount;
+  },
+
+  /**
+   * Set the item's CSS class name.
+   * This sets the class on both the summary and details elements, retaining
+   * any SplitView-specific classes.
+   *
+   * @param DOMElement aSummary
+   *        Summary element of the item to set.
+   * @param string aClassName
+   *        One or more space-separated CSS classes.
+   */
+  setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
+  {
+    let binding = aSummary.getUserData(BINDING_USERDATA);
+    let viewSpecific;
+
+    viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
+    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+    aSummary.className = viewSpecific + " " + aClassName;
+
+    viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
+    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
+    binding._details.className = viewSpecific + " " + aClassName;
+  },
+
+  /**
+   * Set up filter search box.
+   */
+  _setupFilterBox: function ASV__setupFilterBox()
+  {
+    let clearFilter = function clearFilter(aEvent) {
+      this._filter.value = "";
+      this.filterItemsBy("");
+      return false;
+    }.bind(this);
+
+    this._filter.addEventListener("command", function onFilterInput(aEvent) {
+      this.filterItemsBy(this._filter.value);
+    }.bind(this), false);
+
+    this._filter.addEventListener("keyup", function onFilterKeyUp(aEvent) {
+      if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
+        clearFilter();
+      }
+      if (aEvent.keyCode == aEvent.DOM_VK_ENTER ||
+          aEvent.keyCode == aEvent.DOM_VK_RETURN) {
+        // autofocus matching item if there is only one
+        let matches = this._nav.querySelectorAll("* > li:not(.splitview-filtered)");
+        if (matches.length == 1) {
+          this.activeSummary = matches[0];
+        }
+      }
+    }.bind(this), false);
+
+    let clearButtons = this._root.querySelectorAll(".splitview-filter-clearButton");
+    for (let i = 0; i < clearButtons.length; ++i) {
+      clearButtons[i].addEventListener("click", clearFilter, false);
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/splitview.css
@@ -0,0 +1,126 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Style Editor code.
+ *
+ * The Initial Developer of the Original Code is Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Cedric Vivier <cedricv@neonux.com> (original author)
+ *   Paul Rouget <paul@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+box,
+.splitview-nav {
+  -moz-box-flex: 1;
+  -moz-box-orient: vertical;
+}
+
+.splitview-nav-container {
+  -moz-box-pack: center;
+}
+
+.loading .splitview-nav-container > .placeholder {
+  display: none !important;
+}
+
+.splitview-controller,
+.splitview-main {
+  -moz-box-flex: 0;
+}
+
+.splitview-controller {
+  min-height: 3em;
+  max-height: 14em;
+}
+
+.splitview-nav {
+  display: -moz-box;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+/* only the active details pane is shown */
+.splitview-side-details > * {
+  display: none;
+}
+.splitview-side-details > .splitview-active {
+  display: -moz-box;
+}
+
+.splitview-landscape-resizer {
+  cursor: ew-resize;
+}
+
+/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
+@media (min-aspect-ratio: 5/3) {
+  .splitview-root {
+    -moz-box-orient: horizontal;
+  }
+  .splitview-controller {
+    max-height: none;
+  }
+  .splitview-details {
+    display: none;
+  }
+  .splitview-details.splitview-active {
+    display: -moz-box;
+  }
+}
+
+/* filtered items are hidden */
+ol.splitview-nav > li.splitview-filtered {
+  display: none;
+}
+
+/* "empty list" and "all filtered" placeholders are hidden */
+.splitview-nav:empty,
+.splitview-nav.splitview-all-filtered,
+.splitview-nav + .splitview-nav.placeholder {
+  display: none;
+}
+.splitview-nav.splitview-all-filtered ~ .splitview-nav.placeholder.all-filtered,
+.splitview-nav:empty ~ .splitview-nav.placeholder.empty {
+  display: -moz-box;
+}
+
+.splitview-portrait-resizer {
+  display: none;
+}
+
+/* portrait mode */
+@media (max-aspect-ratio: 5/3) {
+  #splitview-details-toolbar {
+    display: none;
+  }
+
+  .splitview-portrait-resizer {
+    display: -moz-box;
+  }
+}
deleted file mode 100644
--- a/browser/devtools/styleeditor/SplitView.jsm
+++ /dev/null
@@ -1,453 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Style Editor code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Cedric Vivier <cedricv@neonux.com> (original author)
- *   Paul Rouget <paul@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-"use strict";
-
-const EXPORTED_SYMBOLS = ["SplitView"];
-
-/* this must be kept in sync with CSS (ie. splitview.css) */
-const LANDSCAPE_MEDIA_QUERY = "(min-aspect-ratio: 5/3)";
-
-const BINDING_USERDATA = "splitview-binding";
-
-
-/**
- * SplitView constructor
- *
- * Initialize the split view UI on an existing DOM element.
- *
- * A split view contains items, each of those having one summary and one details
- * elements.
- * It is adaptive as it behaves similarly to a richlistbox when there the aspect
- * ratio is narrow or as a pair listbox-box otherwise.
- *
- * @param DOMElement aRoot
- * @see appendItem
- */
-function SplitView(aRoot)
-{
-  this._root = aRoot;
-  this._controller = aRoot.querySelector(".splitview-controller");
-  this._nav = aRoot.querySelector(".splitview-nav");
-  this._side = aRoot.querySelector(".splitview-side-details");
-  this._activeSummary = null
-
-  this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
-
-  this._filter = aRoot.querySelector(".splitview-filter");
-  if (this._filter) {
-    this._setupFilterBox();
-  }
-
-  // items list focus and search-on-type handling
-  this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) {
-    function getFocusedItemWithin(nav) {
-      let node = nav.ownerDocument.activeElement;
-      while (node && node.parentNode != nav) {
-        node = node.parentNode;
-      }
-      return node;
-    }
-
-    // do not steal focus from inside iframes or textboxes
-    if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
-        aEvent.target.tagName == "input" ||
-        aEvent.target.tagName == "textbox" ||
-        aEvent.target.tagName == "textarea" ||
-        aEvent.target.classList.contains("textbox")) {
-      return false;
-    }
-
-    // handle keyboard navigation within the items list
-    let newFocusOrdinal;
-    if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP ||
-        aEvent.keyCode == aEvent.DOM_VK_HOME) {
-      newFocusOrdinal = 0;
-    } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
-               aEvent.keyCode == aEvent.DOM_VK_END) {
-      newFocusOrdinal = this._nav.childNodes.length - 1;
-    } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
-      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
-      newFocusOrdinal--;
-    } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
-      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
-      newFocusOrdinal++;
-    }
-    if (newFocusOrdinal !== undefined) {
-      aEvent.stopPropagation();
-      let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
-      if (el) {
-        el.focus();
-      }
-      return false;
-    }
-
-    // search-on-type when any non-whitespace character is pressed while list
-    // has the focus
-    if (this._filter &&
-        !/\s/.test(String.fromCharCode(aEvent.which))) {
-      this._filter.focus();
-    }
-  }.bind(this), false);
-}
-
-SplitView.prototype = {
-  /**
-    * Retrieve whether the UI currently has a landscape orientation.
-    *
-    * @return boolean
-    */
-  get isLandscape() this._mql.matches,
-
-  /**
-    * Retrieve the root element.
-    *
-    * @return DOMElement
-    */
-  get rootElement() this._root,
-
-  /**
-    * Retrieve the active item's summary element or null if there is none.
-    *
-    * @return DOMElement
-    */
-  get activeSummary() this._activeSummary,
-
-  /**
-    * Set the active item's summary element.
-    *
-    * @param DOMElement aSummary
-    */
-  set activeSummary(aSummary)
-  {
-    if (aSummary == this._activeSummary) {
-      return;
-    }
-
-    if (this._activeSummary) {
-      let binding = this._activeSummary.getUserData(BINDING_USERDATA);
-
-      if (binding.onHide) {
-        binding.onHide(this._activeSummary, binding._details, binding.data);
-      }
-
-      this._activeSummary.classList.remove("splitview-active");
-      binding._details.classList.remove("splitview-active");
-    }
-
-    if (!aSummary) {
-      return;
-    }
-
-    let binding = aSummary.getUserData(BINDING_USERDATA);
-    aSummary.classList.add("splitview-active");
-    binding._details.classList.add("splitview-active");
-
-    this._activeSummary = aSummary;
-
-    if (binding.onShow) {
-      binding.onShow(aSummary, binding._details, binding.data);
-    }
-    aSummary.scrollIntoView();
-  },
-
-  /**
-    * Retrieve the active item's details element or null if there is none.
-    * @return DOMElement
-    */
-  get activeDetails()
-  {
-    let summary = this.activeSummary;
-    return summary ? summary.getUserData(BINDING_USERDATA)._details : null;
-  },
-
-  /**
-   * Retrieve the summary element for a given ordinal.
-   *
-   * @param number aOrdinal
-   * @return DOMElement
-   *         Summary element with given ordinal or null if not found.
-   * @see appendItem
-   */
-  getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
-  {
-    return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
-  },
-
-  /**
-   * Append an item to the split view.
-   *
-   * @param DOMElement aSummary
-   *        The summary element for the item.
-   * @param DOMElement aDetails
-   *        The details element for the item.
-   * @param object aOptions
-   *     Optional object that defines custom behavior and data for the item.
-   *     All properties are optional :
-   *     - function(DOMElement summary, DOMElement details, object data) onCreate
-   *         Called when the item has been added.
-   *     - function(summary, details, data) onShow
-   *         Called when the item is shown/active.
-   *     - function(summary, details, data) onHide
-   *         Called when the item is hidden/inactive.
-   *     - function(summary, details, data) onDestroy
-   *         Called when the item has been removed.
-   *     - function(summary, details, data, query) onFilterBy
-   *         Called when the user performs a filtering search.
-   *         If the function returns false, the item does not match query
-   *         string and will be hidden.
-   *     - object data
-   *         Object to pass to the callbacks above.
-   *     - number ordinal
-   *         Items with a lower ordinal are displayed before those with a
-   *         higher ordinal.
-   */
-  appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
-  {
-    let binding = aOptions || {};
-
-    binding._summary = aSummary;
-    binding._details = aDetails;
-    aSummary.setUserData(BINDING_USERDATA, binding, null);
-
-    this._nav.appendChild(aSummary);
-
-    aSummary.addEventListener("click", function onSummaryClick(aEvent) {
-      aEvent.stopPropagation();
-      this.activeSummary = aSummary;
-    }.bind(this), false);
-
-    this._side.appendChild(aDetails);
-
-    if (binding.onCreate) {
-      // queue onCreate handler
-      this._root.ownerDocument.defaultView.setTimeout(function () {
-        binding.onCreate(aSummary, aDetails, binding.data);
-      }, 0);
-    }
-  },
-
-  /**
-   * Append an item to the split view according to two template elements
-   * (one for the item's summary and the other for the item's details).
-   *
-   * @param string aName
-   *        Name of the template elements to instantiate.
-   *        Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
-   *        and "splitview-tpl-details-" suffixed with aName.
-   * @param object aOptions
-   *        Optional object that defines custom behavior and data for the item.
-   *        See appendItem for full description.
-   * @return object{summary:,details:}
-   *         Object with the new DOM elements created for summary and details.
-   * @see appendItem
-   */
-  appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
-  {
-    aOptions = aOptions || {};
-    let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
-    let details = this._root.querySelector("#splitview-tpl-details-" + aName);
-
-    summary = summary.cloneNode(true);
-    summary.id = "";
-    if (aOptions.ordinal !== undefined) { // can be zero
-      summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
-      summary.setAttribute("data-ordinal", aOptions.ordinal);
-    }
-    details = details.cloneNode(true);
-    details.id = "";
-
-    this.appendItem(summary, details, aOptions);
-    return {summary: summary, details: details};
-  },
-
-  /**
-    * Remove an item from the split view.
-    *
-    * @param DOMElement aSummary
-    *        Summary element of the item to remove.
-    */
-  removeItem: function ASV_removeItem(aSummary)
-  {
-    if (aSummary == this._activeSummary) {
-      this.activeSummary = null;
-    }
-
-    let binding = aSummary.getUserData(BINDING_USERDATA);
-    aSummary.parentNode.removeChild(aSummary);
-    binding._details.parentNode.removeChild(binding._details);
-
-    if (binding.onDestroy) {
-      binding.onDestroy(aSummary, binding._details, binding.data);
-    }
-  },
-
-  /**
-   * Remove all items from the split view.
-   */
-  removeAll: function ASV_removeAll()
-  {
-    while (this._nav.hasChildNodes()) {
-      this.removeItem(this._nav.firstChild);
-    }
-  },
-
-  /**
-    * Filter items by given string.
-    * Matching is performed on every item by calling onFilterBy when defined
-    * and then by searching aQuery in the summary element's text item.
-    * Non-matching item is hidden.
-    *
-    * If no item matches, 'splitview-all-filtered' class is set on the filter
-    * input element and the splitview-nav element.
-    *
-    * @param string aQuery
-    *        The query string. Use null to reset (no filter).
-    * @return number
-    *         The number of filtered (non-matching) item.
-    */
-  filterItemsBy: function ASV_filterItemsBy(aQuery)
-  {
-    if (!this._nav.hasChildNodes()) {
-      return 0;
-    }
-    if (aQuery) {
-      aQuery = aQuery.trim();
-    }
-    if (!aQuery) {
-      for (let i = 0; i < this._nav.childNodes.length; ++i) {
-        this._nav.childNodes[i].classList.remove("splitview-filtered");
-      }
-      this._filter.classList.remove("splitview-all-filtered");
-      this._nav.classList.remove("splitview-all-filtered");
-      return 0;
-    }
-
-    let count = 0;
-    let filteredCount = 0;
-    for (let i = 0; i < this._nav.childNodes.length; ++i) {
-      let summary = this._nav.childNodes[i];
-
-      let matches = false;
-      let binding = summary.getUserData(BINDING_USERDATA);
-      if (binding.onFilterBy) {
-        matches = binding.onFilterBy(summary, binding._details, binding.data, aQuery);
-      }
-      if (!matches) { // try text content
-        let content = summary.textContent.toUpperCase();
-        matches = (content.indexOf(aQuery.toUpperCase()) > -1);
-      }
-
-      count++;
-      if (!matches) {
-        summary.classList.add("splitview-filtered");
-        filteredCount++;
-      } else {
-        summary.classList.remove("splitview-filtered");
-      }
-    }
-
-    if (count > 0 && filteredCount == count) {
-      this._filter.classList.add("splitview-all-filtered");
-      this._nav.classList.add("splitview-all-filtered");
-    } else {
-      this._filter.classList.remove("splitview-all-filtered");
-      this._nav.classList.remove("splitview-all-filtered");
-    }
-    return filteredCount;
-  },
-
-  /**
-   * Set the item's CSS class name.
-   * This sets the class on both the summary and details elements, retaining
-   * any SplitView-specific classes.
-   *
-   * @param DOMElement aSummary
-   *        Summary element of the item to set.
-   * @param string aClassName
-   *        One or more space-separated CSS classes.
-   */
-  setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
-  {
-    let binding = aSummary.getUserData(BINDING_USERDATA);
-    let viewSpecific;
-
-    viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
-    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
-    aSummary.className = viewSpecific + " " + aClassName;
-
-    viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
-    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
-    binding._details.className = viewSpecific + " " + aClassName;
-  },
-
-  /**
-   * Set up filter search box.
-   */
-  _setupFilterBox: function ASV__setupFilterBox()
-  {
-    let clearFilter = function clearFilter(aEvent) {
-      this._filter.value = "";
-      this.filterItemsBy("");
-      return false;
-    }.bind(this);
-
-    this._filter.addEventListener("command", function onFilterInput(aEvent) {
-      this.filterItemsBy(this._filter.value);
-    }.bind(this), false);
-
-    this._filter.addEventListener("keyup", function onFilterKeyUp(aEvent) {
-      if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
-        clearFilter();
-      }
-      if (aEvent.keyCode == aEvent.DOM_VK_ENTER ||
-          aEvent.keyCode == aEvent.DOM_VK_RETURN) {
-        // autofocus matching item if there is only one
-        let matches = this._nav.querySelectorAll("* > li:not(.splitview-filtered)");
-        if (matches.length == 1) {
-          this.activeSummary = matches[0];
-        }
-      }
-    }.bind(this), false);
-
-    let clearButtons = this._root.querySelectorAll(".splitview-filter-clearButton");
-    for (let i = 0; i < clearButtons.length; ++i) {
-      clearButtons[i].addEventListener("click", clearFilter, false);
-    }
-  }
-};
deleted file mode 100644
--- a/browser/devtools/styleeditor/splitview.css
+++ /dev/null
@@ -1,126 +0,0 @@
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Style Editor code.
- *
- * The Initial Developer of the Original Code is Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Cedric Vivier <cedricv@neonux.com> (original author)
- *   Paul Rouget <paul@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-box,
-.splitview-nav {
-  -moz-box-flex: 1;
-  -moz-box-orient: vertical;
-}
-
-.splitview-nav-container {
-  -moz-box-pack: center;
-}
-
-.loading .splitview-nav-container > .placeholder {
-  display: none !important;
-}
-
-.splitview-controller,
-.splitview-main {
-  -moz-box-flex: 0;
-}
-
-.splitview-controller {
-  min-height: 3em;
-  max-height: 14em;
-}
-
-.splitview-nav {
-  display: -moz-box;
-  overflow-x: hidden;
-  overflow-y: auto;
-}
-
-/* only the active details pane is shown */
-.splitview-side-details > * {
-  display: none;
-}
-.splitview-side-details > .splitview-active {
-  display: -moz-box;
-}
-
-.splitview-landscape-resizer {
-  cursor: ew-resize;
-}
-
-/* this is to keep in sync with SplitView.jsm's LANDSCAPE_MEDIA_QUERY */
-@media (min-aspect-ratio: 5/3) {
-  .splitview-root {
-    -moz-box-orient: horizontal;
-  }
-  .splitview-controller {
-    max-height: none;
-  }
-  .splitview-details {
-    display: none;
-  }
-  .splitview-details.splitview-active {
-    display: -moz-box;
-  }
-}
-
-/* filtered items are hidden */
-ol.splitview-nav > li.splitview-filtered {
-  display: none;
-}
-
-/* "empty list" and "all filtered" placeholders are hidden */
-.splitview-nav:empty,
-.splitview-nav.splitview-all-filtered,
-.splitview-nav + .splitview-nav.placeholder {
-  display: none;
-}
-.splitview-nav.splitview-all-filtered ~ .splitview-nav.placeholder.all-filtered,
-.splitview-nav:empty ~ .splitview-nav.placeholder.empty {
-  display: -moz-box;
-}
-
-.splitview-portrait-resizer {
-  display: none;
-}
-
-/* portrait mode */
-@media (max-aspect-ratio: 5/3) {
-  #splitview-details-toolbar {
-    display: none;
-  }
-
-  .splitview-portrait-resizer {
-    display: -moz-box;
-  }
-}
--- a/browser/devtools/tilt/Tilt.jsm
+++ b/browser/devtools/tilt/Tilt.jsm
@@ -95,17 +95,17 @@ Tilt.prototype = {
    * Initializes a visualizer for the current tab.
    */
   initialize: function T_initialize()
   {
     let id = this.currentWindowId;
 
     // if the visualizer for the current tab is already open, destroy it now
     if (this.visualizers[id]) {
-      this.destroy(id);
+      this.destroy(id, true);
       return;
     }
 
     // create a visualizer instance for the current tab
     this.visualizers[id] = new TiltVisualizer({
       parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
       contentWindow: this.chromeWindow.gBrowser.selectedBrowser.contentWindow,
       requestAnimationFrame: this.chromeWindow.mozRequestAnimationFrame,
@@ -121,30 +121,52 @@ Tilt.prototype = {
     Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.INITIALIZED, null);
   },
 
   /**
    * Destroys a specific instance of the visualizer.
    *
    * @param {String} aId
    *                 the identifier of the instance in the visualizers array
+   * @param {Boolean} aAnimateFlag
+   *                  optional, set to true to display a destruction transition
    */
-  destroy: function T_destroy(aId)
+  destroy: function T_destroy(aId, aAnimateFlag)
   {
     // if the visualizer is already destroyed, don't do anything
     if (!this.visualizers[aId]) {
       return;
     }
 
-    this.visualizers[aId].removeOverlay();
-    this.visualizers[aId].cleanup();
-    this.visualizers[aId] = null;
+    if (!this.isDestroying) {
+      this.isDestroying = true;
+
+      let finalize = function T_finalize(aId) {
+        this.visualizers[aId].removeOverlay();
+        this.visualizers[aId].cleanup();
+        this.visualizers[aId] = null;
+
+        this.isDestroying = false;
+        this.chromeWindow.gBrowser.selectedBrowser.focus();
+        Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYED, null);
+      };
 
-    this.chromeWindow.gBrowser.selectedBrowser.contentWindow.focus();
-    Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYED, null);
+      if (!aAnimateFlag) {
+        finalize.call(this, aId);
+        return;
+      }
+
+      let controller = this.visualizers[aId].controller;
+      let presenter = this.visualizers[aId].presenter;
+      let content = presenter.contentWindow;
+
+      controller.removeEventListeners();
+      controller.arcball.reset([-content.pageXOffset, -content.pageYOffset]);
+      presenter.executeDestruction(finalize.bind(this, aId));
+    }
   },
 
   /**
    * Handles any supplementary post-initialization work, done immediately
    * after a TILT_NOTIFICATIONS.INITIALIZED notification.
    */
   _whenInitialized: function T__whenInitialized()
   {
--- a/browser/devtools/tilt/TiltUtils.jsm
+++ b/browser/devtools/tilt/TiltUtils.jsm
@@ -627,16 +627,28 @@ TiltUtils.destroyObject = function TU_de
   for (let i in aScope) {
     if (aScope.hasOwnProperty(i)) {
       delete aScope[i];
     }
   }
 };
 
 /**
+ * Gets the most recent browser window.
+ *
+ * @return {Window} the window
+ */
+TiltUtils.getBrowserWindow = function TU_getBrowserWindow()
+{
+  return Cc["@mozilla.org/appshell/window-mediator;1"]
+    .getService(Ci.nsIWindowMediator)
+    .getMostRecentWindow("navigator:browser");
+};
+
+/**
  * Retrieve the unique ID of a window object.
  *
  * @param {Window} aWindow
  *                 the window to get the ID from
  *
  * @return {Number} the window ID
  */
 TiltUtils.getWindowId = function TU_getWindowId(aWindow)
@@ -651,35 +663,29 @@ TiltUtils.getWindowId = function TU_getW
 };
 
 /**
  * Gets the markup document viewer zoom for the currently selected browser.
  *
  * @return {Number} the zoom ammount
  */
 TiltUtils.getDocumentZoom = function TU_getDocumentZoom() {
-  let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]
-    .getService(Ci.nsIWindowMediator)
-    .getMostRecentWindow("navigator:browser");
-
-  return browserWindow.gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
+  return TiltUtils.getBrowserWindow()
+                  .gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
 };
 
 /**
  * Performs a garbage collection.
  */
 TiltUtils.gc = function TU_gc()
 {
-  let browserWindow = Cc["@mozilla.org/appshell/window-mediator;1"]
-    .getService(Ci.nsIWindowMediator)
-    .getMostRecentWindow("navigator:browser");
-
-  browserWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-               .getInterface(Ci.nsIDOMWindowUtils)
-               .garbageCollect();
+  TiltUtils.getBrowserWindow()
+           .QueryInterface(Ci.nsIInterfaceRequestor)
+           .getInterface(Ci.nsIDOMWindowUtils)
+           .garbageCollect();
 };
 
 /**
  * Clears the cache and sets all the variables to null.
  */
 TiltUtils.clearCache = function TU_clearCache()
 {
   TiltUtils.DOM.parentNode = null;
--- a/browser/devtools/tilt/TiltVisualizer.jsm
+++ b/browser/devtools/tilt/TiltVisualizer.jsm
@@ -33,17 +33,17 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the LGPL or the GPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  ***** END LICENSE BLOCK *****/
 
 /*global Components, ChromeWorker */
-/*global TiltGL, TiltMath, vec3, mat4, quat4, TiltUtils, TiltVisualizerStyle */
+/*global TiltGL, TiltMath, EPSILON, vec3, mat4, quat4, TiltUtils */
 "use strict";
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 const ELEMENT_MIN_SIZE = 4;
 const INVISIBLE_ELEMENTS = {
   "head": true,
@@ -55,26 +55,29 @@ const INVISIBLE_ELEMENTS = {
   "option": true,
   "script": true,
   "style": true,
   "title": true
 };
 
 const STACK_THICKNESS = 15;
 const WIREFRAME_COLOR = [0, 0, 0, 0.25];
-const INITIAL_TRANSITION_DURATION = 100;
+const INTRO_TRANSITION_DURATION = 80;
+const OUTRO_TRANSITION_DURATION = 50;
 const INITIAL_Z_TRANSLATION = 400;
 
 const MOUSE_CLICK_THRESHOLD = 10;
-const ARCBALL_SENSITIVITY = 0.3;
+const ARCBALL_SENSITIVITY = 0.5;
 const ARCBALL_ROTATION_STEP = 0.15;
 const ARCBALL_TRANSLATION_STEP = 35;
 const ARCBALL_ZOOM_STEP = 0.1;
 const ARCBALL_ZOOM_MIN = -3000;
 const ARCBALL_ZOOM_MAX = 500;
+const ARCBALL_RESET_FACTOR = 0.9;
+const ARCBALL_RESET_INTERVAL = 1000 / 60;
 
 const TILT_CRAFTER = "resource:///modules/devtools/TiltWorkerCrafter.js";
 const TILT_PICKER = "resource:///modules/devtools/TiltWorkerPicker.js";
 
 Cu.import("resource:///modules/devtools/TiltGL.jsm");
 Cu.import("resource:///modules/devtools/TiltMath.jsm");
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
 Cu.import("resource:///modules/devtools/TiltVisualizerStyle.jsm");
@@ -290,46 +293,71 @@ TiltVisualizer.Presenter = function TV_P
       this.redraw = false;
       this.drawVisualization();
     }
 
     // call the attached ondraw event handler if specified (by the controller)
     if ("function" === typeof this.ondraw) {
       this.ondraw();
     }
+
+    if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) {
+      this.frames = INTRO_TRANSITION_DURATION;
+    }
+    if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) {
+      this.frames = OUTRO_TRANSITION_DURATION;
+    }
+
+    if ("function" === typeof this.onInitializationFinished &&
+        this.frames === INTRO_TRANSITION_DURATION &&
+       !this.isExecutingDestruction) {
+      this.onInitializationFinished();
+    }
+    if ("function" === typeof this.onDestructionFinished &&
+        this.frames === OUTRO_TRANSITION_DURATION &&
+        this.isExecutingDestruction) {
+      this.onDestructionFinished();
+    }
   }.bind(this);
 
   setup();
   loop();
 };
 
 TiltVisualizer.Presenter.prototype = {
 
   /**
    * Draws the visualization mesh and highlight quad.
    */
   drawVisualization: function TVP_drawVisualization()
   {
     let renderer = this.renderer;
     let transforms = this.transforms;
+    let w = renderer.width;
+    let h = renderer.height;
 
     // if the mesh wasn't created yet, don't continue rendering
     if (!this.meshStacks || !this.meshWireframe) {
       return;
     }
 
     // clear the context to an opaque black background
     renderer.clear();
     renderer.perspective();
 
     // apply a transition transformation using an ortho and perspective matrix
-    let f = this.frames / INITIAL_TRANSITION_DURATION;
-    let w = renderer.width;
-    let h = renderer.height;
-    renderer.lerp(renderer.projMatrix, mat4.ortho(0, w, h, 0, -1, 1000), f, 8);
+    let ortho = mat4.ortho(0, w, h, 0, -1000, 1000);
+
+    if (!this.isExecutingDestruction) {
+      let f = this.frames / INTRO_TRANSITION_DURATION;
+      renderer.lerp(renderer.projMatrix, ortho, f, 8);
+    } else {
+      let f = this.frames / OUTRO_TRANSITION_DURATION;
+      renderer.lerp(renderer.projMatrix, ortho, 1 - f, 8);
+    }
 
     // apply the preliminary transformations to the model view
     renderer.translate(w * 0.5, h * 0.5, -INITIAL_Z_TRANSLATION);
 
     // calculate the camera matrix using the rotation and translation
     renderer.translate(transforms.translation[0], 0,
                        transforms.translation[2]);
 
@@ -344,17 +372,18 @@ TiltVisualizer.Presenter.prototype = {
     // draw the visualization mesh
     renderer.strokeWeight(2);
     renderer.depthTest(true);
     this.drawMeshStacks();
     this.drawMeshWireframe();
     this.drawHighlight();
 
     // make sure the initial transition is drawn until finished
-    if (this.frames < INITIAL_TRANSITION_DURATION) {
+    if (this.frames < INTRO_TRANSITION_DURATION ||
+        this.frames < OUTRO_TRANSITION_DURATION) {
       this.redraw = true;
     }
     this.frames++;
   },
 
   /**
    * Draws the meshStacks object.
    */
@@ -455,17 +484,17 @@ TiltVisualizer.Presenter.prototype = {
 
   /**
    * Create the combined mesh representing the document visualization by
    * traversing the document & adding a stack for each node that is drawable.
    *
    * @param {Object} aData
    *                 object containing the necessary mesh verts, texcoord etc.
    */
-  setupMesh: function TVP_setupMesh(aData)
+  setupMesh: function TVP_setupMesh(aData) /*global TiltVisualizerStyle */
   {
     let renderer = this.renderer;
 
     // destroy any previously created mesh
     TiltUtils.destroyObject(this.meshStacks);
     TiltUtils.destroyObject(this.meshWireframe);
 
     // if the renderer was destroyed, don't continue setup
@@ -663,17 +692,18 @@ TiltVisualizer.Presenter.prototype = {
     let z = info.depth;
 
     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);
 
     this._currentSelection = aNodeIndex;
-    this.inspectorUI.inspectNode(node);
+    this.inspectorUI.inspectNode(node, this.contentWindow.innerHeight < y ||
+                                       this.contentWindow.pageYOffset > 0);
   },
 
   /**
    * Picks a stacked dom node at the x and y screen coordinates and issues
    * a callback function with the found intersection.
    *
    * @param {Number} x
    *                 the current horizontal coordinate of the mouse
@@ -783,16 +813,38 @@ TiltVisualizer.Presenter.prototype = {
    * @return {Boolean} true if the object was initialized properly
    */
   isInitialized: function TVP_isInitialized()
   {
     return this.renderer && this.renderer.context;
   },
 
   /**
+   * Starts executing a destruction animation and executes a callback function
+   * when finished.
+   *
+   * @param {Function} aCallback
+   *                   the destruction finished callback
+   */
+  executeDestruction: function TV_executeDestruction(aCallback)
+  {
+    if (!this.isExecutingDestruction) {
+      this.isExecutingDestruction = true;
+      this.onDestructionFinished = aCallback;
+
+      if (this.frames > OUTRO_TRANSITION_DURATION) {
+        this.frames = 0;
+        this.redraw = true;
+      } else {
+        aCallback();
+      }
+    }
+  },
+
+  /**
    * Function called when this object is destroyed.
    */
   finalize: function TVP_finalize()
   {
     TiltUtils.destroyObject(this.visualizationProgram);
     TiltUtils.destroyObject(this.texture);
 
     if (this.meshStacks) {
@@ -846,38 +898,72 @@ TiltVisualizer.Controller = function TV_
    * Object containing the rotation quaternion and the translation amount.
    */
   this.coordinates = null;
 
   // bind the owner object to the necessary functions
   TiltUtils.bindObjectFunc(this, "update");
   TiltUtils.bindObjectFunc(this, "^on");
 
-  // bind commonly used mouse and keyboard events with the controller
-  aCanvas.addEventListener("mousedown", this.onMouseDown, false);
-  aCanvas.addEventListener("mouseup", this.onMouseUp, false);
-  aCanvas.addEventListener("click", this.onMouseClick, false);
-  aCanvas.addEventListener("mousemove", this.onMouseMove, false);
-  aCanvas.addEventListener("mouseover", this.onMouseOver, false);
-  aCanvas.addEventListener("mouseout", this.onMouseOut, false);
-  aCanvas.addEventListener("MozMousePixelScroll", this.onMozScroll, false);
-  aCanvas.addEventListener("keydown", this.onKeyDown, false);
-  aCanvas.addEventListener("keyup", this.onKeyUp, false);
-  aCanvas.addEventListener("blur", this.onBlur, false);
-
-  // handle resize events to change the arcball dimensions
-  aPresenter.contentWindow.addEventListener("resize", this.onResize, false);
+  // add the necessary event listeners
+  this.addEventListeners();
 
   // attach this controller's update function to the presenter ondraw event
   aPresenter.ondraw = this.update;
 };
 
 TiltVisualizer.Controller.prototype = {
 
   /**
+   * Adds all added events listeners required by this controller.
+   */
+  addEventListeners: function TVC_addEventListeners()
+  {
+    let canvas = this.canvas;
+    let presenter = this.presenter;
+
+    // bind commonly used mouse and keyboard events with the controller
+    canvas.addEventListener("mousedown", this.onMouseDown, false);
+    canvas.addEventListener("mouseup", this.onMouseUp, false);
+    canvas.addEventListener("click", this.onMouseClick, false);
+    canvas.addEventListener("mousemove", this.onMouseMove, false);
+    canvas.addEventListener("mouseover", this.onMouseOver, false);
+    canvas.addEventListener("mouseout", this.onMouseOut, false);
+    canvas.addEventListener("MozMousePixelScroll", this.onMozScroll, false);
+    canvas.addEventListener("keydown", this.onKeyDown, false);
+    canvas.addEventListener("keyup", this.onKeyUp, false);
+    canvas.addEventListener("blur", this.onBlur, false);
+
+    // handle resize events to change the arcball dimensions
+    presenter.contentWindow.addEventListener("resize", this.onResize, false);
+  },
+
+  /**
+   * Removes all added events listeners required by this controller.
+   */
+  removeEventListeners: function TVC_removeEventListeners()
+  {
+    let canvas = this.canvas;
+    let presenter = this.presenter;
+
+    canvas.removeEventListener("mousedown", this.onMouseDown, false);
+    canvas.removeEventListener("mouseup", this.onMouseUp, false);
+    canvas.removeEventListener("click", this.onMouseClick, false);
+    canvas.removeEventListener("mousemove", this.onMouseMove, false);
+    canvas.removeEventListener("mouseover", this.onMouseOver, false);
+    canvas.removeEventListener("mouseout", this.onMouseOut, false);
+    canvas.removeEventListener("MozMousePixelScroll", this.onMozScroll, false);
+    canvas.removeEventListener("keydown", this.onKeyDown, false);
+    canvas.removeEventListener("keyup", this.onKeyUp, false);
+    canvas.removeEventListener("blur", this.onBlur, false);
+
+    presenter.contentWindow.removeEventListener("resize", this.onResize,false);
+  },
+
+  /**
    * Function called each frame, updating the visualization camera transforms.
    */
   update: function TVC_update()
   {
     this.coordinates = this.arcball.update();
 
     this.presenter.setRotation(this.coordinates.rotation);
     this.presenter.setTranslation(this.coordinates.translation);
@@ -987,43 +1073,42 @@ TiltVisualizer.Controller.prototype = {
 
   /**
    * Called when a key is pressed.
    */
   onKeyDown: function TVC_onKeyDown(e)
   {
     let code = e.keyCode || e.which;
 
-    if (code >= e.DOM_VK_LEFT && code <= e.DOM_VK_DOWN) {
+    if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
       e.preventDefault();
       e.stopPropagation();
-    }
-    if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
+      this.arcball.keyDown(code);
+    } else {
       this.arcball.cancelKeyEvents();
-    } else {
-      this.arcball.keyDown(code);
     }
   },
 
   /**
    * Called when a key is released.
    */
   onKeyUp: function TVC_onKeyUp(e)
   {
     let code = e.keyCode || e.which;
 
-    if (code >= e.DOM_VK_LEFT && code <= e.DOM_VK_DOWN) {
+    if (code === e.DOM_VK_ESCAPE) {
+      this.presenter.tiltUI.destroy(this.presenter.tiltUI.currentWindowId, 1);
+      return;
+    }
+
+    if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
       e.preventDefault();
       e.stopPropagation();
+      this.arcball.keyUp(code);
     }
-    if (code === e.DOM_VK_ESCAPE) {
-      this.presenter.tiltUI.destroy(this.presenter.tiltUI.currentWindowId);
-      return;
-    }
-    this.arcball.keyUp(code);
   },
 
   /**
    * Called when the canvas looses focus.
    */
   onBlur: function TVC_onBlur(e) {
     this.arcball.cancelKeyEvents();
   },
@@ -1050,34 +1135,21 @@ TiltVisualizer.Controller.prototype = {
     return this.arcball ? true : false;
   },
 
   /**
    * Function called when this object is destroyed.
    */
   finalize: function TVC_finalize()
   {
-    let canvas = this.canvas;
-    let presenter = this.presenter;
-
     TiltUtils.destroyObject(this.arcball);
     TiltUtils.destroyObject(this.coordinates);
 
-    canvas.removeEventListener("mousedown", this.onMouseDown, false);
-    canvas.removeEventListener("mouseup", this.onMouseUp, false);
-    canvas.removeEventListener("click", this.onMouseClick, false);
-    canvas.removeEventListener("mousemove", this.onMouseMove, false);
-    canvas.removeEventListener("mouseover", this.onMouseOver, false);
-    canvas.removeEventListener("mouseout", this.onMouseOut, false);
-    canvas.removeEventListener("MozMousePixelScroll", this.onMozScroll, false);
-    canvas.removeEventListener("keydown", this.onKeyDown, false);
-    canvas.removeEventListener("keyup", this.onKeyUp, false);
-    canvas.removeEventListener("blur", this.onBlur, false);
-    presenter.contentWindow.removeEventListener("resize", this.onResize,false);
-    presenter.ondraw = null;
+    this.removeEventListeners();
+    this.presenter.ondraw = null;
   }
 };
 
 /**
  * This is a general purpose 3D rotation controller described by Ken Shoemake
  * in the Graphics Interface ’92 Proceedings. It features good behavior
  * easy implementation, cheap execution.
  *
@@ -1118,33 +1190,36 @@ TiltVisualizer.Arcball = function TV_Arc
   this._endVec = vec3.create();
   this._pVec = vec3.create();
 
   /**
    * The corresponding rotation quaternions.
    */
   this._lastRot = quat4.create();
   this._deltaRot = quat4.create();
-  this._currentRot = quat4.create(aInitialRot);
+  this._currentRot = quat4.create();
 
   /**
    * The current camera translation coordinates.
    */
   this._lastTrans = vec3.create();
   this._deltaTrans = vec3.create();
-  this._currentTrans = vec3.create(aInitialTrans);
+  this._currentTrans = vec3.create();
   this._zoomAmount = 0;
 
   /**
    * Additional rotation and translation vectors.
    */
-  this._addKeyRot = vec3.create();
-  this._addKeyTrans = vec3.create();
-  this._deltaKeyRot = quat4.create();
-  this._deltaKeyTrans = vec3.create();
+  this._additionalRot = vec3.create(aInitialRot);
+  this._additionalTrans = vec3.create(aInitialTrans);
+  this._deltaAdditionalRot = quat4.create();
+  this._deltaAdditionalTrans = vec3.create();
+
+  // load the keys controlling the arcball
+  this._loadKeys();
 
   // set the current dimensions of the arcball
   this.resize(aWidth, aHeight, aRadius);
 };
 
 TiltVisualizer.Arcball.prototype = {
 
   /**
@@ -1242,78 +1317,85 @@ TiltVisualizer.Arcball.prototype = {
 
     let zoomAmount = this._zoomAmount;
     let keyCode = this._keyCode;
 
     // mouse wheel handles zooming
     deltaTrans[2] = (zoomAmount - currentTrans[2]) * ARCBALL_ZOOM_STEP;
     currentTrans[2] += deltaTrans[2];
 
-    let addKeyRot = this._addKeyRot;
-    let addKeyTrans = this._addKeyTrans;
-    let deltaKeyRot = this._deltaKeyRot;
-    let deltaKeyTrans = this._deltaKeyTrans;
+    let additionalRot = this._additionalRot;
+    let additionalTrans = this._additionalTrans;
+    let deltaAdditionalRot = this._deltaAdditionalRot;
+    let deltaAdditionalTrans = this._deltaAdditionalTrans;
+
+    let rotateKeys = this.rotateKeys;
+    let panKeys = this.panKeys;
+    let zoomKeys = this.zoomKeys;
+    let resetKey = this.resetKey;
 
     // handle additional rotation and translation by the keyboard
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_A]) {
-      addKeyRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+    if (keyCode[rotateKeys.left]) {
+      additionalRot[0] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_D]) {
-      addKeyRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+    if (keyCode[rotateKeys.right]) {
+      additionalRot[0] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_W]) {
-      addKeyRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+    if (keyCode[rotateKeys.up]) {
+      additionalRot[1] += ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_S]) {
-      addKeyRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
+    if (keyCode[rotateKeys.down]) {
+      additionalRot[1] -= ARCBALL_SENSITIVITY * ARCBALL_ROTATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_LEFT]) {
-      addKeyTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+    if (keyCode[panKeys.left]) {
+      additionalTrans[0] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_RIGHT]) {
-      addKeyTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+    if (keyCode[panKeys.right]) {
+      additionalTrans[0] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_UP]) {
-      addKeyTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+    if (keyCode[panKeys.up]) {
+      additionalTrans[1] -= ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_DOWN]) {
-      addKeyTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
+    if (keyCode[panKeys.down]) {
+      additionalTrans[1] += ARCBALL_SENSITIVITY * ARCBALL_TRANSLATION_STEP;
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_I] ||
-        keyCode[Ci.nsIDOMKeyEvent.DOM_VK_ADD] ||
-        keyCode[Ci.nsIDOMKeyEvent.DOM_VK_EQUALS]) {
+    if (keyCode[zoomKeys["in"][0]] ||
+        keyCode[zoomKeys["in"][1]] ||
+        keyCode[zoomKeys["in"][2]]) {
       this.zoom(-ARCBALL_TRANSLATION_STEP);
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_O] ||
-        keyCode[Ci.nsIDOMKeyEvent.DOM_VK_SUBTRACT]) {
+    if (keyCode[zoomKeys["out"][0]] ||
+        keyCode[zoomKeys["out"][1]]) {
       this.zoom(ARCBALL_TRANSLATION_STEP);
     }
-    if (keyCode[Ci.nsIDOMKeyEvent.DOM_VK_R] ||
-        keyCode[Ci.nsIDOMKeyEvent.DOM_VK_0]) {
+    if (keyCode[zoomKeys["unzoom"]]) {
       this._zoomAmount = 0;
     }
+    if (keyCode[resetKey]) {
+      this.reset();
+    }
 
     // update the delta key rotations and translations
-    deltaKeyRot[0] +=
-      (addKeyRot[0] - deltaKeyRot[0]) * ARCBALL_SENSITIVITY;
-    deltaKeyRot[1] +=
-      (addKeyRot[1] - deltaKeyRot[1]) * ARCBALL_SENSITIVITY;
+    deltaAdditionalRot[0] +=
+      (additionalRot[0] - deltaAdditionalRot[0]) * ARCBALL_SENSITIVITY;
+    deltaAdditionalRot[1] +=
+      (additionalRot[1] - deltaAdditionalRot[1]) * ARCBALL_SENSITIVITY;
+    deltaAdditionalRot[2] +=
+      (additionalRot[2] - deltaAdditionalRot[2]) * ARCBALL_SENSITIVITY;
 
-    deltaKeyTrans[0] +=
-      (addKeyTrans[0] - deltaKeyTrans[0]) * ARCBALL_SENSITIVITY;
-    deltaKeyTrans[1] +=
-      (addKeyTrans[1] - deltaKeyTrans[1]) * ARCBALL_SENSITIVITY;
+    deltaAdditionalTrans[0] +=
+      (additionalTrans[0] - deltaAdditionalTrans[0]) * ARCBALL_SENSITIVITY;
+    deltaAdditionalTrans[1] +=
+      (additionalTrans[1] - deltaAdditionalTrans[1]) * ARCBALL_SENSITIVITY;
 
     // create an additional rotation based on the key events
-    quat4.fromEuler(deltaKeyRot[0], deltaKeyRot[1], 0, deltaRot);
+    quat4.fromEuler(deltaAdditionalRot[0], deltaAdditionalRot[1], 0, deltaRot);
 
     // create an additional translation based on the key events
-    deltaTrans[0] = deltaKeyTrans[0];
-    deltaTrans[1] = deltaKeyTrans[1];
-    deltaTrans[2] = 0;
+    vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans);
 
     // return the current rotation and translation
     return {
       rotation: quat4.multiply(deltaRot, currentRot),
       translation: vec3.add(deltaTrans, currentTrans)
     };
   },
 
@@ -1329,16 +1411,17 @@ TiltVisualizer.Arcball.prototype = {
    *                 which mouse button was pressed
    */
   mouseDown: function TVA_mouseDown(x, y, aButton)
   {
     // save the mouse down state and prepare for rotations or translations
     this._mousePress[0] = x;
     this._mousePress[1] = y;
     this._mouseButton = aButton;
+    this._cancelResetInterval();
     this._save();
 
     // find the sphere coordinates of the mouse positions
     this.pointToSphere(
       x, y, this.width, this.height, this.radius, this._startVec);
 
     quat4.set(this._currentRot, this._lastRot);
   },
@@ -1404,29 +1487,31 @@ TiltVisualizer.Arcball.prototype = {
    * Call this, for example, when the mouse wheel was scrolled or zoom keys
    * were pressed.
    *
    * @param {Number} aZoom
    *                 the zoom direction and speed
    */
   zoom: function TVA_zoom(aZoom)
   {
+    this._cancelResetInterval();
     this._zoomAmount = TiltMath.clamp(this._zoomAmount - aZoom,
       ARCBALL_ZOOM_MIN, ARCBALL_ZOOM_MAX);
   },
 
   /**
    * Function handling the keyDown event.
    * Call this when a key was pressed.
    *
    * @param {Number} aCode
    *                 the code corresponding to the key pressed
    */
   keyDown: function TVA_keyDown(aCode)
   {
+    this._cancelResetInterval();
     this._keyCode[aCode] = true;
   },
 
   /**
    * Function handling the keyUp event.
    * Call this when a key was released.
    *
    * @param {Number} aCode
@@ -1479,21 +1564,31 @@ TiltVisualizer.Arcball.prototype = {
       aSphereVec[1] = y;
       aSphereVec[2] = Math.sqrt(1 - sqlength);
     }
   },
 
   /**
    * Cancels all pending transformations caused by key events.
    */
-  cancelKeyEvents: function TVA_cancelKeyEvents() {
+  cancelKeyEvents: function TVA_cancelKeyEvents()
+  {
     this._keyCode = {};
   },
 
   /**
+   * Cancels all pending transformations caused by mouse events.
+   */
+  cancelMouseEvents: function TVA_cancelMouseEvents()
+  {
+    this._rotating = false;
+    this._mouseButton = -1;
+  },
+
+  /**
    * Resize this implementation to use different bounds.
    * This function is automatically called when the arcball is created.
    *
    * @param {Number} newWidth
    *                 the new width of canvas
    * @param {Number} newHeight
    *                 the new  height of canvas
    * @param {Number} newRadius
@@ -1508,29 +1603,148 @@ TiltVisualizer.Arcball.prototype = {
     // set the new width, height and radius dimensions
     this.width = newWidth;
     this.height = newHeight;
     this.radius = newRadius ? newRadius : Math.min(newWidth, newHeight);
     this._save();
   },
 
   /**
+   * Starts an animation resetting the arcball transformations to identity.
+   *
+   * @param {Array} aFinalTranslation
+   *                optional, final vector translation
+   * @param {Array} aFinalRotation
+   *                optional, final quaternion rotation
+   */
+  reset: function TVA_reset(aFinalTranslation, aFinalRotation)
+  {
+    this.cancelMouseEvents();
+    this.cancelKeyEvents();
+
+    if (!this._resetInterval) {
+      let window = TiltUtils.getBrowserWindow();
+      let func = this._nextResetIntervalStep.bind(this);
+
+      vec3.zero(this._additionalTrans);
+      vec3.zero(this._additionalRot);
+
+      this._save();
+      this._resetFinalTranslation = vec3.create(aFinalTranslation);
+      this._resetFinalRotation = quat4.create(aFinalRotation);
+      this._resetInterval = window.setInterval(func, ARCBALL_RESET_INTERVAL);
+    }
+  },
+
+  /**
+   * Cancels the current arcball reset animation if there is one.
+   */
+  _cancelResetInterval: function TVA__cancelResetInterval()
+  {
+    if (this._resetInterval) {
+      let window = TiltUtils.getBrowserWindow();
+
+      window.clearInterval(this._resetInterval);
+      this._resetInterval = null;
+      this._save();
+
+      if ("function" === typeof this.onResetFinish) {
+        this.onResetFinish();
+        this.onResetFinish = null;
+      }
+    }
+  },
+
+  /**
+   * Executes the next step in the arcball reset animation.
+   */
+  _nextResetIntervalStep: function TVA__nextResetIntervalStep()
+  {
+    let fDelta = EPSILON * EPSILON;
+    let fTran = this._resetFinalTranslation;
+    let fRot = this._resetFinalRotation;
+
+    let t = vec3.create(fTran);
+    let r = quat4.multiply(quat4.inverse(quat4.create(this._currentRot)), fRot);
+
+    // reset the rotation quaternion and translation vector
+    vec3.lerp(this._currentTrans, t, ARCBALL_RESET_FACTOR);
+    quat4.slerp(this._currentRot, r, 1 - ARCBALL_RESET_FACTOR);
+
+    // also reset any additional transforms by the keyboard or mouse
+    this._zoomAmount *= ARCBALL_RESET_FACTOR;
+
+    // clear the loop if the all values are very close to zero
+    if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fDelta &&
+        vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fDelta &&
+        vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fDelta &&
+        vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fDelta &&
+        vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fDelta &&
+        vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fDelta) {
+
+      this._cancelResetInterval();
+    }
+  },
+
+  /**
+   * Loads the keys to control this arcball.
+   */
+  _loadKeys: function TVA__loadKeys() {
+
+    this.rotateKeys = {
+      "up": Ci.nsIDOMKeyEvent["DOM_VK_W"],
+      "down": Ci.nsIDOMKeyEvent["DOM_VK_S"],
+      "left": Ci.nsIDOMKeyEvent["DOM_VK_A"],
+      "right": Ci.nsIDOMKeyEvent["DOM_VK_D"],
+    };
+    this.panKeys = {
+      "up": Ci.nsIDOMKeyEvent["DOM_VK_UP"],
+      "down": Ci.nsIDOMKeyEvent["DOM_VK_DOWN"],
+      "left": Ci.nsIDOMKeyEvent["DOM_VK_LEFT"],
+      "right": Ci.nsIDOMKeyEvent["DOM_VK_RIGHT"],
+    };
+    this.zoomKeys = {
+      "in": [
+        Ci.nsIDOMKeyEvent["DOM_VK_I"],
+        Ci.nsIDOMKeyEvent["DOM_VK_ADD"],
+        Ci.nsIDOMKeyEvent["DOM_VK_EQUALS"],
+      ],
+      "out": [
+        Ci.nsIDOMKeyEvent["DOM_VK_O"],
+        Ci.nsIDOMKeyEvent["DOM_VK_SUBTRACT"],
+      ],
+      "unzoom": Ci.nsIDOMKeyEvent["DOM_VK_0"]
+    };
+    this.resetKey = Ci.nsIDOMKeyEvent["DOM_VK_R"];
+  },
+
+  /**
    * Saves the current arcball state, typically after resize or mouse events.
    */
   _save: function TVA__save()
   {
-    let x = this._mousePress[0];
-    let y = this._mousePress[1];
+    if (this._mousePress) {
+      let x = this._mousePress[0];
+      let y = this._mousePress[1];
 
-    this._mouseMove[0] = x;
-    this._mouseMove[1] = y;
-    this._mouseRelease[0] = x;
-    this._mouseRelease[1] = y;
-    this._mouseLerp[0] = x;
-    this._mouseLerp[1] = y;
+      this._mouseMove[0] = x;
+      this._mouseMove[1] = y;
+      this._mouseRelease[0] = x;
+      this._mouseRelease[1] = y;
+      this._mouseLerp[0] = x;
+      this._mouseLerp[1] = y;
+    }
+  },
+
+  /**
+   * Function called when this object is destroyed.
+   */
+  finalize: function TVA_finalize()
+  {
+    this._cancelResetInterval();
   }
 };
 
 /**
  * Tilt configuration preferences.
  */
 TiltVisualizer.Prefs = {
 
@@ -1543,24 +1757,49 @@ TiltVisualizer.Prefs = {
   },
 
   set enabled(value)
   {
     TiltUtils.Preferences.set("enabled", "boolean", value);
     this._enabled = value;
   },
 
+  get introTransition()
+  {
+    return this._introTransition;
+  },
+
+  set introTransition(value)
+  {
+    TiltUtils.Preferences.set("intro_transition", "boolean", value);
+    this._introTransition = value;
+  },
+
+  get outroTransition()
+  {
+    return this._outroTransition;
+  },
+
+  set outroTransition(value)
+  {
+    TiltUtils.Preferences.set("outro_transition", "boolean", value);
+    this._outroTransition = value;
+  },
+
   /**
    * Loads the preferences.
    */
   load: function TVC_load()
   {
-    let prefs = TiltUtils.Preferences;
+    let prefs = TiltVisualizer.Prefs;
+    let get = TiltUtils.Preferences.get;
 
-    TiltVisualizer.Prefs._enabled = prefs.get("enabled", "boolean");
+    prefs._enabled = get("enabled", "boolean");
+    prefs._introTransition = get("intro_transition", "boolean");
+    prefs._outroTransition = get("outro_transition", "boolean");
   }
 };
 
 /**
  * A custom visualization shader.
  *
  * @param {Attribute} vertexPosition: the vertex position
  * @param {Attribute} vertexTexCoord: texture coordinates used by the sampler
--- a/browser/devtools/tilt/test/Makefile.in
+++ b/browser/devtools/tilt/test/Makefile.in
@@ -44,20 +44,23 @@ relativesrcdir 	= browser/devtools/tilt/
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
 	head.js \
 	browser_tilt_01_lazy_getter.js \
 	browser_tilt_02_notifications.js \
 	browser_tilt_03_tab_switch.js \
+	browser_tilt_04_initialization-key.js \
 	browser_tilt_04_initialization.js \
 	browser_tilt_05_destruction-esc.js \
 	browser_tilt_05_destruction-url.js \
 	browser_tilt_05_destruction.js \
+	browser_tilt_arcball-reset-typeahead.js \
+	browser_tilt_arcball-reset.js \
 	browser_tilt_arcball.js \
 	browser_tilt_controller.js \
 	browser_tilt_gl01.js \
 	browser_tilt_gl02.js \
 	browser_tilt_gl03.js \
 	browser_tilt_gl04.js \
 	browser_tilt_gl05.js \
 	browser_tilt_gl06.js \
--- a/browser/devtools/tilt/test/browser_tilt_02_notifications.js
+++ b/browser/devtools/tilt/test/browser_tilt_02_notifications.js
@@ -25,16 +25,17 @@ function test() {
   gBrowser.tabContainer.addEventListener("TabSelect", tabSelect, false);
   createNewTab();
 }
 
 function createNewTab() {
   tab0 = gBrowser.selectedTab;
 
   tab1 = createTab(function() {
+    Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
     Services.obs.addObserver(tab_TILT_INITIALIZED, TILT_INITIALIZED, false);
     Services.obs.addObserver(tab_TILT_DESTROYED, TILT_DESTROYED, false);
     Services.obs.addObserver(tab_TILT_SHOWN, TILT_SHOWN, false);
     Services.obs.addObserver(tab_TILT_HIDDEN, TILT_HIDDEN, false);
 
     createTilt({
       onTiltOpen: function()
       {
@@ -65,33 +66,35 @@ let testSteps = [
   function step0() {
     gBrowser.selectedTab = tab0;
   },
   function step1() {
     gBrowser.selectedTab = tab1;
   },
   function step2() {
     Tilt.destroy(Tilt.currentWindowId);
-
-    Services.obs.removeObserver(tab_TILT_INITIALIZED, TILT_INITIALIZED, false);
-    Services.obs.removeObserver(tab_TILT_DESTROYED, TILT_DESTROYED, false);
-    Services.obs.removeObserver(tab_TILT_SHOWN, TILT_SHOWN, false);
-    Services.obs.removeObserver(tab_TILT_HIDDEN, TILT_HIDDEN, false);
-    gBrowser.removeCurrentTab();
-  },
-  function step3_cleanup() {
-    is(tabEvents, "ti;th;ts;td;",
-      "The notifications weren't fired in the correct order.");
-
-    tab0 = null;
-    tab1 = null;
-
-    gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, false);
-    finish();
   }
 ];
 
+function cleanup() {
+  is(tabEvents, "ti;th;ts;td;",
+    "The notifications weren't fired in the correct order.");
+
+  tab0 = null;
+  tab1 = null;
+
+  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  Services.obs.removeObserver(tab_TILT_INITIALIZED, TILT_INITIALIZED, false);
+  Services.obs.removeObserver(tab_TILT_DESTROYED, TILT_DESTROYED, false);
+  Services.obs.removeObserver(tab_TILT_SHOWN, TILT_SHOWN, false);
+  Services.obs.removeObserver(tab_TILT_HIDDEN, TILT_HIDDEN, false);
+
+  gBrowser.tabContainer.removeEventListener("TabSelect", tabSelect, false);
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
 function tabSelect() {
   if (testStep !== -1) {
     executeSoon(testSteps[testStep]);
     testStep++;
   }
 }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_04_initialization-key.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
+/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
+/*global Services, EventUtils, Tilt, TiltUtils, TiltVisualizer, InspectorUI */
+/*global Ci, TILT_INITIALIZED, TILT_DESTROYED, INSPECTOR_OPENED */
+"use strict";
+
+let id;
+let tiltKey;
+let eventType;
+
+function test() {
+  if (!isTiltEnabled()) {
+    info("Skipping initialization key test because Tilt isn't enabled.");
+    return;
+  }
+  if (!isWebGLSupported()) {
+    info("Skipping initialization key test because WebGL isn't supported.");
+    return;
+  }
+
+  waitForExplicitFinish();
+
+  id = TiltUtils.getWindowId(gBrowser.selectedBrowser.contentWindow);
+  tiltKey = Tilt.tiltButton.getAttribute("accesskey");
+
+  if ("nsILocalFileMac" in Ci) {
+    eventType = { type: "keypress", ctrlKey: true };
+  } else {
+    eventType = { type: "keypress", altKey: true };
+  }
+
+  Services.obs.addObserver(onInspectorOpen, INSPECTOR_OPENED, false);
+  InspectorUI.toggleInspectorUI();
+}
+
+function onInspectorOpen() {
+  Services.obs.removeObserver(onInspectorOpen, INSPECTOR_OPENED);
+
+  executeSoon(function() {
+    is(Tilt.visualizers[id], null,
+      "A instance of the visualizer shouldn't be initialized yet.");
+
+    info("Pressing the accesskey should open Tilt.");
+
+    Services.obs.addObserver(onTiltOpen, TILT_INITIALIZED, false);
+    EventUtils.synthesizeKey(tiltKey, eventType);
+  });
+}
+
+function onTiltOpen() {
+  Services.obs.removeObserver(onTiltOpen, TILT_INITIALIZED);
+
+  executeSoon(function() {
+    ok(Tilt.visualizers[id] instanceof TiltVisualizer,
+      "A new instance of the visualizer wasn't created properly.");
+    ok(Tilt.visualizers[id].isInitialized(),
+      "The new instance of the visualizer wasn't initialized properly.");
+
+    info("Pressing the accesskey again should close Tilt.");
+
+    Services.obs.addObserver(onTiltClose, TILT_DESTROYED, false);
+    EventUtils.synthesizeKey(tiltKey, eventType);
+  });
+}
+
+function onTiltClose() {
+  is(Tilt.visualizers[id], null,
+    "The current instance of the visualizer wasn't destroyed properly.");
+
+  InspectorUI.closeInspectorUI();
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*global ok, is, info, isApproxVec, waitForExplicitFinish, executeSoon, finish */
+/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
+/*global Services, EventUtils, InspectorUI, TiltVisualizer, TILT_DESTROYED */
+"use strict";
+
+function test() {
+  if (!isTiltEnabled()) {
+    info("Skipping part of the arcball test because Tilt isn't enabled.");
+    return;
+  }
+  if (!isWebGLSupported()) {
+    info("Skipping part of the arcball test because WebGL isn't supported.");
+    return;
+  }
+
+  waitForExplicitFinish();
+  Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
+
+  createTab(function() {
+    createTilt({
+      onTiltOpen: function(instance)
+      {
+        performTest(instance.presenter.canvas,
+                    instance.controller.arcball, function() {
+
+          info("Killing arcball reset test.");
+
+          Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+          InspectorUI.closeInspectorUI();
+        });
+      }
+    });
+  });
+}
+
+function performTest(canvas, arcball, callback) {
+  is(document.activeElement, canvas,
+    "The visualizer canvas should be focused when performing this test.");
+
+
+  info("Starting arcball reset test.");
+
+  // start translating and rotating sometime at random
+
+  executeSoon(function() {
+    info("Synthesizing key down events.");
+
+    EventUtils.synthesizeKey("VK_W", { type: "keydown" });
+    EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+
+    // wait for some arcball translations and rotations to happen
+
+    executeSoon(function() {
+      info("Synthesizing key up events.");
+
+      EventUtils.synthesizeKey("VK_W", { type: "keyup" });
+      EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
+
+      // ok, transformations finished, we can now try to reset the model view
+
+      executeSoon(function() {
+        info("Synthesizing arcball reset key press.");
+
+        arcball.onResetFinish = function() {
+          ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+            "The arcball _lastRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+            "The arcball _deltaRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+            "The arcball _currentRot field wasn't reset correctly.");
+
+          ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+            "The arcball _lastTrans field wasn't reset correctly.");
+          ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+            "The arcball _deltaTrans field wasn't reset correctly.");
+          ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+            "The arcball _currentTrans field wasn't reset correctly.");
+
+          ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+            "The arcball _additionalRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+            "The arcball _additionalTrans field wasn't reset correctly.");
+
+          ok(isApproxVec([arcball._zoomAmount], [0]),
+            "The arcball _zoomAmount field wasn't reset correctly.");
+
+          info("Finishing arcball reset test.");
+          callback();
+        };
+
+        EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+      });
+    });
+  });
+}
+
+function cleanup() { /*global gBrowser */
+  info("Cleaning up arcball reset test.");
+
+  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  gBrowser.removeCurrentTab();
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*global ok, is, info, isApproxVec, waitForExplicitFinish, executeSoon, finish */
+/*global isTiltEnabled, isWebGLSupported, createTab, createTilt */
+/*global Services, EventUtils, InspectorUI, TiltVisualizer, TILT_DESTROYED */
+"use strict";
+
+function test() {
+  if (!isTiltEnabled()) {
+    info("Skipping part of the arcball test because Tilt isn't enabled.");
+    return;
+  }
+  if (!isWebGLSupported()) {
+    info("Skipping part of the arcball test because WebGL isn't supported.");
+    return;
+  }
+
+  waitForExplicitFinish();
+
+  createTab(function() {
+    createTilt({
+      onTiltOpen: function(instance)
+      {
+        performTest(instance.presenter.canvas,
+                    instance.controller.arcball, function() {
+
+          info("Killing arcball reset test.");
+
+          Services.obs.addObserver(cleanup, TILT_DESTROYED, false);
+          InspectorUI.closeInspectorUI();
+        });
+      }
+    });
+  });
+}
+
+function performTest(canvas, arcball, callback) {
+  is(document.activeElement, canvas,
+    "The visualizer canvas should be focused when performing this test.");
+
+
+  info("Starting arcball reset test.");
+
+  // start translating and rotating sometime at random
+
+  executeSoon(function() {
+    info("Synthesizing key down events.");
+
+    EventUtils.synthesizeKey("VK_W", { type: "keydown" });
+    EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
+
+    // wait for some arcball translations and rotations to happen
+
+    executeSoon(function() {
+      info("Synthesizing key up events.");
+
+      EventUtils.synthesizeKey("VK_W", { type: "keyup" });
+      EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
+
+      // ok, transformations finished, we can now try to reset the model view
+
+      executeSoon(function() {
+        info("Synthesizing arcball reset key press.");
+
+        arcball.onResetFinish = function() {
+          ok(isApproxVec(arcball._lastRot, [0, 0, 0, 1]),
+            "The arcball _lastRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._deltaRot, [0, 0, 0, 1]),
+            "The arcball _deltaRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._currentRot, [0, 0, 0, 1]),
+            "The arcball _currentRot field wasn't reset correctly.");
+
+          ok(isApproxVec(arcball._lastTrans, [0, 0, 0]),
+            "The arcball _lastTrans field wasn't reset correctly.");
+          ok(isApproxVec(arcball._deltaTrans, [0, 0, 0]),
+            "The arcball _deltaTrans field wasn't reset correctly.");
+          ok(isApproxVec(arcball._currentTrans, [0, 0, 0]),
+            "The arcball _currentTrans field wasn't reset correctly.");
+
+          ok(isApproxVec(arcball._additionalRot, [0, 0, 0]),
+            "The arcball _additionalRot field wasn't reset correctly.");
+          ok(isApproxVec(arcball._additionalTrans, [0, 0, 0]),
+            "The arcball _additionalTrans field wasn't reset correctly.");
+
+          ok(isApproxVec([arcball._zoomAmount], [0]),
+            "The arcball _zoomAmount field wasn't reset correctly.");
+
+          info("Finishing arcball reset test.");
+          callback();
+        };
+
+        EventUtils.synthesizeKey("VK_R", { type: "keydown" });
+      });
+    });
+  });
+}
+
+function cleanup() { /*global gBrowser */
+  info("Cleaning up arcball reset test.");
+
+  Services.obs.removeObserver(cleanup, TILT_DESTROYED);
+  gBrowser.removeCurrentTab();
+  finish();
+}
--- a/browser/devtools/tilt/test/browser_tilt_arcball.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball.js
@@ -1,29 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-/*global ok, is, isApproxVec, vec3, quat4, TiltVisualizer */
-/*global Cc, Ci, Cu */
+/*global ok, is, info, isApprox, isApproxVec, vec3, quat4 */
+/*global TiltVisualizer */
 "use strict";
 
 function cloneUpdate(update) {
   return {
     rotation: quat4.create(update.rotation),
     translation: vec3.create(update.translation)
   };
 }
 
 function isExpectedUpdate(update1, update2) {
   if (update1.length !== update2.length) {
     return false;
   }
   for (let i = 0, len = update1.length; i < len; i++) {
     if (!isApproxVec(update1[i].rotation, update2[i].rotation) ||
         !isApproxVec(update1[i].translation, update2[i].translation)) {
+      info("isExpectedUpdate expected " + JSON.stringify(update1), ", got " +
+                                          JSON.stringify(update2) + " instead.");
       return false;
     }
   }
   return true;
 }
 
 function test() {
   let arcball1 = new TiltVisualizer.Arcball(123, 456);
@@ -52,54 +54,54 @@ function test() {
   arcball3.pointToSphere(123, 456, 256, 512, 512, sphereVec);
 
   ok(isApproxVec(sphereVec, [-0.009765625, 0.390625, 0.9204980731010437]),
     "The pointToSphere() function didn't map the coordinates correctly.");
 
   let stack1 = [];
   let expect1 = [
     { rotation: [
-      -0.054038457572460175, 0.015347825363278389,
-      -0.02533721923828125, -0.9980993270874023],
+      -0.08877250552177429, 0.0242881178855896,
+      -0.04222869873046875, -0.9948599338531494],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.09048379212617874, 0.024709727615118027,
-      -0.04307326674461365, -0.9946591854095459],
+      -0.13086390495300293, 0.03413732722401619,
+      -0.06334304809570312, -0.9887855648994446],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.11537143588066101, 0.03063894622027874,
-      -0.05548851564526558, -0.9912980198860168],
+      -0.15138940513134003, 0.03854173421859741,
+      -0.07390022277832031, -0.9849540591239929],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.13250185549259186, 0.03449848294258118,
-      -0.0641791820526123, -0.9885009527206421],
+      -0.1615273654460907, 0.040619146078825,
+      -0.0791788101196289, -0.9828477501869202],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.14435507357120514, 0.037062086164951324,
-      -0.07026264816522598, -0.9863321781158447],
+      -0.16656573116779327, 0.04162723943591118,
+      -0.0818181037902832, -0.9817478656768799],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.15258607268333435, 0.03879034146666527,
-      -0.07452107220888138, -0.9847128391265869],
+      -0.16907735168933868, 0.042123712599277496,
+      -0.08313775062561035, -0.9811863303184509],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.1583157479763031, 0.03996811807155609,
-      -0.07750196009874344, -0.9835304617881775],
+      -0.17033125460147858, 0.042370058596134186,
+      -0.08379757404327393, -0.9809026718139648],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.16231097280979156, 0.04077700152993202,
-      -0.07958859205245972, -0.982679009437561],
+      -0.17095772922039032, 0.04249274358153343,
+      -0.08412748575210571, -0.9807600975036621],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.16510005295276642, 0.04133564606308937,
-      -0.08104922622442245, -0.9820714592933655],
+      -0.17127084732055664, 0.04255397245287895,
+      -0.0842924416065216, -0.9806886315345764],
       translation: [0, 0, 0] },
     { rotation: [
-      -0.16704875230789185, 0.04172303527593613,
-      -0.08207167685031891, -0.9816405177116394],
+      -0.171427384018898, 0.042584557086229324,
+      -0.08437491953372955, -0.9806528687477112],
       translation: [0, 0, 0] }];
 
   arcball3.mouseDown(10, 10, 1);
   arcball3.mouseMove(10, 100);
   for (let i1 = 0; i1 < 10; i1++) {
     stack1.push(cloneUpdate(arcball3.update()));
   }
 
@@ -246,24 +248,24 @@ function test() {
       -0.17158377170562744, 0.04261511191725731,
       -0.08445732295513153, -0.980617105960846],
       translation: [0, 0, -8.649148941040039] },
     { rotation: [
       -0.17158380150794983, 0.04261511191725731,
       -0.08445733785629272, -0.980617105960846],
       translation: [0, 0, -8.784234046936035] }];
 
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_A);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_D);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_W);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_S);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_LEFT);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_UP);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
+  arcball3.keyDown(arcball3.rotateKeys.left);
+  arcball3.keyDown(arcball3.rotateKeys.right);
+  arcball3.keyDown(arcball3.rotateKeys.up);
+  arcball3.keyDown(arcball3.rotateKeys.down);
+  arcball3.keyDown(arcball3.panKeys.left);
+  arcball3.keyDown(arcball3.panKeys.right);
+  arcball3.keyDown(arcball3.panKeys.up);
+  arcball3.keyDown(arcball3.panKeys.down);
   for (let i4 = 0; i4 < 10; i4++) {
     stack4.push(cloneUpdate(arcball3.update()));
   }
 
   ok(isExpectedUpdate(stack4, expect4),
     "Key down events didn't create the expected transformation results.");
 
   let stack5 = [];
@@ -304,24 +306,24 @@ function test() {
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, -9.528986930847168] },
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, -9.576087951660156] }];
 
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_A);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_D);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_W);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_S);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_LEFT);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_RIGHT);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_UP);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_DOWN);
+  arcball3.keyUp(arcball3.rotateKeys.left);
+  arcball3.keyUp(arcball3.rotateKeys.right);
+  arcball3.keyUp(arcball3.rotateKeys.up);
+  arcball3.keyUp(arcball3.rotateKeys.down);
+  arcball3.keyUp(arcball3.panKeys.left);
+  arcball3.keyUp(arcball3.panKeys.right);
+  arcball3.keyUp(arcball3.panKeys.up);
+  arcball3.keyUp(arcball3.panKeys.down);
   for (let i5 = 0; i5 < 10; i5++) {
     stack5.push(cloneUpdate(arcball3.update()));
   }
 
   ok(isExpectedUpdate(stack5, expect5),
     "Key up events didn't create the expected transformation results.");
 
   let stack6 = [];
@@ -362,25 +364,25 @@ function test() {
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 90.76139831542969] },
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 112.18525695800781] }];
 
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_I);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_ADD);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_EQUALS);
+  arcball3.keyDown(arcball3.zoomKeys["in"][0]);
+  arcball3.keyDown(arcball3.zoomKeys["in"][1]);
+  arcball3.keyDown(arcball3.zoomKeys["in"][2]);
   for (let i6 = 0; i6 < 10; i6++) {
     stack6.push(cloneUpdate(arcball3.update()));
   }
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_I);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_ADD);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_EQUALS);
+  arcball3.keyUp(arcball3.zoomKeys["in"][0]);
+  arcball3.keyUp(arcball3.zoomKeys["in"][1]);
+  arcball3.keyUp(arcball3.zoomKeys["in"][2]);
 
   ok(isExpectedUpdate(stack6, expect6),
     "Key zoom in events didn't create the expected transformation results.");
 
   let stack7 = [];
   let expect7 = [
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
@@ -418,23 +420,23 @@ function test() {
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 151.1427459716797] },
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 138.52847290039062] }];
 
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_O);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_SUBTRACT);
+  arcball3.keyDown(arcball3.zoomKeys["out"][0]);
+  arcball3.keyDown(arcball3.zoomKeys["out"][1]);
   for (let i7 = 0; i7 < 10; i7++) {
     stack7.push(cloneUpdate(arcball3.update()));
   }
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_O);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_SUBTRACT);
+  arcball3.keyUp(arcball3.zoomKeys["out"][0]);
+  arcball3.keyUp(arcball3.zoomKeys["out"][1]);
 
   ok(isExpectedUpdate(stack7, expect7),
     "Key zoom out events didn't create the expected transformation results.");
 
   let stack8 = [];
   let expect8 = [
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
@@ -472,28 +474,26 @@ function test() {
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 53.238304138183594] },
     { rotation: [
       -0.17158392071723938, 0.0426151417195797,
       -0.0844573974609375, -0.980617105960846],
       translation: [0, 0, 47.91447448730469] }];
 
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_R);
-  arcball3.keyDown(Ci.nsIDOMKeyEvent.DOM_VK_0);
+  arcball3.keyDown(arcball3.zoomKeys["unzoom"]);
   for (let i8 = 0; i8 < 10; i8++) {
     stack8.push(cloneUpdate(arcball3.update()));
   }
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_R);
-  arcball3.keyUp(Ci.nsIDOMKeyEvent.DOM_VK_0);
+  arcball3.keyUp(arcball3.zoomKeys["unzoom"]);
 
   ok(isExpectedUpdate(stack8, expect8),
     "Key zoom reset events didn't create the expected transformation results.");
 
 
   arcball3.resize(123, 456);
   is(arcball3.width, 123,
-    "The arcball width wasn't updated correctly.");
+    "The third arcball width wasn't updated correctly.");
   is(arcball3.height, 456,
-    "The arcball height wasn't updated correctly.");
+    "The third arcball height wasn't updated correctly.");
   is(arcball3.radius, 123,
-    "The arcball radius wasn't implicitly updated correctly.");
+    "The third arcball radius wasn't implicitly updated correctly.");
 }
--- a/browser/devtools/tilt/test/browser_tilt_controller.js
+++ b/browser/devtools/tilt/test/browser_tilt_controller.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /*global ok, is, info, waitForExplicitFinish, finish, executeSoon, gBrowser */
 /*global isEqualVec, isTiltEnabled, isWebGLSupported, createTab, createTilt */
-/*global EventUtils, vec3, mat4, quat4 */
+/*global Services, EventUtils, vec3, mat4, quat4 */
 "use strict";
 
 function test() {
   if (!isTiltEnabled()) {
     info("Skipping controller test because Tilt isn't enabled.");
     return;
   }
   if (!isWebGLSupported()) {
@@ -41,16 +41,19 @@ function test() {
 
         ok(isEqualVec(tran(), prev_tran),
           "At init, the translation should be zero.");
         ok(isEqualVec(rot(), prev_rot),
           "At init, the rotation should be zero.");
 
 
         function testEventCancel(cancellingEvent) {
+          is(document.activeElement, canvas,
+            "The visualizer canvas should be focused when performing this test.");
+
           EventUtils.synthesizeKey("VK_A", { type: "keydown" });
           EventUtils.synthesizeKey("VK_LEFT", { type: "keydown" });
           instance.controller.update();
 
           ok(!isEqualVec(tran(), prev_tran),
             "After a translation key is pressed, the vector should change.");
           ok(!isEqualVec(rot(), prev_rot),
             "After a rotation key is pressed, the quaternion should change.");
@@ -74,28 +77,50 @@ function test() {
             instance.controller.update();
             save();
           }
 
           ok(isEqualVec(tran(), prev_tran) && isEqualVec(rot(), prev_rot),
             "After focus lost, the transforms inertia eventually stops.");
         }
 
+        info("Setting typeaheadfind to true.");
+
+        Services.prefs.setBoolPref("accessibility.typeaheadfind", true);
         testEventCancel(function() {
           EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
         });
         testEventCancel(function() {
           EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
         });
         testEventCancel(function() {
           EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
         });
         testEventCancel(function() {
           EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
         });
+
+        info("Setting typeaheadfind to false.");
+
+        Services.prefs.setBoolPref("accessibility.typeaheadfind", false);
+        testEventCancel(function() {
+          EventUtils.synthesizeKey("T", { type: "keydown", altKey: 1 });
+        });
+        testEventCancel(function() {
+          EventUtils.synthesizeKey("I", { type: "keydown", ctrlKey: 1 });
+        });
+        testEventCancel(function() {
+          EventUtils.synthesizeKey("L", { type: "keydown", metaKey: 1 });
+        });
+        testEventCancel(function() {
+          EventUtils.synthesizeKey("T", { type: "keydown", shiftKey: 1 });
+        });
+
+        info("Testing if loosing focus halts any stacked arcball animations.");
+
         testEventCancel(function() {
           gBrowser.selectedBrowser.contentWindow.focus();
         });
       },
       onEnd: function()
       {
         gBrowser.removeCurrentTab();
         finish();
--- a/browser/devtools/tilt/test/browser_tilt_utils06.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils06.js
@@ -38,9 +38,13 @@ function test() {
     "The finalize function wasn't called when an object was destroyed.");
 
 
   TiltUtils.destroyObject(someObject);
   is(typeof someObject.a, "undefined",
     "Not all members of the destroyed object were deleted.");
   is(typeof someObject.func, "undefined",
     "Not all function members of the destroyed object were deleted.");
+
+
+  is(TiltUtils.getBrowserWindow(), window,
+    "The getBrowserWindow() function didn't return the correct window.");
 }
--- a/browser/themes/gnomestripe/browser.css
+++ b/browser/themes/gnomestripe/browser.css
@@ -1984,17 +1984,16 @@ panel[dimmed="true"] {
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   border-top: 1px solid hsla(210, 8%, 5%, .65);
 }
 
 #inspector-toolbar[treepanel-open] {
   padding-top: 0;
-  -moz-padding-end: 0;
 }
 
 #devtools-side-splitter {
   -moz-appearance: none;
   border: 0;
   -moz-border-start: 1px solid #242b33;
   min-width: 0;
   width: 3px;
@@ -2002,41 +2001,25 @@ panel[dimmed="true"] {
   -moz-margin-end: -3px;
   position: relative;
 }
 
 #devtools-sidebar-box {
   background-color: -moz-Field;
 }
 
-/* Highlighter - toolbar resizers */
+/* Highlighter - toolbar resizer */
 
-.inspector-resizer {
+#inspector-top-resizer {
   -moz-appearance: none;
   cursor: n-resize;
-}
-
-#inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
-#inspector-end-resizer {
-  width: 12px;
-  height: 8px;
-  background-image: -moz-linear-gradient(top, black 1px, rgba(255,255,255,0.2) 1px);
-  background-size: 10px 2px;
-  background-clip: padding-box;
-  background-repeat: repeat-y;
-  border-width: 1px 1px 0;
-  border-style: solid;
-  border-color: rgba(255, 255, 255, 0.05);
-  margin: 7px 7px 8px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 #highlighter-nodeinfobar-tagname {
   color: white;
 }
 
--- a/browser/themes/pinstripe/browser.css
+++ b/browser/themes/pinstripe/browser.css
@@ -2715,21 +2715,34 @@ panel[dimmed="true"] {
   box-shadow: 0 0 0 1px black;
   outline-color: white;
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   border-top: 1px solid hsla(210, 8%, 5%, .65);
-  padding: 4px 16px 4px 0; /* use -moz-padding-end: 16px when/if bug 631729 gets fixed */
+  padding-top: 4px;
+  padding-bottom: 4px;
+}
+
+#inspector-toolbar:-moz-locale-dir(ltr) {
+  padding-left: 2px;
+  padding-right: 16px; /* use -moz-padding-end when/if bug 631729 gets fixed */
+}
+
+#inspector-toolbar:-moz-locale-dir(rtl) {
+  padding-left: 4px;
+  padding-right: 18px; /* use -moz-padding-end when/if bug 631729 gets fixed */
 }
 
 #inspector-toolbar[treepanel-open] {
-  padding: 0 0 4px;
+  padding-top: 0;
+  padding-right: 0;
+  -moz-padding-end: 4px;
 }
 
 #devtools-side-splitter {
   background-image: none !important;
   border: 0;
   -moz-border-start: 1px solid #242b33;
   min-width: 0;
   width: 3px;
@@ -2737,41 +2750,25 @@ panel[dimmed="true"] {
   -moz-margin-end: -3px;
   position: relative;
 }
 
 #devtools-sidebar-box {
   background-color: -moz-Field;
 }
 
-/* Highlighter - toolbar resizers */
-
-.inspector-resizer {
+/* Highlighter - toolbar resizer */
+
+#inspector-top-resizer {
   -moz-appearance: none;
   cursor: n-resize;
-}
-
-#inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
-#inspector-end-resizer {
-  width: 12px;
-  height: 8px;
-  background-image: -moz-linear-gradient(top, black 1px, rgba(255,255,255,0.2) 1px);
-  background-size: 10px 2px;
-  background-clip: padding-box;
-  background-repeat: repeat-y;
-  border-width: 1px 1px 0;
-  border-style: solid;
-  border-color: rgba(255, 255, 255, 0.05);
-  margin: 7px 7px 8px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 #highlighter-nodeinfobar-tagname {
   color: white;
 }
 
--- a/browser/themes/winstripe/browser.css
+++ b/browser/themes/winstripe/browser.css
@@ -2665,58 +2665,41 @@ panel[dimmed="true"] {
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   border-top: 1px solid hsla(211,68%,6%,.65) !important;
 }
 
 #inspector-toolbar[treepanel-open] {
   padding-top: 0;
-  -moz-padding-end: 0;
 }
 
 #devtools-side-splitter {
   border: 0;
   -moz-border-start: 1px solid #242b33;
   min-width: 0;
   width: 3px;
   background-color: transparent;
   -moz-margin-end: -3px;
   position: relative;
 }
 
 #devtools-sidebar-box {
   background-color: -moz-Field;
 }
 
-/* Highlighter - toolbar resizers */
-
-.inspector-resizer {
+/* Highlighter - toolbar resizer */
+
+#inspector-top-resizer {
   -moz-appearance: none;
   cursor: n-resize;
-}
-
-#inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
-#inspector-end-resizer {
-  width: 12px;
-  height: 8px;
-  background-image: -moz-linear-gradient(top, black 1px, rgba(255,255,255,0.2) 1px);
-  background-size: 10px 2px;
-  background-clip: padding-box;
-  background-repeat: repeat-y;
-  border-width: 1px 1px 0;
-  border-style: solid;
-  border-color: rgba(255, 255, 255, 0.05);
-  margin: 7px 7px 8px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 #highlighter-nodeinfobar-tagname {
   color: white;
 }
 
--- a/configure.in
+++ b/configure.in
@@ -4904,19 +4904,17 @@ cairo-android)
     AC_DEFINE(MOZ_WIDGET_ANDROID)
     AC_DEFINE(MOZ_TOUCH)
     MOZ_WIDGET_TOOLKIT=android
     TK_CFLAGS='$(MOZ_CAIRO_CFLAGS)'
     TK_LIBS='$(MOZ_CAIRO_LIBS)'
     MOZ_WEBGL=1
     MOZ_PDF_PRINTING=1
     MOZ_INSTRUMENT_EVENT_LOOP=1
-    if test "$MOZ_BUILD_APP" = "mobile/xul"; then
-        MOZ_OLD_LINKER=1
-    fi
+    MOZ_OLD_LINKER=1
     MOZ_TOUCH=1
     ;;
 
 cairo-gonk)
     AC_DEFINE(MOZ_WIDGET_GONK)
     MOZ_WIDGET_TOOLKIT=gonk
     TK_CFLAGS='$(MOZ_CAIRO_CFLAGS)'
     TK_LIBS='$(MOZ_CAIRO_LIBS)'
--- a/mozglue/android/APKOpen.cpp
+++ b/mozglue/android/APKOpen.cpp
@@ -754,17 +754,17 @@ static void loadSQLiteLibs(const char *a
 
 #ifdef MOZ_CRASHREPORTER
   file_ids = (char *)extractBuf("lib.id", zip);
 #endif
 
 #ifndef MOZ_OLD_LINKER
   char *file = new char[strlen(apkName) + sizeof("!/mozsqlite3.so")];
   sprintf(file, "%s!/mozsqlite3.so", apkName);
-  __wrap_dlopen(file, RTLD_GLOBAL | RTLD_LAZY);
+  sqlite_handle = __wrap_dlopen(file, RTLD_GLOBAL | RTLD_LAZY);
   delete [] file;
 #else
 #define MOZLOAD(name) mozload("lib" name ".so", zip)
   sqlite_handle = MOZLOAD("mozsqlite3");
 #undef MOZLOAD
 #endif
 
   delete zip;
--- a/toolkit/components/places/tests/unit/test_download_history.js
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -119,50 +119,46 @@ add_test(function test_dh_addDownload_re
     uri: REFERRER_URI,
     visits: [{
       transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
       visitDate: Date.now() * 1000
     }]
   });
 });
 
-add_test(function test_dh_addDownload_privateBrowsing()
-{
-  if (!("@mozilla.org/privatebrowsing;1" in Cc)) {
-    todo(false, "PB service is not available, bail out");
-    run_next_test();
-    return;
-  }
+if ("@mozilla.org/privatebrowsing;1" in Cc) {
+  add_test(function test_dh_addDownload_privateBrowsing()
+  {
+    waitForOnVisit(function DHAD_onVisit(aURI) {
+      // We should only receive the notification for the non-private URI.  This
+      // test is based on the assumption that visit notifications are received
+      // in the same order of the addDownload calls, which is currently true
+      // because database access is serialized on the same worker thread.
+      do_check_true(aURI.equals(DOWNLOAD_URI));
 
-  waitForOnVisit(function DHAD_onVisit(aURI) {
-    // We should only receive the notification for the non-private URI.  This
-    // test is based on the assumption that visit notifications are received in
-    // the same order of the addDownload calls, which is currently true because
-    // database access is serialized on the same worker thread.
-    do_check_true(aURI.equals(DOWNLOAD_URI));
+      uri_in_db(DOWNLOAD_URI, true);
+      uri_in_db(PRIVATE_URI, false);
 
-    uri_in_db(DOWNLOAD_URI, true);
-    uri_in_db(PRIVATE_URI, false);
-
-    waitForClearHistory(run_next_test);
-  });
+      waitForClearHistory(run_next_test);
+    });
 
-  let pb = Cc["@mozilla.org/privatebrowsing;1"]
-           .getService(Ci.nsIPrivateBrowsingService);
-  Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session",
-                             true);
-  pb.privateBrowsingEnabled = true;
-  gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
+    let pb = Cc["@mozilla.org/privatebrowsing;1"]
+             .getService(Ci.nsIPrivateBrowsingService);
+    Services.prefs.setBoolPref("browser.privatebrowsing.keep_current_session",
+                               true);
+    pb.privateBrowsingEnabled = true;
+    gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
 
-  // The addDownload functions calls CanAddURI synchronously, thus we can exit
-  // Private Browsing Mode immediately.
-  pb.privateBrowsingEnabled = false;
-  Services.prefs.clearUserPref("browser.privatebrowsing.keep_current_session");
-  gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
-});
+    // The addDownload functions calls CanAddURI synchronously, thus we can
+    // exit Private Browsing Mode immediately.
+    pb.privateBrowsingEnabled = false;
+    Services.prefs.clearUserPref("browser.privatebrowsing.keep_current_session");
+    gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
+  });
+}
 
 add_test(function test_dh_addDownload_disabledHistory()
 {
   waitForOnVisit(function DHAD_onVisit(aURI) {
     // We should only receive the notification for the non-private URI.  This
     // test is based on the assumption that visit notifications are received in
     // the same order of the addDownload calls, which is currently true because
     // database access is serialized on the same worker thread.
@@ -172,19 +168,20 @@ add_test(function test_dh_addDownload_di
     uri_in_db(PRIVATE_URI, false);
 
     waitForClearHistory(run_next_test);
   });
 
   Services.prefs.setBoolPref("places.history.enabled", false);
   gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
 
-  // The addDownload functions calls CanAddURI synchronously, thus we can reset
-  // the preference immediately.
-  Services.prefs.clearUserPref("places.history.enabled");
+  // The addDownload functions calls CanAddURI synchronously, thus we can set
+  // the preference back to true immediately (not all apps enable places by
+  // default).
+  Services.prefs.setBoolPref("places.history.enabled", true);
   gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
 });
 
 /**
  * Tests that nsIDownloadHistory::AddDownload saves the additional download
  * details if the optional destination URL is specified.
  */
 add_test(function test_dh_details()