Merge central to inbound
authorMarco Bonardo <mbonardo@mozilla.com>
Mon, 27 Feb 2012 13:46:22 +0100
changeset 87841 c6fe976aac16f404887a5fc725f10e355328cc43
parent 87840 8ea9dc2f857031a0642d9ad6bc28c0b1ca8a7278 (current diff)
parent 87816 7d7179d2d8096d5ef290779da549d0d11c1fea92 (diff)
child 87842 f91b74bfcb1fb8ef99bba7f11535c23364896b01
push id22160
push usermbrubeck@mozilla.com
push dateTue, 28 Feb 2012 17:21:33 +0000
treeherdermozilla-central@dde4e0089a18 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone13.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge central to inbound
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1023,16 +1023,19 @@ pref("services.sync.prefs.sync.xpinstall
 #endif
 
 // Disable the error console
 pref("devtools.errorconsole.enabled", false);
 
 // Enable the Inspector
 pref("devtools.inspector.enabled", true);
 pref("devtools.inspector.htmlHeight", 112);
+pref("devtools.inspector.htmlPanelOpen", false);
+pref("devtools.inspector.sidebarOpen", false);
+pref("devtools.inspector.activeSidebar", "ruleview");
 
 // Enable the Debugger
 pref("devtools.debugger.enabled", false);
 
 // The default Debugger UI height
 pref("devtools.debugger.ui.height", 250);
 
 // Enable the style inspector
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -20,16 +20,17 @@
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
 #   Ben Goodger <ben@bengoodger.com> (v2.0)
 #   Blake Ross <blakeross@telocity.com>
 #   Shawn Wilsher <me@shawnwilsher.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Rob Campbell <rcampbell@mozilla.com>
+#   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
@@ -148,31 +149,39 @@
 
   <commandset id="inspectorCommands">
     <command id="Inspector:Inspect"
              oncommand="InspectorUI.toggleInspection();"/>
     <command id="Inspector:Sidebar"
              oncommand="InspectorUI.toggleSidebar();"/>
     <command id="Inspector:Tilt"
              oncommand="Tilt.initialize();"/>
+    <command id="Inspector:HTMLPanel"
+             oncommand="InspectorUI.toggleHTMLPanel();"/>
+    <command id="Inspector:CopyInner"
+             oncommand="InspectorUI.copyInnerHTML();"/>
+    <command id="Inspector:CopyOuter"
+             oncommand="InspectorUI.copyOuterHTML();"/>
+    <command id="Inspector:DeleteNode"
+             oncommand="InspectorUI.deleteNode();"/>
   </commandset>
 
   <broadcasterset id="mainBroadcasterSet">
     <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
                  oncommand="toggleSidebar('viewBookmarksSidebar');"/>
 
     <!-- for both places and non-places, the sidebar lives at
          chrome://browser/content/history/history-panel.xul so there are no
          problems when switching between versions -->
     <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
                  type="checkbox" group="sidebar"
                  sidebarurl="chrome://browser/content/history/history-panel.xul"
                  oncommand="toggleSidebar('viewHistorySidebar');"/>
-                 
+
     <broadcaster id="viewWebPanelsSidebar" autoCheck="false"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/web-panels.xul"
                  oncommand="toggleSidebar('viewWebPanelsSidebar');"/>
 
     <!-- popup blocking menu items -->
     <broadcaster id="blockedPopupAllowSite"
                  accesskey="&allowPopups.accesskey;"
                  oncommand="gPopupBlockerObserver.toggleAllowPopupsForSite(event);"/>
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -33,16 +33,17 @@
 #   Dão Gottwald <dao@mozilla.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Robert Strong <robert.bugzilla@gmail.com>
 #   Rob Campbell <rcampbell@mozilla.com>
 #   Patrick Walton <pcwalton@mozilla.com>
 #   David Dahl <ddahl@mozilla.com>
 #   Frank Yan <fyan@mozilla.com>
 #   Victor Porof <vporof@mozilla.com>
+#   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
@@ -222,30 +223,31 @@
                 class="editBookmarkPanelBottomButton"
                 label="&editBookmark.done.label;"
                 default="true"
                 oncommand="StarUI.panel.hidePopup();"/>
 #endif
       </hbox>
     </panel>
 
-    <panel id="inspector-tree-panel"
-           orient="vertical"
-           hidden="true"
-           ignorekeys="true"
-           noautofocus="true"
-           noautohide="true"
-           titlebar="normal"
-           close="true"
-           label="&inspectPanelTitle.label;">
-      <hbox id="tree-panel-resizer-box" align="end">
-        <spacer flex="1" />
-        <resizer dir="bottomend" />
-      </hbox>
-    </panel>
+    <menupopup id="inspector-node-popup">
+      <menuitem id="inspectorHTMLCopyInner"
+                label="&inspectorHTMLCopyInner.label;"
+                accesskey="&inspectorHTMLCopyInner.accesskey;"
+                command="Inspector:CopyInner"/>
+      <menuitem id="inspectorHTMLCopyOuter"
+                label="&inspectorHTMLCopyOuter.label;"
+                accesskey="&inspectorHTMLCopyOuter.accesskey;"
+                command="Inspector:CopyOuter"/>
+      <menuseparator/>
+      <menuitem id="inspectorHTMLDelete"
+                label="&inspectorHTMLDelete.label;"
+                accesskey="&inspectorHTMLDelete.accesskey;"
+                command="Inspector:DeleteNode"/>
+    </menupopup>
 
     <menupopup id="toolbar-context-menu"
                onpopupshowing="onViewToolbarsPopupShowing(event);">
       <menuseparator/>
       <menuitem command="cmd_ToggleTabsOnTop"
                 type="checkbox"
                 label="&viewTabsOnTop.label;"
                 accesskey="&viewTabsOnTop.accesskey;"/>
@@ -989,55 +991,54 @@
     </hbox>
   </hbox>
 
   <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" 
-                 dir="top" disabled="true"
-                 element="inspector-tree-box"/>
-        <hbox>
 #ifdef XP_MACOSX
-          <toolbarbutton id="highlighter-closebutton"
-                         oncommand="InspectorUI.closeInspectorUI(false);"
-                         tooltiptext="&inspectCloseButton.tooltiptext;"/>
+      <toolbarbutton id="highlighter-closebutton"
+                     oncommand="InspectorUI.closeInspectorUI(false);"
+                     tooltiptext="&inspectCloseButton.tooltiptext;"/>
 #endif
-          <toolbarbutton id="inspector-inspect-toolbutton"
-                         class="devtools-toolbarbutton"
-                         label="&inspectButton.label;"
-                         accesskey="&inspectButton.accesskey;"
-                         command="Inspector:Inspect"/>
-          <arrowscrollbox id="inspector-breadcrumbs"
-                          flex="1" orient="horizontal"
-                          clicktoscroll="true"/>
-          <hbox id="inspector-tools">
-            <toolbarbutton id="inspector-3D-button"
-                           class="devtools-toolbarbutton"
-                           hidden="true"
-                           label="&inspect3DViewButton.label;"
-                           accesskey="&inspect3DViewButton.accesskey;"
-                           command="Inspector:Tilt"/>
-            <toolbarbutton id="inspector-style-button"
-                           class="devtools-toolbarbutton"
-                           label="&inspectStyleButton.label;"
-                           accesskey="&inspectStyleButton.accesskey;"
-                           command="Inspector:Sidebar"/>
-            <!-- registered tools go here -->
-          </hbox>
+      <toolbarbutton id="inspector-inspect-toolbutton"
+                     class="devtools-toolbarbutton"
+                     label="&inspectButton.label;"
+                     accesskey="&inspectButton.accesskey;"
+                     command="Inspector:Inspect"/>
+      <toolbarbutton id="inspector-treepanel-toolbutton"
+                     class="devtools-toolbarbutton"
+                     label="&htmlPanel.label;"
+                     accesskey="&htmlPanel.accesskey;"
+                     tooltiptext="&htmlPanel.tooltiptext;"
+                     command="Inspector:HTMLPanel"/>
+      <arrowscrollbox id="inspector-breadcrumbs"
+                      flex="1" orient="horizontal"
+                      clicktoscroll="true"/>
+      <hbox id="inspector-tools">
+        <toolbarbutton id="inspector-3D-button"
+                       class="devtools-toolbarbutton"
+                       hidden="true"
+                       label="&inspect3DViewButton.label;"
+                       accesskey="&inspect3DViewButton.accesskey;"
+                       command="Inspector:Tilt"/>
+        <toolbarbutton id="inspector-style-button"
+                       class="devtools-toolbarbutton"
+                       label="&inspectStyleButton.label;"
+                       accesskey="&inspectStyleButton.accesskey;"
+                       command="Inspector:Sidebar"/>
+        <!-- registered tools go here -->
+      </hbox>
 #ifndef XP_MACOSX
-          <toolbarbutton id="highlighter-closebutton"
-                         oncommand="InspectorUI.closeInspectorUI(false);"
-                         tooltiptext="&inspectCloseButton.tooltiptext;"/>
+      <toolbarbutton id="highlighter-closebutton"
+                     oncommand="InspectorUI.closeInspectorUI(false);"
+                     tooltiptext="&inspectCloseButton.tooltiptext;"/>
 #endif
-        </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"
              mode="icons" iconsize="small" defaulticonsize="small"
              lockiconsize="true"
--- a/browser/base/content/highlighter.css
+++ b/browser/base/content/highlighter.css
@@ -29,24 +29,16 @@
 #highlighter-veil-middlebox:-moz-locale-dir(rtl) {
   -moz-box-direction: reverse;
 }
 
 .inspector-breadcrumbs-button {
   direction: ltr;
 }
 
-#inspector-top-resizer {
-  display: none;
-}
-
-#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer {
-  display: -moz-box;
-}
-
 /*
  * Node Infobar
  */
 
 #highlighter-nodeinfobar-container {
   position: absolute;
   max-width: 95%;
 }
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -344,17 +344,17 @@ DebuggerView.Properties = {
    */
   _addScope: function DVP__addScope(aName, aId) {
     // make sure the parent container exists
     if (!this._vars) {
       return null;
     }
 
     // compute the id of the element if not specified
-    aId = aId || (aName + "-scope");
+    aId = aId || (aName.toLowerCase().trim().replace(" ", "-") + "-scope");
 
     // contains generic nodes and functionality
     let element = this._createPropertyElement(aName, aId, "scope", this._vars);
 
     // make sure the element was created successfully
     if (!element) {
       dump("The debugger scope container wasn't created properly: " + aId);
       return null;
@@ -536,22 +536,22 @@ DebuggerView.Properties = {
    * default id set as aVar.id->aKey-property.
    *
    * @param object aVar
    *        The parent variable element.
    * @param {Array} aProperty
    *        An array containing the key and grip properties, specifying
    *        the value and/or type & class of the variable (if the type
    *        is not specified, it will be inferred from the value).
-   *        e.g. ["someProp0": 42]
-   *             ["someProp1": true]
-   *             ["someProp2": "nasu"]
-   *             ["someProp3": { type: "undefined" }]
-   *             ["someProp4": { type: "null" }]
-   *             ["someProp5": { type: "object", class: "Object" }]
+   *        e.g. ["someProp0", 42]
+   *             ["someProp1", true]
+   *             ["someProp2", "nasu"]
+   *             ["someProp3", { type: "undefined" }]
+   *             ["someProp4", { type: "null" }]
+   *             ["someProp5", { type: "object", class: "Object" }]
    * @param string aName
    *        Optional, the property name.
    * @paarm string aId
    *        Optional, an id for the property html node.
    * @return object
    *         The newly created html node representing the added prop.
    */
   _addProperty: function DVP__addProperty(aVar, aProperty, aName, aId) {
--- a/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
+++ b/browser/devtools/debugger/test/browser_dbg_debuggerstatement.js
@@ -29,17 +29,18 @@ function test_early_debugger_statement(a
 {
   let paused = function(aEvent, aPacket) {
     ok(false, "Pause shouldn't be called before we've attached!\n");
     finish_test();
   };
   gClient.addListener("paused", paused);
   // This should continue without nesting an event loop and calling
   // the onPaused hook, because we haven't attached yet.
-  gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
+  // TODO: uncomment this when bug 723563 is fixed.
+  //gTab.linkedBrowser.contentWindow.wrappedJSObject.runDebuggerStatement();
 
   gClient.removeListener("paused", paused);
 
   // Now attach and resume...
   gClient.request({ to: aActor.threadActor, type: "attach" }, function(aResponse) {
     gClient.request({ to: aActor.threadActor, type: "resume" }, function(aResponse) {
       test_debugger_statement(aActor);
     });
--- a/browser/devtools/debugger/test/browser_dbg_listtabs.js
+++ b/browser/devtools/debugger/test/browser_dbg_listtabs.js
@@ -82,17 +82,17 @@ function test_attach_removed_tab()
   removeTab(gTab2);
   gTab2 = null;
   gClient.addListener("paused", function(aEvent, aPacket) {
     ok(false, "Attaching to an exited tab actor shouldn't generate a pause.");
     finish_test();
   });
 
   gClient.request({ to: gTab2Actor, type: "attach" }, function(aResponse) {
-    is(aResponse.type, "exited", "Tab should consider itself exited.");
+    is(aResponse.error, "noSuchActor", "Tab should be gone.");
     finish_test();
   });
 }
 
 function finish_test()
 {
   gClient.close(function() {
     finish();
--- a/browser/devtools/highlighter/TreePanel.jsm
+++ b/browser/devtools/highlighter/TreePanel.jsm
@@ -59,74 +59,51 @@ const INSPECTOR_URI = "chrome://browser/
  */
 function TreePanel(aContext, aIUI) {
   this._init(aContext, aIUI);
 };
 
 TreePanel.prototype = {
   showTextNodesWithWhitespace: false,
   id: "treepanel", // DO NOT LOCALIZE
-  openInDock: true,
+  _open: false,
 
   /**
    * The tree panel container element.
    * @returns xul:panel|xul:vbox|null
    *          xul:panel is returned when the tree panel is not docked, or
    *          xul:vbox when when the tree panel is docked.
    *          null is returned when no container is available.
    */
   get container()
   {
-    if (this.openInDock) {
-      return this.document.getElementById("inspector-tree-box");
-    }
-
-    return this.document.getElementById("inspector-tree-panel");
+    return this.document.getElementById("inspector-tree-box");
   },
 
   /**
    * Main TreePanel boot-strapping method. Initialize the TreePanel with the
    * originating context and the InspectorUI global.
    * @param aContext nsIDOMWindow (xulwindow)
    * @param aIUI global InspectorUI object
    */
   _init: function TP__init(aContext, aIUI)
   {
     this.IUI = aIUI;
     this.window = aContext;
     this.document = this.window.document;
+    this.button =
+     this.IUI.chromeDoc.getElementById("inspector-treepanel-toolbutton");
 
     domplateUtils.setDOM(this.window);
 
     this.DOMHelpers = new DOMHelpers(this.window);
 
     let isOpen = this.isOpen.bind(this);
 
-    this.registrationObject = {
-      id: this.id,
-      label: this.IUI.strings.GetStringFromName("htmlPanel.label"),
-      tooltiptext: this.IUI.strings.GetStringFromName("htmlPanel.tooltiptext"),
-      accesskey: this.IUI.strings.GetStringFromName("htmlPanel.accesskey"),
-      context: this,
-      get isOpen() isOpen(),
-      show: this.open,
-      hide: this.close,
-      onSelect: this.select,
-      panel: this.openInDock ? null : this.container,
-      unregister: this.destroy,
-    };
     this.editingEvents = {};
-
-    if (!this.openInDock) {
-      this._boundClose = this.close.bind(this);
-      this.container.addEventListener("popuphiding", this._boundClose, false);
-    }
-
-    // Register the HTML panel with the highlighter
-    this.IUI.registerTool(this.registrationObject);
   },
 
   /**
    * Initialization function for the TreePanel.
    */
   initializeIFrame: function TP_initializeIFrame()
   {
     if (!this.initializingTreePanel || this.treeLoaded) {
@@ -149,138 +126,96 @@ TreePanel.prototype = {
       this.select(this.IUI.selection, true);
   },
 
   /**
    * Open the inspector's tree panel and initialize it.
    */
   open: function TP_open()
   {
-    if (this.initializingTreePanel && !this.treeLoaded) {
+    if (this._open) {
       return;
     }
 
+    this._open = true;
+
+    this.button.setAttribute("checked", true);
     this.initializingTreePanel = true;
-    if (!this.openInDock)
-      this.container.hidden = false;
 
     this.treeIFrame = this.document.getElementById("inspector-tree-iframe");
     if (!this.treeIFrame) {
       this.treeIFrame = this.document.createElement("iframe");
       this.treeIFrame.setAttribute("id", "inspector-tree-iframe");
       this.treeIFrame.flex = 1;
       this.treeIFrame.setAttribute("type", "content");
-    }
-
-    if (this.openInDock) { // Create vbox
-      this.openDocked();
-      return;
+      this.treeIFrame.setAttribute("context", "inspector-node-popup");
     }
 
-    let resizerBox = this.document.getElementById("tree-panel-resizer-box");
-    this.treeIFrame = this.container.insertBefore(this.treeIFrame, resizerBox);
-
-    let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel()
-    {
-      this.treeIFrame.removeEventListener("load",
-        boundLoadedInitializeTreePanel, true);
-      this.initializeIFrame();
-    }.bind(this);
-
-    let boundTreePanelShown = function treePanelShown()
-    {
-      this.container.removeEventListener("popupshown",
-        boundTreePanelShown, false);
-
-      this.treeIFrame.addEventListener("load",
-        boundLoadedInitializeTreePanel, true);
-
-      let src = this.treeIFrame.getAttribute("src");
-      if (src != INSPECTOR_URI) {
-        this.treeIFrame.setAttribute("src", INSPECTOR_URI);
-      } else {
-        this.treeIFrame.contentWindow.location.reload();
-      }
-    }.bind(this);
-
-    this.container.addEventListener("popupshown", boundTreePanelShown, false);
-
-    const panelWidthRatio = 7 / 8;
-    const panelHeightRatio = 1 / 5;
-
-    let width = parseInt(this.IUI.win.outerWidth * panelWidthRatio);
-    let height = parseInt(this.IUI.win.outerHeight * panelHeightRatio);
-    let y = Math.min(this.document.defaultView.screen.availHeight - height,
-      this.IUI.win.innerHeight);
-
-    this.container.openPopup(this.browser, "overlap", 0, 0,
-      false, false);
-
-    this.container.moveTo(80, y);
-    this.container.sizeTo(width, height);
-  },
-
-  openDocked: function TP_openDocked()
-  {
     let treeBox = null;
-    let toolbar = this.IUI.toolbar.nextSibling; // Addons bar, typically
-    let toolbarParent =
-      this.IUI.browser.ownerDocument.getElementById("browser-bottombox");
     treeBox = this.document.createElement("vbox");
     treeBox.id = "inspector-tree-box";
-    treeBox.state = "open"; // for the registerTools API.
+    treeBox.state = "open";
     try {
       treeBox.height =
         Services.prefs.getIntPref("devtools.inspector.htmlHeight");
     } catch(e) {
       treeBox.height = 112;
     }
 
     treeBox.minHeight = 64;
-    treeBox.flex = 1;
-    toolbarParent.insertBefore(treeBox, toolbar);
+
+    this.splitter = this.document.createElement("splitter");
+    this.splitter.id = "inspector-tree-splitter";
 
-    this.IUI.toolbar.setAttribute("treepanel-open", "true");
+    let container = this.document.getElementById("appcontent");
+    container.appendChild(this.splitter);
+    container.appendChild(treeBox);
 
     treeBox.appendChild(this.treeIFrame);
 
-    let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel()
+    this._boundLoadedInitializeTreePanel = function loadedInitializeTreePanel()
     {
       this.treeIFrame.removeEventListener("load",
-        boundLoadedInitializeTreePanel, true);
+        this._boundLoadedInitializeTreePanel, true);
+      delete this._boundLoadedInitializeTreePanel;
       this.initializeIFrame();
     }.bind(this);
 
     this.treeIFrame.addEventListener("load",
-      boundLoadedInitializeTreePanel, true);
+      this._boundLoadedInitializeTreePanel, true);
 
     let src = this.treeIFrame.getAttribute("src");
     if (src != INSPECTOR_URI) {
       this.treeIFrame.setAttribute("src", INSPECTOR_URI);
     } else {
       this.treeIFrame.contentWindow.location.reload();
     }
   },
 
   /**
    * Close the TreePanel.
    */
   close: function TP_close()
   {
-    if (this.openInDock) {
-      this.IUI.toolbar.removeAttribute("treepanel-open");
+    this._open = false;
 
-      let treeBox = this.container;
-      Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height);
-      let treeBoxParent = treeBox.parentNode;
-      treeBoxParent.removeChild(treeBox);
-    } else {
-      this.container.hidePopup();
+    // Stop caring about the tree iframe load if it's in progress.
+    if (this._boundLoadedInitializeTreePanel) {
+      this.treeIFrame.removeEventListener("load",
+        this._boundLoadedInitializeTreePanel, true);
+      delete this._boundLoadedInitializeTreePanel;
     }
 
+    this.button.removeAttribute("checked");
+    let treeBox = this.container;
+    Services.prefs.setIntPref("devtools.inspector.htmlHeight", treeBox.height);
+    let treeBoxParent = treeBox.parentNode;
+    treeBoxParent.removeChild(this.splitter);
+    treeBoxParent.removeChild(treeBox);
+
     if (this.treePanelDiv) {
       this.treePanelDiv.ownerPanel = null;
       let parent = this.treePanelDiv.parentNode;
       parent.removeChild(this.treePanelDiv);
       delete this.treePanelDiv;
       delete this.treeBrowserDocument;
     }
 
@@ -288,20 +223,25 @@ TreePanel.prototype = {
   },
 
   /**
    * Is the TreePanel open?
    * @returns boolean
    */
   isOpen: function TP_isOpen()
   {
-    if (this.openInDock)
-      return this.treeLoaded && this.container;
+    return this._open;
+  },
 
-    return this.treeLoaded && this.container.state == "open";
+  /**
+   * Toggle the TreePanel.
+   */
+  toggle: function TP_toggle()
+  {
+    this.isOpen() ? this.close() : this.open();
   },
 
   /**
    * Create the ObjectBox for the given object.
    * @param object nsIDOMNode
    * @param isRoot boolean - Is this the root object?
    * @returns InsideOutBox
    */
@@ -665,16 +605,30 @@ TreePanel.prototype = {
         else
           return child.repObject;
       }
     }
     return null;
   },
 
   /**
+   * Remove a node box from the tree view.
+   * @param aElement
+   *        The DOM node to remove from the HTML IOBox.
+   */
+  deleteChildBox: function TP_deleteChildBox(aElement)
+  {
+    let childBox = this.ioBox.findObjectBox(aElement);
+    if (!childBox) {
+      return;
+    }
+    childBox.parentNode.removeChild(childBox);
+  },
+
+  /**
    * Destructor function. Cleanup.
    */
   destroy: function TP_destroy()
   {
     if (this.isOpen()) {
       this.close();
     }
 
@@ -700,21 +654,16 @@ TreePanel.prototype = {
       parent.removeChild(this.treeIFrame);
       delete this.treeIFrame;
     }
 
     if (this.ioBox) {
       this.ioBox.destroy();
       delete this.ioBox;
     }
-
-    if (!this.openInDock) {
-      this.container.removeEventListener("popuphiding", this._boundClose, false);
-      delete this._boundClose;
-    }
   }
 };
 
 
 /**
  * DOMHelpers
  * Makes DOM traversal easier. Goes through iframes.
  *
--- a/browser/devtools/highlighter/highlighter.jsm
+++ b/browser/devtools/highlighter/highlighter.jsm
@@ -38,32 +38,39 @@
  * 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 ***** */
 
 const Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var EXPORTED_SYMBOLS = ["Highlighter"];
 
 const INSPECTOR_INVISIBLE_ELEMENTS = {
   "head": true,
   "base": true,
   "basefont": true,
   "isindex": true,
   "link": true,
   "meta": true,
   "script": true,
   "style": true,
   "title": true,
 };
 
+const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+  // add ":visited" and ":link" after bug 713106 is fixed
+
 /**
  * A highlighter mechanism.
  *
  * The highlighter is built dynamically into the browser element.
  * The caller is in charge of destroying the highlighter (ie, the highlighter
  * won't be destroyed if a new tab is selected for example).
  *
  * API:
@@ -104,16 +111,18 @@ const INSPECTOR_INVISIBLE_ELEMENTS = {
  *
  * Events:
  *
  *   "closed" - Highlighter is closing
  *   "nodeselected" - A new node has been selected
  *   "highlighting" - Highlighter is highlighting
  *   "locked" - The selected node has been locked
  *   "unlocked" - The selected ndoe has been unlocked
+ *   "pseudoclasstoggled" - A pseudo-class lock has changed on the selected node
+
  *
  * Structure:
  *
  *   <stack id="highlighter-container">
  *     <vbox id="highlighter-veil-container">...</vbox>
  *     <box id="highlighter-controls>...</vbox>
  *   </stack>
  *
@@ -234,16 +243,27 @@ Highlighter.prototype = {
     this.invalidateSize(!!aScroll);
 
     if (oldNode !== this.node) {
       this.emitEvent("nodeselected");
     }
   },
 
   /**
+   * Notify that a pseudo-class lock was toggled on the highlighted element
+   *
+   * @param aPseudo - The pseudo-class to toggle, e.g. ":hover".
+   */
+  pseudoClassLockToggled: function Highlighter_pseudoClassLockToggled(aPseudo)
+  {  
+    this.emitEvent("pseudoclasstoggled", [aPseudo]);
+    this.updateInfobar();
+  },
+
+  /**
    * Update the highlighter size and position.
    */
   invalidateSize: function Highlighter_invalidateSize(aScroll)
   {
     let rect = null;
 
     if (this.node && this.isNodeHighlightable(this.node)) {
 
@@ -441,40 +461,91 @@ Highlighter.prototype = {
     let tagNameLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
     tagNameLabel.id = "highlighter-nodeinfobar-tagname";
 
     let idLabel = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
     idLabel.id = "highlighter-nodeinfobar-id";
 
     let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
     classesBox.id = "highlighter-nodeinfobar-classes";
+    
+    let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span");
+    pseudoClassesBox.id = "highlighter-nodeinfobar-pseudo-classes";
+    
     // Add some content to force a better boundingClientRect down below.
-    classesBox.textContent = "&nbsp;";
+    pseudoClassesBox.textContent = "&nbsp;";
 
     nodeInfobar.appendChild(tagNameLabel);
     nodeInfobar.appendChild(idLabel);
     nodeInfobar.appendChild(classesBox);
+    nodeInfobar.appendChild(pseudoClassesBox);
     container.appendChild(arrowBoxTop);
     container.appendChild(nodeInfobar);
     container.appendChild(arrowBoxBottom);
 
     aParent.appendChild(container);
 
+    nodeInfobar.onclick = (function _onInfobarRightClick(aEvent) {
+      if (aEvent.button == 2) {
+        this.openPseudoClassMenu();
+      }
+    }).bind(this);
+
     let barHeight = container.getBoundingClientRect().height;
 
     this.nodeInfo = {
       tagNameLabel: tagNameLabel,
       idLabel: idLabel,
       classesBox: classesBox,
+      pseudoClassesBox: pseudoClassesBox,
       container: container,
       barHeight: barHeight,
     };
   },
 
   /**
+   * Open the infobar's pseudo-class context menu.
+   */
+  openPseudoClassMenu: function Highlighter_openPseudoClassMenu()
+  {
+    let menu = this.chromeDoc.createElement("menupopup");
+    menu.id = "infobar-context-menu";
+
+    let popupSet = this.chromeDoc.getElementById("mainPopupSet");
+    popupSet.appendChild(menu);
+    
+    let fragment = this.buildPseudoClassMenu();
+    menu.appendChild(fragment);
+
+    menu.openPopup(this.nodeInfo.pseudoClassesBox, "end_before", 0, 0, true, false);
+  },  
+  
+  /**
+   * Create the menuitems for toggling the selection's pseudo-class state
+   *
+   * @returns DocumentFragment. The menuitems for toggling pseudo-classes.
+   */
+  buildPseudoClassMenu: function IUI_buildPseudoClassesMenu()
+  {
+    let fragment = this.chromeDoc.createDocumentFragment();
+    for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
+      let pseudo = PSEUDO_CLASSES[i];
+      let item = this.chromeDoc.createElement("menuitem");
+      item.setAttribute("type", "checkbox");
+      item.setAttribute("label", pseudo);
+      item.addEventListener("command",
+                            this.pseudoClassLockToggled.bind(this, pseudo), false);
+      item.setAttribute("checked", DOMUtils.hasPseudoClassLock(this.node,
+                         pseudo));
+      fragment.appendChild(item);
+    }
+    return fragment;
+  },
+
+  /**
    * Highlight a rectangular region.
    *
    * @param object aRect
    *        The rectangle region to highlight.
    * @returns boolean
    *          True if the rectangle was highlighted, false otherwise.
    */
   highlightRectangle: function Highlighter_highlightRectangle(aRect)
@@ -538,16 +609,24 @@ Highlighter.prototype = {
     // ID
     this.nodeInfo.idLabel.textContent = this.node.id ? "#" + this.node.id : "";
 
     // Classes
     let classes = this.nodeInfo.classesBox;
 
     classes.textContent = this.node.classList.length ?
                             "." + Array.join(this.node.classList, ".") : "";
+
+    // Pseudo-classes
+    let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
+      return DOMUtils.hasPseudoClassLock(this.node, pseudo);
+    }, this);
+
+    let pseudoBox = this.nodeInfo.pseudoClassesBox;
+    pseudoBox.textContent = pseudos.join("");
   },
 
   /**
    * Move the Infobar to the right place in the highlighter.
    */
   moveInfobar: function Highlighter_moveInfobar()
   {
     if (this._highlightRect) {
@@ -612,18 +691,18 @@ Highlighter.prototype = {
     }
   },
 
   /**
    * Store page zoom factor.
    */
   computeZoomFactor: function Highlighter_computeZoomFactor() {
     this.zoom =
-      this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-      .getInterface(Components.interfaces.nsIDOMWindowUtils)
+      this.win.QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIDOMWindowUtils)
       .screenPixelsPerCSSPixel;
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Event Emitter Mechanism
 
   addListener: function Highlighter_addListener(aEvent, aListener)
   {
@@ -800,8 +879,11 @@ Highlighter.prototype = {
     if (element && element != this.node) {
       this.highlight(element);
     }
   },
 };
 
 ///////////////////////////////////////////////////////////////////////////
 
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
+});
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -37,16 +37,17 @@
  * 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 ***** */
 
+const Cc = Components.classes;
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 
 var EXPORTED_SYMBOLS = ["InspectorUI"];
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -77,16 +78,18 @@ const INSPECTOR_NOTIFICATIONS = {
   RULEVIEWREADY: "inspector-ruleview-ready",
 
   // Event notifications for the attribute-value editor
   EDITOR_OPENED: "inspector-editor-opened",
   EDITOR_CLOSED: "inspector-editor-closed",
   EDITOR_SAVED: "inspector-editor-saved",
 };
 
+const PSEUDO_CLASSES = [":hover", ":active", ":focus"];
+
 ///////////////////////////////////////////////////////////////////////////
 //// InspectorUI
 
 /**
  * Main controller class for the Inspector.
  *
  * @constructor
  * @param nsIDOMWindow aWindow
@@ -103,17 +106,16 @@ function InspectorUI(aWindow)
   this.INSPECTOR_NOTIFICATIONS = INSPECTOR_NOTIFICATIONS;
 }
 
 InspectorUI.prototype = {
   browser: null,
   tools: null,
   toolEvents: null,
   inspecting: false,
-  treePanelEnabled: true,
   ruleViewEnabled: true,
   isDirty: false,
   store: null,
 
   /**
    * Toggle the inspector interface elements on or off.
    *
    * @param aEvent
@@ -132,49 +134,87 @@ InspectorUI.prototype = {
    * Show the Sidebar.
    */
   showSidebar: function IUI_showSidebar()
   {
     this.sidebarBox.removeAttribute("hidden");
     this.sidebarSplitter.removeAttribute("hidden");
     this.stylingButton.checked = true;
 
-    // Activate the first tool in the sidebar, only if none previously-
-    // selected. We'll want to do a followup to remember selected tool-states.
+    // If no tool is already selected, show the last-used sidebar if available,
+    // otherwise just show the first.
+
     if (!Array.some(this.sidebarToolbar.children,
       function(btn) btn.hasAttribute("checked"))) {
-        let firstButtonId = this.getToolbarButtonId(this.sidebarTools[0].id);
-        this.chromeDoc.getElementById(firstButtonId).click();
+
+      let activePanel = this.sidebarTools[0];
+      let activeId = this.store.getValue(this.winID, "activeSidebar");
+      if (activeId && this.tools[activeId]) {
+        activePanel = this.tools[activeId];
+      }
+      this.activateSidebarPanel(activePanel.id);
     }
+
+    this.store.setValue(this.winID, "sidebarOpen", true);
+    Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", true);
   },
 
   /**
-   * Hide the Sidebar.
+   * Tear down the sidebar.
    */
-  hideSidebar: function IUI_hideSidebar()
+  _destroySidebar: function IUI_destroySidebar()
   {
     this.sidebarBox.setAttribute("hidden", "true");
     this.sidebarSplitter.setAttribute("hidden", "true");
     this.stylingButton.checked = false;
   },
 
   /**
+   * Hide the sidebar.
+   */
+  hideSidebar: function IUI_hideSidebar()
+  {
+    this._destroySidebar();
+    this.store.setValue(this.winID, "sidebarOpen", false);
+    Services.prefs.setBoolPref("devtools.inspector.sidebarOpen", false);
+  },
+
+  /**
    * Show or hide the sidebar. Called from the Styling button on the
    * highlighter toolbar.
    */
   toggleSidebar: function IUI_toggleSidebar()
   {
     if (!this.isSidebarOpen) {
       this.showSidebar();
     } else {
       this.hideSidebar();
     }
   },
 
   /**
+   * Activate a sidebar panel by id.
+   */
+  activateSidebarPanel: function IUI_activateSidebarPanel(aID)
+  {
+    let buttonId = this.getToolbarButtonId(aID);
+    this.chromeDoc.getElementById(buttonId).click();
+  },
+
+  get activeSidebarPanel()
+  {
+    for each (let tool in this.sidebarTools) {
+      if (this.sidebarDeck.selectedPanel == this.getToolIframe(tool)) {
+        return tool.id;
+      }
+    }
+    return null;
+  },
+
+  /**
    * Getter to test if the Sidebar is open or not.
    */
   get isSidebarOpen()
   {
     return this.stylingButton.checked &&
           !this.sidebarBox.hidden &&
           !this.sidebarSplitter.hidden;
   },
@@ -188,16 +228,32 @@ InspectorUI.prototype = {
     if (this.inspecting) {
       this.stopInspecting();
     } else {
       this.startInspecting();
     }
   },
 
   /**
+   * Toggle the TreePanel.
+   */
+  toggleHTMLPanel: function TP_toggle()
+  {
+    if (this.treePanel.isOpen()) {
+      this.treePanel.close();
+      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", false);
+      this.store.setValue(this.winID, "htmlPanelOpen", false);
+    } else {
+      this.treePanel.open();
+      Services.prefs.setBoolPref("devtools.inspector.htmlPanelOpen", true);
+      this.store.setValue(this.winID, "htmlPanelOpen", true);
+    }
+  },
+
+  /**
    * Is the inspector UI open? Simply check if the toolbar is visible or not.
    *
    * @returns boolean
    */
   get isInspectorOpen()
   {
     return this.toolbar && !this.toolbar.hidden && this.highlighter;
   },
@@ -255,19 +311,17 @@ InspectorUI.prototype = {
     this.toolbar = this.chromeDoc.getElementById("inspector-toolbar");
     this.inspectMenuitem = this.chromeDoc.getElementById("Tools:Inspect");
     this.inspectToolbutton =
       this.chromeDoc.getElementById("inspector-inspect-toolbutton");
 
     this.initTools();
     this.chromeWin.Tilt.setup();
 
-    if (this.treePanelEnabled) {
-      this.treePanel = new TreePanel(this.chromeWin, this);
-    }
+    this.treePanel = new TreePanel(this.chromeWin, this);
 
     if (Services.prefs.getBoolPref("devtools.ruleview.enabled") &&
         !this.toolRegistered("ruleview")) {
       this.registerRuleView();
     }
 
     if (Services.prefs.getBoolPref("devtools.styleinspector.enabled") &&
         !this.toolRegistered("styleinspector")) {
@@ -305,16 +359,17 @@ InspectorUI.prototype = {
       label: this.strings.GetStringFromName("ruleView.label"),
       tooltiptext: this.strings.GetStringFromName("ruleView.tooltiptext"),
       accesskey: this.strings.GetStringFromName("ruleView.accesskey"),
       context: this,
       get isOpen() isOpen(),
       show: this.openRuleView,
       hide: this.closeRuleView,
       onSelect: this.selectInRuleView,
+      onChanged: this.changeInRuleView,
       panel: null,
       unregister: this.destroyRuleView,
       sidebar: true,
     };
 
     this.registerTool(this.ruleViewObject);
   },
 
@@ -344,16 +399,26 @@ InspectorUI.prototype = {
       }
       this.isDirty = this.store.getValue(this.winID, "isDirty");
     } else {
       // First time inspecting, set state to no selection + live inspection.
       this.store.addStore(this.winID);
       this.store.setValue(this.winID, "selectedNode", null);
       this.store.setValue(this.winID, "inspecting", true);
       this.store.setValue(this.winID, "isDirty", this.isDirty);
+
+      this.store.setValue(this.winID, "htmlPanelOpen",
+        Services.prefs.getBoolPref("devtools.inspector.htmlPanelOpen"));
+
+      this.store.setValue(this.winID, "sidebarOpen",
+        Services.prefs.getBoolPref("devtools.inspector.sidebarOpen"));
+
+      this.store.setValue(this.winID, "activeSidebar",
+        Services.prefs.getCharPref("devtools.inspector.activeSidebar"));
+
       this.win.addEventListener("pagehide", this, true);
     }
   },
 
   /**
    * Browse nodes according to the breadcrumbs layout, only for some specific
    * elements of the UI.
    */
@@ -395,16 +460,18 @@ InspectorUI.prototype = {
    */
   closeInspectorUI: function IUI_closeInspectorUI(aKeepStore)
   {
     // if currently editing an attribute value, closing the
     // highlighter/HTML panel dismisses the editor
     if (this.treePanel && this.treePanel.editingContext)
       this.treePanel.closeEditor();
 
+    this.treePanel.destroy();
+
     if (this.closing || !this.win || !this.browser) {
       return;
     }
 
     let winId = new String(this.winID); // retain this to notify observers.
 
     this.closing = true;
     this.toolbar.hidden = true;
@@ -412,16 +479,17 @@ InspectorUI.prototype = {
     this.removeNavigationKeys();
 
     this.progressListener.destroy();
     delete this.progressListener;
 
     if (!aKeepStore) {
       this.store.deleteStore(this.winID);
       this.win.removeEventListener("pagehide", this, true);
+      this.clearPseudoClassLocks();
     } else {
       // Update the store before closing.
       if (this.selection) {
         this.store.setValue(this.winID, "selectedNode",
           this.selection);
       }
       this.store.setValue(this.winID, "inspecting", this.inspecting);
       this.store.setValue(this.winID, "isDirty", this.isDirty);
@@ -430,23 +498,22 @@ InspectorUI.prototype = {
     if (this.store.isEmpty()) {
       this.tabbrowser.tabContainer.removeEventListener("TabSelect", this, false);
     }
 
     this.chromeWin.removeEventListener("keypress", this, false);
 
     this.stopInspecting();
 
-    this.saveToolState(this.winID);
     this.toolsDo(function IUI_toolsHide(aTool) {
       this.unregisterTool(aTool);
     }.bind(this));
 
     // close the sidebar
-    this.hideSidebar();
+    this._destroySidebar();
 
     if (this.highlighter) {
       this.highlighter.destroy();
       this.highlighter = null;
     }
 
     if (this.breadcrumbs) {
       this.breadcrumbs.destroy();
@@ -498,65 +565,108 @@ InspectorUI.prototype = {
       return;
     }
 
     this.inspectToolbutton.checked = false;
 
     this.inspecting = false;
     this.toolsDim(false);
     if (this.highlighter.getNode()) {
-      this.select(this.highlighter.getNode(), true, true, !aPreventScroll);
+      this.select(this.highlighter.getNode(), true, !aPreventScroll);
     } else {
       this.select(null, true, true);
     }
     this.highlighter.lock();
   },
 
   /**
-   * Select an object in the tree view.
+   * Select an object in the inspector.
    * @param aNode
    *        node to inspect
    * @param forceUpdate
    *        force an update?
    * @param aScroll boolean
    *        scroll the tree panel?
+   * @param aFrom [optional] string
+   *        which part of the UI the selection occured from
    */
-  select: function IUI_select(aNode, forceUpdate, aScroll)
+  select: function IUI_select(aNode, forceUpdate, aScroll, aFrom)
   {
     // if currently editing an attribute value, using the
     // highlighter dismisses the editor
     if (this.treePanel && this.treePanel.editingContext)
       this.treePanel.closeEditor();
 
     if (!aNode)
       aNode = this.defaultSelection;
 
     if (forceUpdate || aNode != this.selection) {
+      if (aFrom != "breadcrumbs") {
+        this.clearPseudoClassLocks();
+      }
+      
       this.selection = aNode;
       if (!this.inspecting) {
         this.highlighter.highlight(this.selection);
       }
     }
 
     this.breadcrumbs.update();
     this.chromeWin.Tilt.update(aNode);
+    this.treePanel.select(aNode, aScroll);
 
     this.toolsSelect(aScroll);
   },
+  
+  /**
+   * Toggle the pseudo-class lock on the currently inspected element. If the
+   * pseudo-class is :hover or :active, that pseudo-class will also be toggled
+   * on every ancestor of the element, mirroring real :hover and :active
+   * behavior.
+   * 
+   * @param aPseudo the pseudo-class lock to toggle, e.g. ":hover"
+   */
+  togglePseudoClassLock: function IUI_togglePseudoClassLock(aPseudo)
+  {
+    if (DOMUtils.hasPseudoClassLock(this.selection, aPseudo)) {
+      this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
+        DOMUtils.removePseudoClassLock(crumb.node, aPseudo);
+      });
+    } else {
+      let hierarchical = aPseudo == ":hover" || aPseudo == ":active";
+      let node = this.selection;
+      do {
+        DOMUtils.addPseudoClassLock(node, aPseudo);
+        node = node.parentNode;
+      } while (hierarchical && node.parentNode)
+    }
+    this.nodeChanged();
+  },
+
+  /**
+   * Clear all pseudo-class locks applied to elements in the node hierarchy
+   */
+  clearPseudoClassLocks: function IUI_clearPseudoClassLocks()
+  {
+    this.breadcrumbs.nodeHierarchy.forEach(function(crumb) {
+      DOMUtils.clearPseudoClassLocks(crumb.node);
+    });
+  },
 
   /**
    * Called when the highlighted node is changed by a tool.
    *
    * @param object aUpdater
    *        The tool that triggered the update (if any), that tool's
    *        onChanged will not be called.
    */
   nodeChanged: function IUI_nodeChanged(aUpdater)
   {
     this.highlighter.invalidateSize();
+    this.breadcrumbs.updateSelectors();
     this.toolsOnChanged(aUpdater);
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Event Handling
 
   highlighterReady: function IUI_highlighterReady()
   {
@@ -572,27 +682,42 @@ InspectorUI.prototype = {
     this.highlighter.addListener("unlocked", function() {
       self.startInspecting();
     });
 
     this.highlighter.addListener("nodeselected", function() {
       self.select(self.highlighter.getNode(), false, false);
     });
 
+    this.highlighter.addListener("pseudoclasstoggled", function(aPseudo) {
+      self.togglePseudoClassLock(aPseudo);
+    });
+
     if (this.store.getValue(this.winID, "inspecting")) {
       this.startInspecting();
+      this.highlighter.unlock();
+    } else {
+      this.highlighter.lock();
     }
 
-    this.restoreToolState(this.winID);
+    Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null);
 
     this.win.focus();
+    this.highlighter.highlight();
+
+    if (this.store.getValue(this.winID, "htmlPanelOpen")) {
+      this.treePanel.open();
+    }
+
+    if (this.store.getValue(this.winID, "sidebarOpen")) {
+      this.showSidebar();
+    }
+
     Services.obs.notifyObservers({wrappedJSObject: this},
                                  INSPECTOR_NOTIFICATIONS.OPENED, null);
-
-    this.highlighter.highlight();
   },
 
   /**
    * Main callback handler for events.
    *
    * @param event
    *        The event to be handled.
    */
@@ -709,16 +834,56 @@ InspectorUI.prototype = {
           this.highlighter.highlight(node, true);
         }
         event.preventDefault();
         event.stopPropagation();
         break;
     }
   },
 
+  /**
+   * Copy the innerHTML of the selected Node to the clipboard. Called via the
+   * Inspector:CopyInner command.
+   */
+  copyInnerHTML: function IUI_copyInnerHTML()
+  {
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+                    getService(Ci.nsIClipboardHelper);
+    clipboard.copyString(this.selection.innerHTML);
+  },
+
+  /**
+   * Copy the outerHTML of the selected Node to the clipboard. Called via the
+   * Inspector:CopyOuter command.
+   */
+  copyOuterHTML: function IUI_copyOuterHTML()
+  {
+    let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+                    getService(Ci.nsIClipboardHelper);
+    clipboard.copyString(this.selection.outerHTML);
+  },
+
+  /**
+   * Delete the selected node. Called via the Inspector:DeleteNode command.
+   */
+  deleteNode: function IUI_deleteNode()
+  {
+    let selection = this.selection;
+    let parent = this.selection.parentNode;
+
+    // remove the node from the treepanel
+    this.treePanel.deleteChildBox(selection);
+
+    // remove the node from content
+    parent.removeChild(selection);
+    this.breadcrumbs.invalidateHierarchy();
+
+    // select the parent node in the highlighter, treepanel, breadcrumbs
+    this.inspectNode(parent);
+  },
 
   /////////////////////////////////////////////////////////////////////////
   //// CssRuleView methods
 
   /**
    * Is the cssRuleView open?
    */
   isRuleViewOpen: function IUI_isRuleViewOpen()
@@ -791,16 +956,25 @@ InspectorUI.prototype = {
    * Update the selected node in the Css Rule View.
    * @param {nsIDOMnode} the selected node.
    */
   selectInRuleView: function IUI_selectInRuleView(aNode)
   {
     if (this.ruleView)
       this.ruleView.highlight(aNode);
   },
+  
+  /**
+   * Update the rules for the current node in the Css Rule View.
+   */
+  changeInRuleView: function IUI_selectInRuleView()
+  {
+    if (this.ruleView)
+      this.ruleView.nodeChanged();
+  },
 
   ruleViewChanged: function IUI_ruleViewChanged()
   {
     this.isDirty = true;
     this.nodeChanged(this.ruleViewObject);
   },
 
   /**
@@ -1082,16 +1256,18 @@ InspectorUI.prototype = {
    * Show the specified tool.
    * @param aTool Object (see comment for IUI_registerTool)
    */
   toolShow: function IUI_toolShow(aTool)
   {
     let btn = this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id));
     btn.setAttribute("checked", "true");
     if (aTool.sidebar) {
+      Services.prefs.setCharPref("devtools.inspector.activeSidebar", aTool.id);
+      this.store.setValue(this.winID, "activeSidebar", aTool.id);
       this.sidebarDeck.selectedPanel = this.getToolIframe(aTool);
       this.sidebarTools.forEach(function(other) {
         if (other != aTool)
           this.chromeDoc.getElementById(
             this.getToolbarButtonId(other.id)).removeAttribute("checked");
       }.bind(this));
     }
 
@@ -1176,67 +1352,16 @@ InspectorUI.prototype = {
     // the iframe.
     if (aRegObj.unregister)
       aRegObj.unregister.call(aRegObj.context);
 
     delete this.tools[aRegObj.id];
   },
 
   /**
-   * Save a list of open tools to the inspector store.
-   *
-   * @param aWinID The ID of the window used to save the associated tools
-   */
-  saveToolState: function IUI_saveToolState(aWinID)
-  {
-    let openTools = {};
-    this.toolsDo(function IUI_toolsSetId(aTool) {
-      if (aTool.isOpen) {
-        openTools[aTool.id] = true;
-      }
-    });
-    this.store.setValue(aWinID, "openTools", openTools);
-  },
-
-  /**
-   * Restore tools previously save using saveToolState().
-   *
-   * @param aWinID The ID of the window to which the associated tools are to be
-   *               restored.
-   */
-  restoreToolState: function IUI_restoreToolState(aWinID)
-  {
-    let openTools = this.store.getValue(aWinID, "openTools");
-    let activeSidebarTool;
-    if (openTools) {
-      this.toolsDo(function IUI_toolsOnShow(aTool) {
-        if (aTool.id in openTools) {
-          if (aTool.sidebar && !this.isSidebarOpen) {
-            this.showSidebar();
-            activeSidebarTool = aTool;
-          }
-          this.toolShow(aTool);
-        }
-      }.bind(this));
-      this.sidebarTools.forEach(function(tool) {
-        if (tool != activeSidebarTool)
-          this.chromeDoc.getElementById(
-            this.getToolbarButtonId(tool.id)).removeAttribute("checked");
-      }.bind(this));
-    }
-    if (this.store.getValue(this.winID, "inspecting")) {
-      this.highlighter.unlock();
-    } else {
-      this.highlighter.lock();
-    }
-
-    Services.obs.notifyObservers(null, INSPECTOR_NOTIFICATIONS.STATE_RESTORED, null);
-  },
-
-  /**
    * For each tool in the tools collection select the current node that is
    * selected in the highlighter
    * @param aScroll boolean
    *        Do you want to scroll the treepanel?
    */
   toolsSelect: function IUI_toolsSelect(aScroll)
   {
     let selection = this.selection;
@@ -1249,33 +1374,33 @@ InspectorUI.prototype = {
 
   /**
    * Dim or undim each tool in the tools collection
    * @param aState true = dim, false = undim
    */
   toolsDim: function IUI_toolsDim(aState)
   {
     this.toolsDo(function IUI_toolsDim(aTool) {
-      if (aTool.isOpen && "dim" in aTool) {
+      if ("dim" in aTool) {
         aTool.dim.call(aTool.context, aState);
       }
     });
   },
 
   /**
    * Notify registered tools of changes to the highlighted element.
    *
    * @param object aUpdater
    *        The tool that triggered the update (if any), that tool's
    *        onChanged will not be called.
    */
   toolsOnChanged: function IUI_toolsChanged(aUpdater)
   {
     this.toolsDo(function IUI_toolsOnChanged(aTool) {
-      if (aTool.isOpen && ("onChanged" in aTool) && aTool != aUpdater) {
+      if (("onChanged" in aTool) && aTool != aUpdater) {
         aTool.onChanged.call(aTool.context);
       }
     });
   },
 
   /**
    * Loop through all registered tools and pass each into the provided function
    * @param aFunction The function to which each tool is to be passed
@@ -1659,16 +1784,23 @@ HTMLBreadcrumbs.prototype = {
   {
     let text = aNode.tagName.toLowerCase();
     if (aNode.id) {
       text += "#" + aNode.id;
     }
     for (let i = 0; i < aNode.classList.length; i++) {
       text += "." + aNode.classList[i];
     }
+    for (let i = 0; i < PSEUDO_CLASSES.length; i++) {
+      let pseudo = PSEUDO_CLASSES[i];
+      if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) {
+        text += pseudo;  
+      }      
+    }
+
     return text;
   },
 
 
   /**
    * Build <label>s that represent the node:
    *   <label class="inspector-breadcrumbs-tag">tagName</label>
    *   <label class="inspector-breadcrumbs-id">#id</label>
@@ -1684,29 +1816,38 @@ HTMLBreadcrumbs.prototype = {
     let tagLabel = this.IUI.chromeDoc.createElement("label");
     tagLabel.className = "inspector-breadcrumbs-tag plain";
 
     let idLabel = this.IUI.chromeDoc.createElement("label");
     idLabel.className = "inspector-breadcrumbs-id plain";
 
     let classesLabel = this.IUI.chromeDoc.createElement("label");
     classesLabel.className = "inspector-breadcrumbs-classes plain";
+    
+    let pseudosLabel = this.IUI.chromeDoc.createElement("label");
+    pseudosLabel.className = "inspector-breadcrumbs-pseudo-classes plain";
 
     tagLabel.textContent = aNode.tagName.toLowerCase();
     idLabel.textContent = aNode.id ? ("#" + aNode.id) : "";
 
     let classesText = "";
     for (let i = 0; i < aNode.classList.length; i++) {
       classesText += "." + aNode.classList[i];
     }
     classesLabel.textContent = classesText;
 
+    let pseudos = PSEUDO_CLASSES.filter(function(pseudo) {
+      return DOMUtils.hasPseudoClassLock(aNode, pseudo);
+    }, this);
+    pseudosLabel.textContent = pseudos.join("");
+
     fragment.appendChild(tagLabel);
     fragment.appendChild(idLabel);
     fragment.appendChild(classesLabel);
+    fragment.appendChild(pseudosLabel);
 
     return fragment;
   },
 
   /**
    * Open the sibling menu.
    *
    * @param aButton the button representing the node.
@@ -1736,17 +1877,17 @@ HTMLBreadcrumbs.prototype = {
           item.setAttribute("checked", "true");
         }
 
         item.setAttribute("type", "radio");
         item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i]));
 
         item.onmouseup = (function(aNode) {
           return function() {
-            inspector.select(aNode, true, true);
+            inspector.select(aNode, true, true, "breadcrumbs");
           }
         })(nodes[i]);
 
         fragment.appendChild(item);
       }
     }
     this.menu.appendChild(fragment);
     this.menu.openPopup(aButton, "before_start", 0, 0, true, false);
@@ -1890,17 +2031,17 @@ HTMLBreadcrumbs.prototype = {
     let inspector = this.IUI;
     button.appendChild(this.prettyPrintNodeAsXUL(aNode));
     button.className = "inspector-breadcrumbs-button";
 
     button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(aNode));
 
     button.onBreadcrumbsClick = function onBreadcrumbsClick() {
       inspector.stopInspecting();
-      inspector.select(aNode, true, true);
+      inspector.select(aNode, true, true, "breadcrumbs");
     };
 
     button.onclick = (function _onBreadcrumbsRightClick(aEvent) {
       if (aEvent.button == 2) {
         this.openSiblingMenu(button, aNode);
       }
     }).bind(this);
 
@@ -2005,16 +2146,30 @@ HTMLBreadcrumbs.prototype = {
   scroll: function BC_scroll()
   {
     // FIXME bug 684352: make sure its immediate neighbors are visible too.
 
     let scrollbox = this.container;
     let element = this.nodeHierarchy[this.currentIndex].button;
     scrollbox.ensureElementIsVisible(element);
   },
+  
+  updateSelectors: function BC_updateSelectors()
+  {
+    for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) {
+      let crumb = this.nodeHierarchy[i];
+      let button = crumb.button;
+
+      while(button.hasChildNodes()) {
+        button.removeChild(button.firstChild);
+      }
+      button.appendChild(this.prettyPrintNodeAsXUL(crumb.node));
+      button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node));
+    }
+  },
 
   /**
    * Update the breadcrumbs display when a new node is selected.
    */
   update: function BC_update()
   {
     this.menu.hidePopup();
 
@@ -2046,16 +2201,18 @@ HTMLBreadcrumbs.prototype = {
       idx = this.indexOf(selection);
       this.setCursor(idx);
     }
     // Add the first child of the very last node of the breadcrumbs if possible.
     this.ensureFirstChild();
 
     // Make sure the selected node and its neighbours are visible.
     this.scroll();
+
+    this.updateSelectors();
   },
 
 }
 
 /////////////////////////////////////////////////////////////////////////
 //// Initializers
 
 XPCOMUtils.defineLazyGetter(InspectorUI.prototype, "strings",
@@ -2065,8 +2222,11 @@ XPCOMUtils.defineLazyGetter(InspectorUI.
   });
 
 XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () {
   var obj = {};
   Cu.import("resource:///modules/devtools/StyleInspector.jsm", obj);
   return obj.StyleInspector;
 });
 
+XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
+  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
--- a/browser/devtools/highlighter/test/Makefile.in
+++ b/browser/devtools/highlighter/test/Makefile.in
@@ -65,16 +65,19 @@ include $(topsrcdir)/config/rules.mk
 		browser_inspector_keybindings.js \
 		browser_inspector_breadcrumbs.html \
 		browser_inspector_breadcrumbs.js \
 		browser_inspector_bug_699308_iframe_navigation.js \
 		browser_inspector_changes.js \
 		browser_inspector_ruleviewstore.js \
 		browser_inspector_duplicate_ruleview.js \
 		browser_inspector_invalidate.js \
+		browser_inspector_sidebarstate.js \
+		browser_inspector_treePanel_menu.js \
+		browser_inspector_pseudoclass_lock.js \
 		head.js \
 		$(NULL)
 
 # Disabled due to constant failures
 # 		browser_inspector_treePanel_click.js \
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
--- a/browser/devtools/highlighter/test/browser_inspector_editor.js
+++ b/browser/devtools/highlighter/test/browser_inspector_editor.js
@@ -29,17 +29,17 @@ function setupEditorTests()
   Services.obs.addObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
   InspectorUI.toggleInspectorUI();
 }
 
 function setupHTMLPanel()
 {
   Services.obs.removeObserver(setupHTMLPanel, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
   Services.obs.addObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
-  InspectorUI.toolShow(InspectorUI.treePanel.registrationObject);
+  InspectorUI.toggleHTMLPanel();
 }
 
 function runEditorTests()
 {
   Services.obs.removeObserver(runEditorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
   InspectorUI.stopInspecting();
   InspectorUI.inspectNode(doc.body, true);
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/test/browser_inspector_pseudoclass_lock.js
@@ -0,0 +1,154 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+
+let doc;
+let div;
+
+let pseudo = ":hover";
+
+function test()
+{
+  waitForExplicitFinish();
+  ignoreAllUncaughtExceptions();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,pseudo-class lock tests";
+}
+
+function createDocument()
+{  
+  div = doc.createElement("div");
+  div.textContent = "test div";
+
+  let head = doc.getElementsByTagName('head')[0];
+  let style = doc.createElement('style');
+  let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }');
+
+  style.appendChild(rules);
+  head.appendChild(style);
+  doc.body.appendChild(div);
+  
+  setupTests();
+}
+
+function setupTests()
+{
+  Services.obs.addObserver(selectNode,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+  InspectorUI.openInspectorUI();
+}
+
+function selectNode()
+{
+  Services.obs.removeObserver(selectNode,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+
+  executeSoon(function() {
+    InspectorUI.highlighter.addListener("nodeselected", openRuleView);
+    InspectorUI.inspectNode(div);
+  });
+}
+
+function openRuleView()
+{
+  Services.obs.addObserver(performTests,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false);
+
+  InspectorUI.showSidebar();
+  InspectorUI.openRuleView();
+}
+
+function performTests()
+{
+  Services.obs.removeObserver(performTests,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY);
+
+  InspectorUI.highlighter.removeListener("nodeselected", performTests);
+
+  // toggle the class
+  InspectorUI.highlighter.pseudoClassLockToggled(pseudo);
+
+  testAdded();
+
+  // toggle the lock off
+  InspectorUI.highlighter.pseudoClassLockToggled(pseudo);
+
+  testRemoved();
+  testRemovedFromUI();
+
+  // toggle it back on
+  InspectorUI.highlighter.pseudoClassLockToggled(pseudo);  
+
+  // close the inspector
+  Services.obs.addObserver(testInspectorClosed,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+  InspectorUI.closeInspectorUI();
+}
+
+function testAdded()
+{
+  // lock is applied to it and ancestors
+  let node = div;
+  do {
+    is(DOMUtils.hasPseudoClassLock(node, pseudo), true,
+       "pseudo-class lock has been applied");
+    node = node.parentNode;
+  } while (node.parentNode)
+
+  // infobar selector contains pseudo-class
+  let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes");
+  is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector");
+  
+  // ruleview contains pseudo-class rule
+  is(InspectorUI.ruleView.element.children.length, 3,
+     "rule view is showing 3 rules for pseudo-class locked div");
+     
+  is(InspectorUI.ruleView.element.children[1]._ruleEditor.rule.selectorText,
+     "div:hover", "rule view is showing " + pseudo + " rule");
+}
+
+function testRemoved()
+{
+  // lock removed from node and ancestors  
+  let node = div;
+  do {
+    is(DOMUtils.hasPseudoClassLock(node, pseudo), false,
+       "pseudo-class lock has been removed");
+    node = node.parentNode;
+  } while (node.parentNode)
+}
+
+function testRemovedFromUI()
+{
+  // infobar selector doesn't contain pseudo-class
+  let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes");
+  is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector");    
+
+  // ruleview no longer contains pseudo-class rule
+  is(InspectorUI.ruleView.element.children.length, 2,
+     "rule view is showing 2 rules after removing lock");    
+}
+
+function testInspectorClosed()
+{
+  Services.obs.removeObserver(testInspectorClosed,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
+
+  testRemoved();
+
+  finishUp();  
+}
+
+function finishUp()
+{
+  doc = div = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
--- a/browser/devtools/highlighter/test/browser_inspector_registertools.js
+++ b/browser/devtools/highlighter/test/browser_inspector_registertools.js
@@ -143,55 +143,16 @@ function startToolTests(evt)
   InspectorUI.toolShow(tool1);
   InspectorUI.toolShow(tool3);
 
   info("Checking panel states 4");
   ok(tool1.isOpen, "Panel 1 is open");
   ok(!tool2.isOpen, "Panel 2 is closed");
   ok(tool3.isOpen, "Panel 3 is open");
 
-  gBrowser.selectedTab = gBrowser.addTab();
-  gBrowser.selectedBrowser.addEventListener("load", function() {
-    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
-    waitForFocus(testSecondTab, content);
-  }, true);
-
-  content.location = "data:text/html,registertool new tab test for inspector";
-}
-
-function testSecondTab()
-{
-  info("Opened second tab");
-  info("Checking panel states 5");
-
-  let tools = InspectorUI.tools;
-  ok(!(tool1 in tools), "Panel 1 not in tools");
-  ok(!(tool2 in tools), "Panel 2 not in tools");
-  ok(!(tool3 in tools), "Panel 3 not in tools");
-
-  info("Closing current tab");
-  Services.obs.addObserver(testOriginalTab, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
-  gBrowser.removeCurrentTab();
-}
-
-function testOriginalTab()
-{
-  Services.obs.removeObserver(testOriginalTab, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
-  info("Checking panel states 6");
-
-  info("Tools: " + InspectorUI.tools);
-  // reacquaint ourselves with our tools
-  tool1 = InspectorUI.tools["tool_1"];
-  tool2 = InspectorUI.tools["tool_2"];
-  tool3 = InspectorUI.tools["tool_3"];
-
-  ok(tool1.isOpen, "Panel 1 is open after reactivation");
-  ok(!tool2.isOpen, "Panel 2 is closed after reactivation");
-  ok(tool3.isOpen, "Panel 3 is open after reactivation");
-
   Services.obs.addObserver(unregisterTools, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
   InspectorUI.closeInspectorUI(true);
 }
 
 function unregisterTools()
 {
   Services.obs.removeObserver(unregisterTools, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
   let tools = InspectorUI.tools;
--- a/browser/devtools/highlighter/test/browser_inspector_ruleviewstore.js
+++ b/browser/devtools/highlighter/test/browser_inspector_ruleviewstore.js
@@ -123,18 +123,18 @@ function inspectorFocusTab1()
 }
 
 function ruleViewOpened2()
 {
   let prop = InspectorUI.ruleView._elementStyle.rules[0].textProps[0];
   is(prop.name, "background-color", "First prop is the background color prop.");
   ok(!prop.enabled, "First prop should be disabled.");
 
+  InspectorUI.closeInspectorUI();
   gBrowser.removeCurrentTab();
-  InspectorUI.closeInspectorUI();
   finish();
 }
 
 function test()
 {
   waitForExplicitFinish();
 
   tab1 = gBrowser.addTab();
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/test/browser_inspector_sidebarstate.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let doc;
+
+function createDocument()
+{
+  doc.body.innerHTML = '<h1>Sidebar state test</h1>';
+  doc.title = "Sidebar State Test";
+
+  // Open the sidebar and wait for the default view (the rule view) to show.
+  Services.obs.addObserver(inspectorRuleViewOpened,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false);
+
+  InspectorUI.openInspectorUI();
+  InspectorUI.showSidebar();
+}
+
+function inspectorRuleViewOpened()
+{
+  Services.obs.removeObserver(inspectorRuleViewOpened,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY);
+  is(InspectorUI.activeSidebarPanel, "ruleview", "Rule View is selected by default");
+
+  // Select the computed view and turn off the inspector.
+  InspectorUI.activateSidebarPanel("styleinspector");
+
+  Services.obs.addObserver(inspectorClosed,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+  InspectorUI.closeInspectorUI();
+}
+
+function inspectorClosed()
+{
+  // Reopen the inspector, expect the computed view to be loaded.
+  Services.obs.removeObserver(inspectorClosed,
+    InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
+
+  Services.obs.addObserver(computedViewPopulated,
+    "StyleInspector-populated", false);
+
+  InspectorUI.openInspectorUI();
+}
+
+function computedViewPopulated()
+{
+  Services.obs.removeObserver(computedViewPopulated,
+    "StyleInspector-populated");
+  is(InspectorUI.activeSidebarPanel, "styleinspector", "Computed view is selected by default.");
+
+  finishTest();
+}
+
+
+function finishTest()
+{
+  InspectorUI.closeInspectorUI();
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function() {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = "data:text/html,basic tests for inspector";
+}
+
--- a/browser/devtools/highlighter/test/browser_inspector_tab_switch.js
+++ b/browser/devtools/highlighter/test/browser_inspector_tab_switch.js
@@ -91,16 +91,17 @@ function inspectorTabOpen2()
   ok(!InspectorUI.treePanel, "Inspector Tree Panel is closed");
   ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
 
   // Activate the inspector again.
   executeSoon(function() {
     Services.obs.addObserver(inspectorUIOpen2,
       InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+    clearUserPrefs();
     InspectorUI.openInspectorUI();
   });
 }
 
 function inspectorUIOpen2()
 {
   Services.obs.removeObserver(inspectorUIOpen2,
     InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
@@ -131,34 +132,34 @@ function inspectorFocusTab1()
   ok(InspectorUI.inspecting, "Inspector is highlighting");
   ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
   is(InspectorUI.selection, div, "selection matches the div element");
 
   Services.obs.addObserver(inspectorOpenTreePanelTab1,
     InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
 
-  InspectorUI.treePanel.open();
+  InspectorUI.toggleHTMLPanel();
 }
 
 function inspectorOpenTreePanelTab1()
 {
   Services.obs.removeObserver(inspectorOpenTreePanelTab1,
     InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY);
 
   ok(InspectorUI.inspecting, "Inspector is highlighting");
   ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open");
   is(InspectorUI.store.length, 2, "Inspector.store.length = 2");
   is(InspectorUI.selection, div, "selection matches the div element");
 
   Services.obs.addObserver(inspectorSidebarStyleView1, "StyleInspector-opened", false);
 
   executeSoon(function() {
     InspectorUI.showSidebar();
-    InspectorUI.toolShow(InspectorUI.stylePanel.registrationObject);
+    InspectorUI.activateSidebarPanel("styleinspector");
   });
 }
 
 function inspectorSidebarStyleView1()
 {
   Services.obs.removeObserver(inspectorSidebarStyleView1, "StyleInspector-opened");
   ok(InspectorUI.isSidebarOpen, "Inspector Sidebar is open");
   ok(InspectorUI.stylePanel, "Inspector Has a Style Panel Instance");
new file mode 100644
--- /dev/null
+++ b/browser/devtools/highlighter/test/browser_inspector_treePanel_menu.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function test() {
+
+  waitForExplicitFinish();
+
+  let doc;
+  let node1;
+  let div;
+
+  function createDocument() {
+    div = doc.createElement("div");
+    let h1 = doc.createElement("h1");
+    let p1 = doc.createElement("p");
+    let p2 = doc.createElement("p");
+    doc.title = "Inspector Tree Menu Test";
+    h1.textContent = "Inspector Tree Menu Test";
+    p1.textContent = "This is some example text";
+    div.appendChild(h1);
+    div.appendChild(p1);
+    doc.body.appendChild(div);
+    node1 = p1;
+    setupTest();
+  }
+
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(createDocument, content);
+  }, true);
+
+  content.location = content.location = "data:text/html,basic tests for inspector";;
+
+  function setupTest() {
+    Services.obs.addObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
+    InspectorUI.toggleInspectorUI();
+  }
+
+  function runTests() {
+    Services.obs.removeObserver(runTests, InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED);
+    Services.obs.addObserver(testCopyInnerMenu, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false);
+    InspectorUI.stopInspecting();
+    InspectorUI.inspectNode(node1, true);
+    InspectorUI.treePanel.open();
+  }
+
+  function testCopyInnerMenu() {
+    let copyInner = document.getElementById("inspectorHTMLCopyInner");
+    ok(copyInner, "the popup menu has a copy inner html menu item");
+
+    waitForClipboard("This is some example text",
+                     function() { copyInner.doCommand(); },
+                     testCopyOuterMenu, testCopyOuterMenu);
+  }
+
+  function testCopyOuterMenu() {
+    let copyOuter = document.getElementById("inspectorHTMLCopyOuter");
+    ok(copyOuter, "the popup menu has a copy outer html menu item");
+
+    waitForClipboard("<p>This is some example text</p>",
+                     function() { copyOuter.doCommand(); },
+                     testDeleteNode, testDeleteNode);
+  }
+
+  function testDeleteNode() {
+    let deleteNode = document.getElementById("inspectorHTMLDelete");
+    ok(deleteNode, "the popup menu has a delete menu item");
+
+    InspectorUI.highlighter.addListener("nodeselected", deleteTest);
+
+    let commandEvent = document.createEvent("XULCommandEvent");
+    commandEvent.initCommandEvent("command", true, true, window, 0, false, false,
+                                  false, false, null);
+    deleteNode.dispatchEvent(commandEvent);
+  }
+
+  function deleteTest() {
+    InspectorUI.highlighter.removeListener("nodeSelected", deleteTest);
+    Services.obs.addObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
+    is(InspectorUI.selection, div, "parent node selected");
+    let p = doc.querySelector("P");
+    is(p, null, "node deleted");
+    executeSoon(function() {
+      InspectorUI.closeInspectorUI();
+    });
+  }
+
+  function finishUp() {
+    Services.obs.removeObserver(finishUp, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
+    doc = node1 = div = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/highlighter/test/head.js
+++ b/browser/devtools/highlighter/test/head.js
@@ -36,16 +36,26 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const Cu = Components.utils;
 let tempScope = {};
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
 let LayoutHelpers = tempScope.LayoutHelpers;
 
+// Clear preferences that may be set during the course of tests.
+function clearUserPrefs()
+{
+  Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
+  Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
+  Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
+}
+
+registerCleanupFunction(clearUserPrefs);
+
 function isHighlighting()
 {
   let veil = InspectorUI.highlighter.veilTransparentBox;
   return !(veil.style.visibility == "hidden");
 }
 
 function getHighlitNode()
 {
@@ -73,8 +83,9 @@ function getHighlitNode()
 
 function midPoint(aPointA, aPointB)
 {
   let pointC = { };
   pointC.x = (aPointB.x - aPointA.x) / 2 + aPointA.x;
   pointC.y = (aPointB.y - aPointA.y) / 2 + aPointA.y;
   return pointC;
 }
+
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -436,28 +436,29 @@ var Scratchpad = {
     if (!error) {
       this.writeAsComment(result);
     } else {
       this.writeAsErrorComment(error);
     }
   },
 
   /**
-   * Write out a value at the current insertion point as a block comment
+   * Write out a value at the next line from the current insertion point.
+   * The comment block will always be preceded by a newline character.
    * @param object aValue
    *        The Object to write out as a string
    */
   writeAsComment: function SP_writeAsComment(aValue)
   {
     let selection = this.getSelectionRange();
     let insertionPoint = selection.start != selection.end ?
                          selection.end : // after selected text
                          this.editor.getCharCount(); // after text end
                          
-    let newComment = "/*\n" + aValue + "\n*/";
+    let newComment = "\n/*\n" + aValue + "\n*/";
     
     this.setText(newComment, insertionPoint, insertionPoint);
 
     // Select the new comment.
     this.selectRange(insertionPoint, insertionPoint + newComment.length);
   },
 
   /**
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug690552_display_outputs_errors.js
@@ -16,17 +16,17 @@ function test()
       "comments for 'display' and not sent to the console in Scratchpad";
 }
 
 function runTests()
 {
   var scratchpad = gScratchpadWindow.Scratchpad;
 
   var message = "\"Hello World!\""
-  var openComment = "/*\n";
+  var openComment = "\n/*\n";
   var closeComment = "\n*/";
   var error = "throw new Error(\"Ouch!\")";
   let messageArray = {};
   let count = {};
 
   scratchpad.setText(message);
   scratchpad.display();
   is(scratchpad.getText(),
--- a/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js
@@ -25,30 +25,30 @@ function testFalsy()
 
   finish();
 }
 
 function verifyFalsies(sp)
 {
   sp.setText("undefined");
   sp.display();
-  is(sp.selectedText, "/*\nundefined\n*/", "'undefined' is displayed");
+  is(sp.selectedText, "\n/*\nundefined\n*/", "'undefined' is displayed");
 
   sp.setText("false");
   sp.display();
-  is(sp.selectedText, "/*\nfalse\n*/", "'false' is displayed");
+  is(sp.selectedText, "\n/*\nfalse\n*/", "'false' is displayed");
 
   sp.setText("0");
   sp.display();
-  is(sp.selectedText, "/*\n0\n*/", "'0' is displayed");
+  is(sp.selectedText, "\n/*\n0\n*/", "'0' is displayed");
 
   sp.setText("null");
   sp.display();
-  is(sp.selectedText, "/*\nnull\n*/", "'null' is displayed");
+  is(sp.selectedText, "\n/*\nnull\n*/", "'null' is displayed");
 
   sp.setText("NaN");
   sp.display();
-  is(sp.selectedText, "/*\nNaN\n*/", "'NaN' is displayed");
+  is(sp.selectedText, "\n/*\nNaN\n*/", "'NaN' is displayed");
 
   sp.setText("''");
   sp.display();
-  is(sp.selectedText, "/*\n\n*/", "empty string is displayed");
+  is(sp.selectedText, "\n/*\n\n*/", "empty string is displayed");
 }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js
@@ -35,23 +35,23 @@ function runTests()
   is(content.wrappedJSObject.foobarBug636725, 2,
      "run() updated window.foobarBug636725");
 
   sp.display();
 
   is(content.wrappedJSObject.foobarBug636725, 3,
      "display() updated window.foobarBug636725");
 
-  is(sp.getText(), "++window.foobarBug636725/*\n3\n*/",
+  is(sp.getText(), "++window.foobarBug636725\n/*\n3\n*/",
      "display() shows evaluation result in the textbox");
 
-  is(sp.selectedText, "/*\n3\n*/", "selectedText is correct");
+  is(sp.selectedText, "\n/*\n3\n*/", "selectedText is correct");
   let selection = sp.getSelectionRange();
   is(selection.start, 24, "selection.start is correct");
-  is(selection.end, 31, "selection.end is correct");
+  is(selection.end, 32, "selection.end is correct");
 
   // Test selection run() and display().
 
   sp.setText("window.foobarBug636725 = 'a';\n" +
              "window.foobarBug636725 = 'b';");
 
   sp.selectRange(1, 2);
 
@@ -89,26 +89,26 @@ function runTests()
   sp.selectRange(0, 22);
 
   sp.display();
 
   is(content.wrappedJSObject.foobarBug636725, "a",
      "display() worked for the selected range");
 
   is(sp.getText(), "window.foobarBug636725" +
-                   "/*\na\n*/" +
+                   "\n/*\na\n*/" +
                    " = 'c';\n" +
                    "window.foobarBug636725 = 'b';",
      "display() shows evaluation result in the textbox");
 
-  is(sp.selectedText, "/*\na\n*/", "selectedText is correct");
+  is(sp.selectedText, "\n/*\na\n*/", "selectedText is correct");
 
   selection = sp.getSelectionRange();
   is(selection.start, 22, "selection.start is correct");
-  is(selection.end, 29, "selection.end is correct");
+  is(selection.end, 30, "selection.end is correct");
 
   sp.deselect();
 
   ok(!sp.selectedText, "selectedText is empty");
 
   selection = sp.getSelectionRange();
   is(selection.start, selection.end, "deselect() works");
 
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -111,17 +111,17 @@ function ElementStyle(aElement, aStore)
 
   let doc = aElement.ownerDocument;
 
   // To figure out how shorthand properties are interpreted by the
   // engine, we will set properties on a dummy element and observe
   // how their .style attribute reflects them as computed values.
   this.dummyElement = doc.createElementNS(this.element.namespaceURI,
                                           this.element.tagName);
-  this._populate();
+  this.populate();
 }
 // We're exporting _ElementStyle for unit tests.
 var _ElementStyle = ElementStyle;
 
 ElementStyle.prototype = {
 
   // The element we're looking at.
   element: null,
@@ -142,17 +142,17 @@ ElementStyle.prototype = {
       this.onChanged();
     }
   },
 
   /**
    * Refresh the list of rules to be displayed for the active element.
    * Upon completion, this.rules[] will hold a list of Rule objects.
    */
-  _populate: function ElementStyle_populate()
+  populate: function ElementStyle_populate()
   {
     this.rules = [];
 
     let element = this.element;
     do {
       this._addElementRules(element);
     } while ((element = element.parentNode) &&
              element.nodeType === Ci.nsIDOMNode.ELEMENT_NODE);
@@ -708,25 +708,43 @@ CssRuleView.prototype = {
 
     this._elementStyle = new ElementStyle(aElement, this.store);
     this._elementStyle.onChanged = function() {
       this._changed();
     }.bind(this);
 
     this._createEditors();
   },
+  
+  /**
+   * Update the rules for the currently highlighted element.
+   */
+  nodeChanged: function CssRuleView_nodeChanged()
+  {
+    this._clearRules();
+    this._elementStyle.populate();
+    this._createEditors();
+  },  
+
+  /**
+   * Clear the rules.
+   */
+  _clearRules: function CssRuleView_clearRules()
+  {
+    while (this.element.hasChildNodes()) {
+      this.element.removeChild(this.element.lastChild);
+    }
+  },
 
   /**
    * Clear the rule view.
    */
   clear: function CssRuleView_clear()
   {
-    while (this.element.hasChildNodes()) {
-      this.element.removeChild(this.element.lastChild);
-    }
+    this._clearRules();
     this._viewedElement = null;
     this._elementStyle = null;
   },
 
   /**
    * Called when the user has made changes to the ElementStyle.
    * Emits an event that clients can listen to.
    */
--- a/browser/devtools/styleinspector/StyleInspector.jsm
+++ b/browser/devtools/styleinspector/StyleInspector.jsm
@@ -115,16 +115,21 @@ StyleInspector.prototype = {
           this.iframe.getAttribute("src") ==
           "chrome://browser/content/devtools/csshtmltree.xul") {
         let selectedNode = this.selectedNode || null;
         this.cssHtmlTree = new CssHtmlTree(this);
         this.cssLogic.highlight(selectedNode);
         this.cssHtmlTree.highlight(selectedNode);
         this.iframe.removeEventListener("load", boundIframeOnLoad, true);
         this.iframeReady = true;
+
+        // Now that we've loaded, select any node we were previously asked
+        // to show.
+        this.selectNode(this.selectedNode);
+
         Services.obs.notifyObservers(null, "StyleInspector-opened", null);
       }
     }.bind(this);
 
     this.iframe = this.IUI.getToolIframe(this.registrationObject);
 
     this.iframe.addEventListener("load", boundIframeOnLoad, true);
   },
@@ -210,21 +215,26 @@ StyleInspector.prototype = {
   },
 
   /**
    * Check if the style inspector is open.
    * @returns boolean
    */
   isOpen: function SI_isOpen()
   {
-    return this.openDocked ? this.iframeReady && this.IUI.isSidebarOpen &&
+    return this.openDocked ? this.IUI.isSidebarOpen &&
             (this.IUI.sidebarDeck.selectedPanel == this.iframe) :
            this.panel && this.panel.state && this.panel.state == "open";
   },
 
+  isLoaded: function SI_isLoaded()
+  {
+    return this.openDocked ? this.iframeReady : this.iframeReady && this.panelReady;
+  },
+
   /**
    * Select from Path (via CssHtmlTree_pathClick)
    * @param aNode The node to inspect.
    */
   selectFromPath: function SI_selectFromPath(aNode)
   {
     if (this.IUI && this.IUI.selection) {
       if (aNode != this.IUI.selection) {
@@ -237,28 +247,28 @@ StyleInspector.prototype = {
 
   /**
    * Select a node to inspect in the Style Inspector panel
    * @param aNode The node to inspect.
    */
   selectNode: function SI_selectNode(aNode)
   {
     this.selectedNode = aNode;
-    if (this.isOpen() && !this.dimmed) {
+    if (this.isLoaded() && !this.dimmed) {
       this.cssLogic.highlight(aNode);
       this.cssHtmlTree.highlight(aNode);
     }
   },
 
   /**
    * Update the display for the currently-selected node.
    */
   updateNode: function SI_updateNode()
   {
-    if (this.isOpen() && !this.dimmed) {
+    if (this.isLoaded() && !this.dimmed) {
       this.cssLogic.highlight(this.selectedNode);
       this.cssHtmlTree.refreshPanel();
     }
   },
 
   /**
    * Dim or undim a panel by setting or removing a dimmed attribute.
    * @param aState
--- a/browser/devtools/tilt/Tilt.jsm
+++ b/browser/devtools/tilt/Tilt.jsm
@@ -122,76 +122,81 @@ Tilt.prototype = {
       return;
     }
 
     // create a visualizer instance for the current tab
     this.visualizers[id] = new TiltVisualizer({
       chromeWindow: this.chromeWindow,
       contentWindow: this.chromeWindow.gBrowser.selectedBrowser.contentWindow,
       parentNode: this.chromeWindow.gBrowser.selectedBrowser.parentNode,
-      requestAnimationFrame: this.chromeWindow.mozRequestAnimationFrame,
       notifications: this.NOTIFICATIONS
     });
 
     // make sure the visualizer object was initialized properly
     if (!this.visualizers[id].isInitialized()) {
       this.destroy(id);
       return;
     }
 
     Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.INITIALIZING, null);
   },
 
   /**
-   * Destroys a specific instance of the visualizer.
+   * Starts destroying 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, aAnimateFlag)
   {
-    // if the visualizer is already destroyed, don't do anything
-    if (!this.visualizers[aId]) {
+    // if the visualizer is destroyed or destroying, don't do anything
+    if (!this.visualizers[aId] || this._isDestroying) {
+      return;
+    }
+    this._isDestroying = true;
+
+    let controller = this.visualizers[aId].controller;
+    let presenter = this.visualizers[aId].presenter;
+
+    let content = presenter.contentWindow;
+    let pageXOffset = content.pageXOffset * presenter.transforms.zoom;
+    let pageYOffset = content.pageYOffset * presenter.transforms.zoom;
+    TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom);
+
+    // if we're not doing any outro animation, just finish destruction directly
+    if (!aAnimateFlag) {
+      this._finish(aId);
       return;
     }
 
-    if (!this.isDestroying) {
-      this.isDestroying = true;
+    // otherwise, trigger the outro animation and notify necessary observers
+    Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYING, null);
 
-      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);
-      };
+    controller.removeEventListeners();
+    controller.arcball.reset([-pageXOffset, -pageYOffset]);
+    presenter.executeDestruction(this._finish.bind(this, aId));
+  },
 
-      if (!aAnimateFlag) {
-        finalize.call(this, aId);
-        return;
-      }
-
-      let controller = this.visualizers[aId].controller;
-      let presenter = this.visualizers[aId].presenter;
+  /**
+   * Finishes detroying a specific instance of the visualizer.
+   *
+   * @param {String} aId
+   *                 the identifier of the instance in the visualizers array
+   */
+  _finish: function T__finish(aId)
+  {
+    this.visualizers[aId].removeOverlay();
+    this.visualizers[aId].cleanup();
+    this.visualizers[aId] = null;
 
-      let content = presenter.contentWindow;
-      let pageXOffset = content.pageXOffset * presenter.transforms.zoom;
-      let pageYOffset = content.pageYOffset * presenter.transforms.zoom;
-
-      Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYING, null);
-      TiltUtils.setDocumentZoom(this.chromeWindow, presenter.transforms.zoom);
-
-      controller.removeEventListeners();
-      controller.arcball.reset([-pageXOffset, -pageYOffset]);
-      presenter.executeDestruction(finalize.bind(this, aId));
-    }
+    this._isDestroying = false;
+    this.chromeWindow.gBrowser.selectedBrowser.focus();
+    Services.obs.notifyObservers(null, TILT_NOTIFICATIONS.DESTROYED, null);
   },
 
   /**
    * Handles any supplementary post-initialization work, done immediately
    * after a TILT_NOTIFICATIONS.INITIALIZING notification.
    */
   _whenInitializing: function T__whenInitializing()
   {
@@ -281,26 +286,28 @@ Tilt.prototype = {
       this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.DESTROYED, false);
 
     this.chromeWindow.gBrowser.tabContainer.addEventListener("TabSelect",
       this._onTabSelect.bind(this), false);
 
 
     // FIXME: this shouldn't be done here, see bug #705131
     let onOpened = function() {
-      if (this.currentInstance) {
-        this.chromeWindow.InspectorUI.stopInspecting();
-        this.inspectButton.disabled = true;
-        this.highlighterContainer.style.display = "none";
+      if (this.inspector && this.highlighter && this.currentInstance) {
+        this.inspector.stopInspecting();
+        this.inspector.inspectToolbutton.disabled = true;
+        this.highlighter.hide();
       }
     }.bind(this);
 
     let onClosed = function() {
-      this.inspectButton.disabled = false;
-      this.highlighterContainer.style.display = "";
+      if (this.inspector && this.highlighter) {
+        this.inspector.inspectToolbutton.disabled = false;
+        this.highlighter.show();
+      }
     }.bind(this);
 
     Services.obs.addObserver(onOpened,
       this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false);
     Services.obs.addObserver(onClosed,
       this.chromeWindow.InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false);
     Services.obs.addObserver(onOpened,
       TILT_NOTIFICATIONS.INITIALIZING, false);
@@ -333,36 +340,31 @@ Tilt.prototype = {
    * Gets the visualizer instance for the current tab.
    */
   get currentInstance()
   {
     return this.visualizers[this.currentWindowId];
   },
 
   /**
+   * Gets the current InspectorUI instance.
+   */
+  get inspector()
+  {
+    return this.chromeWindow.InspectorUI;
+  },
+
+  /**
+   * Gets the current Highlighter instance from the InspectorUI.
+   */
+  get highlighter()
+  {
+    return this.inspector.highlighter;
+  },
+
+  /**
    * Gets the Tilt button in the Inspector toolbar.
    */
   get tiltButton()
   {
-    return this.chromeWindow.document.getElementById(
-      "inspector-3D-button");
-  },
-
-  /**
-   * Gets the Inspect button in the Inspector toolbar.
-   * FIXME: this shouldn't be needed here, remove after bug #705131
-   */
-  get inspectButton()
-  {
-    return this.chromeWindow.document.getElementById(
-      "inspector-inspect-toolbutton");
-  },
-
-  /**
-   * Gets the Highlighter contaniner stack.
-   * FIXME: this shouldn't be needed here, remove after bug #705131
-   */
-  get highlighterContainer()
-  {
-    return this.chromeWindow.document.getElementById(
-      "highlighter-container");
+    return this.chromeWindow.document.getElementById("inspector-3D-button");
   }
 };
--- a/browser/devtools/tilt/TiltGL.jsm
+++ b/browser/devtools/tilt/TiltGL.jsm
@@ -87,16 +87,18 @@ TiltGL.Renderer = function TGL_Renderer(
   this.context.clearColor(0, 0, 0, 0);
   this.context.clearDepth(1);
 
   /**
    * Variables representing the current framebuffer width and height.
    */
   this.width = aCanvas.width;
   this.height = aCanvas.height;
+  this.initialWidth = this.width;
+  this.initialHeight = this.height;
 
   /**
    * The current model view matrix.
    */
   this.mvMatrix = mat4.identity(mat4.create());
 
   /**
    * The current projection matrix.
@@ -859,32 +861,40 @@ TiltGL.Program.prototype = {
     let utils = TiltGL.ProgramUtils;
 
     // check if the program wasn't already active
     if (utils._activeProgram !== id) {
       utils._activeProgram = id;
 
       // use the the program if it wasn't already set
       this._context.useProgram(this._ref);
-
-      // check if the required vertex attributes aren't already set
-      if (utils._enabledAttributes < this._attributes.length) {
-        utils._enabledAttributes = this._attributes.length;
+      this.cleanupVertexAttrib();
 
-        // enable any necessary vertex attributes using the cache
-        for (let i in this._attributes) {
-          if (this._attributes.hasOwnProperty(i)) {
-            this._context.enableVertexAttribArray(this._attributes[i]);
-          }
-        }
+      // enable any necessary vertex attributes using the cache
+      for each (let attribute in this._attributes) {
+        this._context.enableVertexAttribArray(attribute);
+        utils._enabledAttributes.push(attribute);
       }
     }
   },
 
   /**
+   * Disables all currently enabled vertex attribute arrays.
+   */
+  cleanupVertexAttrib: function TGLP_cleanupVertexAttrib()
+  {
+    let utils = TiltGL.ProgramUtils;
+
+    for each (let attribute in utils._enabledAttributes) {
+      this._context.disableVertexAttribArray(attribute);
+    }
+    utils._enabledAttributes = [];
+  },
+
+  /**
    * Binds a vertex buffer as an array buffer for a specific shader attribute.
    *
    * @param {String} aAtribute
    *                 the attribute name obtained from the shader
    * @param {Float32Array} aBuffer
    *                       the buffer to be bound
    */
   bindVertexBuffer: function TGLP_bindVertexBuffer(aAtribute, aBuffer)
@@ -944,19 +954,19 @@ TiltGL.Program.prototype = {
    *                 the sampler name to bind the texture to
    * @param {TiltGL.Texture} aTexture
    *                       the texture to be bound
    */
   bindTexture: function TGLP_bindTexture(aSampler, aTexture)
   {
     let gl = this._context;
 
-    gl.uniform1i(this._uniforms[aSampler], 0);
     gl.activeTexture(gl.TEXTURE0);
     gl.bindTexture(gl.TEXTURE_2D, aTexture._ref);
+    gl.uniform1i(this._uniforms[aSampler], 0);
   },
 
   /**
    * Function called when this object is destroyed.
    */
   finalize: function TGLP_finalize()
   {
     if (this._context) {
@@ -1172,17 +1182,17 @@ TiltGL.ProgramUtils = {
   /**
    * Represents the current active shader, identified by an id.
    */
   _activeProgram: -1,
 
   /**
    * Represents the current enabled attributes.
    */
-  _enabledAttributes: -1
+  _enabledAttributes: []
 };
 
 /**
  * This constructor creates a texture from an Image.
  *
  * @param {Object} aContext
  *                 a WebGL context
  * @param {Object} aProperties
@@ -1410,17 +1420,17 @@ TiltGL.TextureUtils = {
     // generate mipmap if necessary
     if (aProperties.mipmap) {
       gl.generateMipmap(gl.TEXTURE_2D);
     }
   },
 
   /**
    * This shim renders a content window to a canvas element, but clamps the
-   * maximum width and height of the canvas to half the WebGL MAX_TEXTURE_SIZE.
+   * maximum width and height of the canvas to the WebGL MAX_TEXTURE_SIZE.
    *
    * @param {Window} aContentWindow
    *                 the content window to get a texture from
    * @param {Number} aMaxImageSize
    *                 the maximum image size to be used
    *
    * @return {Image} the new content window image
    */
@@ -1610,10 +1620,10 @@ TiltGL.create3DContext = function TGL_cr
 };
 
 /**
  * Clears the cache and sets all the variables to default.
  */
 TiltGL.clearCache = function TGL_clearCache()
 {
   TiltGL.ProgramUtils._activeProgram = -1;
-  TiltGL.ProgramUtils._enabledAttributes = -1;
+  TiltGL.ProgramUtils._enabledAttributes = [];
 };
--- a/browser/devtools/tilt/TiltUtils.jsm
+++ b/browser/devtools/tilt/TiltUtils.jsm
@@ -513,18 +513,18 @@ TiltUtils.bindObjectFunc = function TU_b
  */
 TiltUtils.destroyObject = function TU_destroyObject(aScope)
 {
   if (!aScope) {
     return;
   }
 
   // objects in Tilt usually use a function to handle internal destruction
-  if ("function" === typeof aScope.finalize) {
-    aScope.finalize();
+  if ("function" === typeof aScope._finalize) {
+    aScope._finalize();
   }
   for (let i in aScope) {
     if (aScope.hasOwnProperty(i)) {
       delete aScope[i];
     }
   }
 };
 
--- a/browser/devtools/tilt/TiltVisualizer.jsm
+++ b/browser/devtools/tilt/TiltVisualizer.jsm
@@ -50,33 +50,40 @@ const INVISIBLE_ELEMENTS = {
   "link": true,
   "meta": true,
   "option": true,
   "script": true,
   "style": true,
   "title": true
 };
 
+// a node is represented in the visualization mesh as a rectangular stack
+// of 5 quads composed of 12 vertices; we draw these as triangles using an
+// index buffer of 12 unsigned int elements, obviously one for each vertex;
+// if a webpage has enough nodes to overflow the index buffer elements size,
+// weird things may happen; thus, when necessary, we'll split into groups
+const MAX_GROUP_NODES = Math.pow(2, Uint16Array.BYTES_PER_ELEMENT * 8) / 12 - 1;
+
 const STACK_THICKNESS = 15;
 const WIREFRAME_COLOR = [0, 0, 0, 0.25];
-const INTRO_TRANSITION_DURATION = 50;
-const OUTRO_TRANSITION_DURATION = 40;
+const INTRO_TRANSITION_DURATION = 1000;
+const OUTRO_TRANSITION_DURATION = 800;
 const INITIAL_Z_TRANSLATION = 400;
 const MOVE_INTO_VIEW_ACCURACY = 50;
 
 const MOUSE_CLICK_THRESHOLD = 10;
-const MOUSE_INTRO_DELAY = 10;
+const MOUSE_INTRO_DELAY = 200;
 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 ARCBALL_RESET_SPHERICAL_FACTOR = 0.1;
+const ARCBALL_RESET_LINEAR_FACTOR = 0.01;
 
 const TILT_CRAFTER = "resource:///modules/devtools/TiltWorkerCrafter.js";
 const TILT_PICKER = "resource:///modules/devtools/TiltWorkerPicker.js";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/TiltGL.jsm");
 Cu.import("resource:///modules/devtools/TiltMath.jsm");
 Cu.import("resource:///modules/devtools/TiltUtils.jsm");
@@ -87,17 +94,16 @@ let EXPORTED_SYMBOLS = ["TiltVisualizer"
 /**
  * Initializes the visualization presenter and controller.
  *
  * @param {Object} aProperties
  *                 an object containing the following properties:
  *        {Window} chromeWindow: a reference to the top level window
  *        {Window} contentWindow: the content window holding the visualized doc
  *       {Element} parentNode: the parent node to hold the visualization
- *      {Function} requestAnimationFrame: responsible with scheduling loops
  *        {Object} notifications: necessary notifications for Tilt
  *      {Function} onError: optional, function called if initialization failed
  *      {Function} onLoad: optional, function called if initialization worked
  */
 function TiltVisualizer(aProperties)
 {
   // make sure the properties parameter is a valid object
   aProperties = aProperties || {};
@@ -116,17 +122,16 @@ function TiltVisualizer(aProperties)
   });
 
   /**
    * Visualization logic and drawing loop.
    */
   this.presenter = new TiltVisualizer.Presenter(this.canvas,
     aProperties.chromeWindow,
     aProperties.contentWindow,
-    aProperties.requestAnimationFrame,
     aProperties.notifications,
     aProperties.onError || null,
     aProperties.onLoad || null);
 
   /**
    * Visualization mouse and keyboard controller.
    */
   this.controller = new TiltVisualizer.Controller(this.canvas, this.presenter);
@@ -179,28 +184,25 @@ TiltVisualizer.prototype = {
  * This object manages the visualization logic and drawing loop.
  *
  * @param {HTMLCanvasElement} aCanvas
  *                            the canvas element used for rendering
  * @param {Window} aChromeWindow
  *                 a reference to the top-level window
  * @param {Window} aContentWindow
  *                 the content window holding the document to be visualized
- * @param {Function} aRequestAnimationFrame
- *                   function responsible with scheduling loop frames
  * @param {Object} aNotifications
  *                 necessary notifications for Tilt
  * @param {Function} onError
  *                   function called if initialization failed
  * @param {Function} onLoad
  *                   function called if initialization worked
  */
 TiltVisualizer.Presenter = function TV_Presenter(
-  aCanvas, aChromeWindow, aContentWindow, aRequestAnimationFrame, aNotifications,
-  onError, onLoad)
+  aCanvas, aChromeWindow, aContentWindow, aNotifications, onError, onLoad)
 {
   /**
    * A canvas overlay used for drawing the visualization.
    */
   this.canvas = aCanvas;
 
   /**
    * Save a reference to the top-level window, to access InspectorUI or Tilt.
@@ -215,35 +217,36 @@ TiltVisualizer.Presenter = function TV_P
   /**
    * Shortcut for accessing notifications strings.
    */
   this.NOTIFICATIONS = aNotifications;
 
   /**
    * Create the renderer, containing useful functions for easy drawing.
    */
-  this.renderer = new TiltGL.Renderer(aCanvas, onError, onLoad);
+  this._renderer = new TiltGL.Renderer(aCanvas, onError, onLoad);
 
   /**
    * A custom shader used for drawing the visualization mesh.
    */
-  this.visualizationProgram = null;
+  this._visualizationProgram = null;
 
   /**
    * The combined mesh representing the document visualization.
    */
-  this.texture = null;
-  this.meshStacks = null;
-  this.meshWireframe = null;
-  this.traverseData = null;
+  this._texture = null;
+  this._meshData = null;
+  this._meshStacks = null;
+  this._meshWireframe = null;
+  this._traverseData = null;
 
   /**
    * A highlight quad drawn over a stacked dom node.
    */
-  this.highlight = {
+  this._highlight = {
     disabled: true,
     v0: vec3.create(),
     v1: vec3.create(),
     v2: vec3.create(),
     v3: vec3.create()
   };
 
   /**
@@ -263,396 +266,435 @@ TiltVisualizer.Presenter = function TV_P
   this._currentSelection = -1; // the selected node index
   this._initialSelection = false; // true if an initial selection was made
   this._initialMeshConfiguration = false; // true if the 3D mesh was configured
 
   /**
    * Variable specifying if the scene should be redrawn.
    * This should happen usually when the visualization is translated/rotated.
    */
-  this.redraw = true;
+  this._redraw = true;
+
+  /**
+   * Total time passed since the rendering started.
+   * If the rendering is paused, this property won't get updated.
+   */
+  this._time = 0;
 
   /**
-   * A frame counter, incremented each time the scene is redrawn.
+   * Frame delta time (the ammount of time passed for each frame).
+   * This is used to smoothly interpolate animation transfroms.
    */
-  this.frames = 0;
+  this._delta = 0;
+  this._prevFrameTime = 0;
+  this._currFrameTime = 0;
+
+
+  this._setup();
+  this._loop();
+};
+
+TiltVisualizer.Presenter.prototype = {
 
   /**
    * The initialization logic.
    */
-  let setup = function TVP_setup()
+  _setup: function TVP__setup()
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
     let inspector = this.chromeWindow.InspectorUI;
 
     // if the renderer was destroyed, don't continue setup
     if (!renderer || !renderer.context) {
       return;
     }
 
     // create the visualization shaders and program to draw the stacks mesh
-    this.visualizationProgram = new renderer.Program({
+    this._visualizationProgram = new renderer.Program({
       vs: TiltVisualizer.MeshShader.vs,
       fs: TiltVisualizer.MeshShader.fs,
       attributes: ["vertexPosition", "vertexTexCoord", "vertexColor"],
       uniforms: ["mvMatrix", "projMatrix", "sampler"]
     });
 
     // get the document zoom to properly scale the visualization
     if (inspector.highlighter) {
       this.transforms.zoom = inspector.highlighter.zoom;
     }
 
-    this.setupTexture();
-    this.setupMeshData();
-    this.setupEventListeners();
+    // bind the owner object to the necessary functions
+    TiltUtils.bindObjectFunc(this, "^_on");
+    TiltUtils.bindObjectFunc(this, "_loop");
+
+    this._setupTexture();
+    this._setupMeshData();
+    this._setupEventListeners();
     this.canvas.focus();
-  }.bind(this);
+  },
 
   /**
    * The animation logic.
    */
-  let loop = function TVP_loop()
+  _loop: function TVP__loop()
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
 
     // if the renderer was destroyed, don't continue rendering
     if (!renderer || !renderer.context) {
       return;
     }
 
     // prepare for the next frame of the animation loop
-    aRequestAnimationFrame(loop);
+    this.chromeWindow.mozRequestAnimationFrame(this._loop);
 
     // only redraw if we really have to
-    if (this.redraw) {
-      this.redraw = false;
-      this.drawVisualization();
+    if (this._redraw) {
+      this._redraw = false;
+      this._drawVisualization();
+    }
+
+    // update the current presenter transfroms from the controller
+    if ("function" === typeof this._controllerUpdate) {
+      this._controllerUpdate(this._time, this._delta);
     }
 
-    // call the attached ondraw function and handle all keyframe notifications
-    if ("function" === typeof this.ondraw) {
-      this.ondraw(this.frames);
-    }
+    this._handleFrameDelta();
+    this._handleKeyframeNotifications();
+  },
 
-    this.handleKeyframeNotifications();
-  }.bind(this);
-
-  setup();
-  loop();
-};
-
-TiltVisualizer.Presenter.prototype = {
+  /**
+   * Calculates the current frame delta time.
+   */
+  _handleFrameDelta: function TVP__handleFrameDelta()
+  {
+    this._prevFrameTime = this._currFrameTime;
+    this._currFrameTime = this.chromeWindow.mozAnimationStartTime;
+    this._delta = this._currFrameTime - this._prevFrameTime;
+  },
 
   /**
    * Draws the visualization mesh and highlight quad.
    */
-  drawVisualization: function TVP_drawVisualization()
+  _drawVisualization: function TVP__drawVisualization()
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
     let transforms = this.transforms;
     let w = renderer.width;
     let h = renderer.height;
+    let ih = renderer.initialHeight;
 
     // if the mesh wasn't created yet, don't continue rendering
-    if (!this.meshStacks || !this.meshWireframe) {
+    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 ortho = mat4.ortho(0, w, h, 0, -1000, 1000);
 
-    if (!this.isExecutingDestruction) {
-      let f = this.frames / INTRO_TRANSITION_DURATION;
+    if (!this._isExecutingDestruction) {
+      let f = this._time / INTRO_TRANSITION_DURATION;
       renderer.lerp(renderer.projMatrix, ortho, f, 8);
     } else {
-      let f = this.frames / OUTRO_TRANSITION_DURATION;
+      let f = this._time / 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);
+    renderer.translate(w * 0.5, ih * 0.5, -INITIAL_Z_TRANSLATION);
 
     // calculate the camera matrix using the rotation and translation
     renderer.translate(transforms.translation[0], 0,
                        transforms.translation[2]);
 
     renderer.transform(quat4.toMat4(transforms.rotation));
 
     // offset the visualization mesh to center
     renderer.translate(transforms.offset[0],
                        transforms.offset[1] + transforms.translation[1], 0);
 
     renderer.scale(transforms.zoom, transforms.zoom);
 
     // draw the visualization mesh
     renderer.strokeWeight(2);
     renderer.depthTest(true);
-    this.drawMeshStacks();
-    this.drawMeshWireframe();
-    this.drawHighlight();
+    this._drawMeshStacks();
+    this._drawMeshWireframe();
+    this._drawHighlight();
 
     // make sure the initial transition is drawn until finished
-    if (this.frames < INTRO_TRANSITION_DURATION ||
-        this.frames < OUTRO_TRANSITION_DURATION) {
-      this.redraw = true;
+    if (this._time < INTRO_TRANSITION_DURATION ||
+        this._time < OUTRO_TRANSITION_DURATION) {
+      this._redraw = true;
     }
-    this.frames++;
+    this._time += this._delta;
   },
 
   /**
    * Draws the meshStacks object.
    */
-  drawMeshStacks: function TVP_drawMeshStacks()
+  _drawMeshStacks: function TVP__drawMeshStacks()
   {
-    let renderer = this.renderer;
-    let mesh = this.meshStacks;
+    let renderer = this._renderer;
+    let mesh = this._meshStacks;
 
-    let visualizationProgram = this.visualizationProgram;
-    let texture = this.texture;
+    let visualizationProgram = this._visualizationProgram;
+    let texture = this._texture;
     let mvMatrix = renderer.mvMatrix;
     let projMatrix = renderer.projMatrix;
 
     // use the necessary shader
     visualizationProgram.use();
 
-    // bind the attributes and uniforms as necessary
-    visualizationProgram.bindVertexBuffer("vertexPosition", mesh.vertices);
-    visualizationProgram.bindVertexBuffer("vertexTexCoord", mesh.texCoord);
-    visualizationProgram.bindVertexBuffer("vertexColor", mesh.color);
+    for (let i = 0, len = mesh.length; i < len; i++) {
+      let group = mesh[i];
+
+      // bind the attributes and uniforms as necessary
+      visualizationProgram.bindVertexBuffer("vertexPosition", group.vertices);
+      visualizationProgram.bindVertexBuffer("vertexTexCoord", group.texCoord);
+      visualizationProgram.bindVertexBuffer("vertexColor", group.color);
 
-    visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix);
-    visualizationProgram.bindUniformMatrix("projMatrix", projMatrix);
-    visualizationProgram.bindTexture("sampler", texture);
+      visualizationProgram.bindUniformMatrix("mvMatrix", mvMatrix);
+      visualizationProgram.bindUniformMatrix("projMatrix", projMatrix);
+      visualizationProgram.bindTexture("sampler", texture);
 
-    // draw the vertices as TRIANGLES indexed elements
-    renderer.drawIndexedVertices(renderer.context.TRIANGLES, mesh.indices);
+      // draw the vertices as TRIANGLES indexed elements
+      renderer.drawIndexedVertices(renderer.context.TRIANGLES, group.indices);
+    }
 
     // save the current model view and projection matrices
     mesh.mvMatrix = mat4.create(mvMatrix);
     mesh.projMatrix = mat4.create(projMatrix);
   },
 
   /**
    * Draws the meshWireframe object.
    */
-  drawMeshWireframe: function TVP_drawMeshWireframe()
+  _drawMeshWireframe: function TVP__drawMeshWireframe()
   {
-    let renderer = this.renderer;
-    let mesh = this.meshWireframe;
+    let renderer = this._renderer;
+    let mesh = this._meshWireframe;
 
-    // use the necessary shader
-    renderer.useColorShader(mesh.vertices, WIREFRAME_COLOR);
+    for (let i = 0, len = mesh.length; i < len; i++) {
+      let group = mesh[i];
 
-    // draw the vertices as LINES indexed elements
-    renderer.drawIndexedVertices(renderer.context.LINES, mesh.indices);
+      // use the necessary shader
+      renderer.useColorShader(group.vertices, WIREFRAME_COLOR);
+
+      // draw the vertices as LINES indexed elements
+      renderer.drawIndexedVertices(renderer.context.LINES, group.indices);
+    }
   },
 
   /**
    * Draws a highlighted quad around a currently selected node.
    */
-  drawHighlight: function TVP_drawHighlight()
+  _drawHighlight: function TVP__drawHighlight()
   {
     // check if there's anything to highlight (i.e any node is selected)
-    if (!this.highlight.disabled) {
+    if (!this._highlight.disabled) {
 
       // set the corresponding state to draw the highlight quad
-      let renderer = this.renderer;
-      let highlight = this.highlight;
+      let renderer = this._renderer;
+      let highlight = this._highlight;
 
       renderer.depthTest(false);
       renderer.fill(highlight.fill, 0.5);
       renderer.stroke(highlight.stroke);
       renderer.strokeWeight(highlight.strokeWeight);
       renderer.quad(highlight.v0, highlight.v1, highlight.v2, highlight.v3);
     }
   },
 
   /**
    * Creates or refreshes the texture applied to the visualization mesh.
    */
-  setupTexture: function TVP_setupTexture()
+  _setupTexture: function TVP__setupTexture()
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
 
     // destroy any previously created texture
-    TiltUtils.destroyObject(this.texture);
+    TiltUtils.destroyObject(this._texture); this._texture = null;
 
     // if the renderer was destroyed, don't continue setup
     if (!renderer || !renderer.context) {
       return;
     }
 
     // get the maximum texture size
-    this.maxTextureSize =
+    this._maxTextureSize =
       renderer.context.getParameter(renderer.context.MAX_TEXTURE_SIZE);
 
     // use a simple shim to get the image representation of the document
     // this will be removed once the MOZ_window_region_texture bug #653656
     // is finished; currently just converting the document image to a texture
     // applied to the mesh
-    this.texture = new renderer.Texture({
+    this._texture = new renderer.Texture({
       source: TiltGL.TextureUtils.createContentImage(this.contentWindow,
-                                                     this.maxTextureSize),
+                                                     this._maxTextureSize),
       format: "RGB"
     });
 
-    if ("function" === typeof this.onSetupTexture) {
-      this.onSetupTexture();
-      this.onSetupTexture = null;
+    if ("function" === typeof this._onSetupTexture) {
+      this._onSetupTexture();
+      this._onSetupTexture = null;
     }
   },
 
   /**
    * Create the combined mesh representing the document visualization by
    * traversing the document & adding a stack for each node that is drawable.
    *
-   * @param {Object} aData
+   * @param {Object} aMeshData
    *                 object containing the necessary mesh verts, texcoord etc.
    */
-  setupMesh: function TVP_setupMesh(aData)
+  _setupMesh: function TVP__setupMesh(aMeshData)
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
 
     // destroy any previously created mesh
-    TiltUtils.destroyObject(this.meshStacks);
-    TiltUtils.destroyObject(this.meshWireframe);
+    TiltUtils.destroyObject(this._meshStacks); this._meshStacks = [];
+    TiltUtils.destroyObject(this._meshWireframe); this._meshWireframe = [];
 
     // if the renderer was destroyed, don't continue setup
     if (!renderer || !renderer.context) {
       return;
     }
 
     // save the mesh data for future use
-    this.meshData = aData;
+    this._meshData = aMeshData;
+
+    // create a sub-mesh for each group in the mesh data
+    for (let i = 0, len = aMeshData.groups.length; i < len; i++) {
+      let group = aMeshData.groups[i];
 
-    // create the visualization mesh using the vertices, texture coordinates
-    // and indices computed when traversing the document object model
-    this.meshStacks = {
-      vertices: new renderer.VertexBuffer(aData.vertices, 3),
-      texCoord: new renderer.VertexBuffer(aData.texCoord, 2),
-      color: new renderer.VertexBuffer(aData.color, 3),
-      indices: new renderer.IndexBuffer(aData.stacksIndices)
-    };
+      // create the visualization mesh using the vertices, texture coordinates
+      // and indices computed when traversing the document object model
+      this._meshStacks.push({
+        vertices: new renderer.VertexBuffer(group.vertices, 3),
+        texCoord: new renderer.VertexBuffer(group.texCoord, 2),
+        color: new renderer.VertexBuffer(group.color, 3),
+        indices: new renderer.IndexBuffer(group.stacksIndices)
+      });
 
-    // additionally, create a wireframe representation to make the
-    // visualization a bit more pretty
-    this.meshWireframe = {
-      vertices: this.meshStacks.vertices,
-      indices: new renderer.IndexBuffer(aData.wireframeIndices)
-    };
+      // additionally, create a wireframe representation to make the
+      // visualization a bit more pretty
+      this._meshWireframe.push({
+        vertices: this._meshStacks[i].vertices,
+        indices: new renderer.IndexBuffer(group.wireframeIndices)
+      });
+    }
 
     // if there's no initial selection made, highlight the required node
     if (!this._initialSelection) {
       this._initialSelection = true;
       this.highlightNode(this.chromeWindow.InspectorUI.selection);
+
+      if (this._currentSelection === 0) { // if the "html" node is selected
+        this._highlight.disabled = true;
+      }
     }
 
+    // configure the required mesh transformations and background only once
     if (!this._initialMeshConfiguration) {
       this._initialMeshConfiguration = true;
 
-      let width = renderer.width;
-      let height = renderer.height;
-
       // set the necessary mesh offsets
-      this.transforms.offset[0] = -width * 0.5;
-      this.transforms.offset[1] = -height * 0.5;
+      this.transforms.offset[0] = -renderer.width * 0.5;
+      this.transforms.offset[1] = -renderer.height * 0.5;
 
       // make sure the canvas is opaque now that the initialization is finished
       this.canvas.style.background = TiltVisualizerStyle.canvas.background;
 
-      this.drawVisualization();
-      this.redraw = true;
+      this._drawVisualization();
+      this._redraw = true;
     }
 
-    if ("function" === typeof this.onSetupMesh) {
-      this.onSetupMesh();
-      this.onSetupMesh = null;
+    if ("function" === typeof this._onSetupMesh) {
+      this._onSetupMesh();
+      this._onSetupMesh = null;
     }
   },
 
   /**
-   * Computes the mesh vertices, texture coordinates etc.
+   * Computes the mesh vertices, texture coordinates etc. by groups of nodes.
    */
-  setupMeshData: function TVP_setupMeshData()
+  _setupMeshData: function TVP__setupMeshData()
   {
-    let renderer = this.renderer;
+    let renderer = this._renderer;
 
     // if the renderer was destroyed, don't continue setup
     if (!renderer || !renderer.context) {
       return;
     }
 
     // traverse the document and get the depths, coordinates and local names
-    this.traverseData = TiltUtils.DOM.traverse(this.contentWindow, {
+    this._traverseData = TiltUtils.DOM.traverse(this.contentWindow, {
       invisibleElements: INVISIBLE_ELEMENTS,
       minSize: ELEMENT_MIN_SIZE,
-      maxX: this.texture.width,
-      maxY: this.texture.height
+      maxX: this._texture.width,
+      maxY: this._texture.height
     });
 
     let worker = new ChromeWorker(TILT_CRAFTER);
 
     worker.addEventListener("message", function TVP_onMessage(event) {
-      this.setupMesh(event.data);
+      this._setupMesh(event.data);
     }.bind(this), false);
 
     // calculate necessary information regarding vertices, texture coordinates
     // etc. in a separate thread, as this process may take a while
     worker.postMessage({
+      maxGroupNodes: MAX_GROUP_NODES,
       thickness: STACK_THICKNESS,
       style: TiltVisualizerStyle.nodes,
-      texWidth: this.texture.width,
-      texHeight: this.texture.height,
-      nodesInfo: this.traverseData.info
+      texWidth: this._texture.width,
+      texHeight: this._texture.height,
+      nodesInfo: this._traverseData.info
     });
   },
 
   /**
    * Sets up event listeners necessary for the presenter.
    */
-  setupEventListeners: function TVP_setupEventListeners()
+  _setupEventListeners: function TVP__setupEventListeners()
   {
-    // bind the owner object to the necessary functions
-    TiltUtils.bindObjectFunc(this, "^on");
-
-    this.contentWindow.addEventListener("resize", this.onResize, false);
+    this.contentWindow.addEventListener("resize", this._onResize, false);
   },
 
   /**
    * Called when the content window of the current browser is resized.
    */
-  onResize: function TVP_onResize(e)
+  _onResize: function TVP_onResize(e)
   {
     let zoom = this.chromeWindow.InspectorUI.highlighter.zoom;
     let width = e.target.innerWidth * zoom;
     let height = e.target.innerHeight * zoom;
 
     // handle aspect ratio changes to update the projection matrix
-    this.renderer.width = width;
-    this.renderer.height = height;
+    this._renderer.width = width;
+    this._renderer.height = height;
 
-    this.redraw = true;
+    this._redraw = true;
   },
 
   /**
    * Highlights a specific node.
    *
    * @param {Element} aNode
    *                  the html node to be highlighted
    * @param {String} aFlags
    *                 flags specifying highlighting options
    */
   highlightNode: function TVP_highlightNode(aNode, aFlags)
   {
-    this.highlightNodeFor(this.traverseData.nodes.indexOf(aNode), aFlags);
+    this.highlightNodeFor(this._traverseData.nodes.indexOf(aNode), aFlags);
   },
 
   /**
    * Picks a stacked dom node at the x and y screen coordinates and highlights
    * the selected node in the mesh.
    *
    * @param {Number} x
    *                 the current horizontal coordinate of the mouse
@@ -700,41 +742,41 @@ TiltVisualizer.Presenter.prototype = {
     });
   },
 
   /**
    * Sets the corresponding highlight coordinates and color based on the
    * information supplied.
    *
    * @param {Number} aNodeIndex
-   *                 the index of the node in the this.traverseData array
+   *                 the index of the node in the this._traverseData array
    * @param {String} aFlags
    *                 flags specifying highlighting options
    */
   highlightNodeFor: function TVP_highlightNodeFor(aNodeIndex, aFlags)
   {
-    this.redraw = true;
+    this._redraw = true;
 
     // if the node was already selected, don't do anything
     if (this._currentSelection === aNodeIndex) {
       return;
     }
 
     // if an invalid or nonexisted node is specified, disable the highlight
     if (aNodeIndex < 0) {
       this._currentSelection = -1;
-      this.highlight.disabled = true;
+      this._highlight.disabled = true;
 
       Services.obs.notifyObservers(null, this.NOTIFICATIONS.UNHIGHLIGHTING, null);
       return;
     }
 
-    let highlight = this.highlight;
-    let info = this.traverseData.info[aNodeIndex];
-    let node = this.traverseData.nodes[aNodeIndex];
+    let highlight = this._highlight;
+    let info = this._traverseData.info[aNodeIndex];
+    let node = this._traverseData.nodes[aNodeIndex];
     let style = TiltVisualizerStyle.nodes;
 
     highlight.disabled = false;
     highlight.fill = style[info.name] || style.highlight.defaultFill;
     highlight.stroke = style.highlight.defaultStroke;
     highlight.strokeWeight = style.highlight.defaultStrokeWeight;
 
     let x = info.coord.left;
@@ -756,50 +798,53 @@ TiltVisualizer.Presenter.prototype = {
 
     // if something is highlighted, make sure it's inside the current viewport;
     // the point which should be moved into view is considered the center [x, y]
     // position along the top edge of the currently selected node
 
     if (aFlags && aFlags.indexOf("moveIntoView") !== -1)
     {
       this.controller.arcball.moveIntoView(vec3.lerp(
-        vec3.scale(this.highlight.v0, this.transforms.zoom, []),
-        vec3.scale(this.highlight.v1, this.transforms.zoom, []), 0.5));
+        vec3.scale(this._highlight.v0, this.transforms.zoom, []),
+        vec3.scale(this._highlight.v1, this.transforms.zoom, []), 0.5));
     }
 
     Services.obs.notifyObservers(null, this.NOTIFICATIONS.HIGHLIGHTING, null);
   },
 
   /**
    * Deletes a node from the visualization mesh.
    *
    * @param {Number} aNodeIndex
-   *                 the index of the node in the this.traverseData array;
+   *                 the index of the node in the this._traverseData array;
    *                 if not specified, it will default to the current selection
    */
   deleteNode: function TVP_deleteNode(aNodeIndex)
   {
     // we probably don't want to delete the html or body node.. just sayin'
     if ((aNodeIndex = aNodeIndex || this._currentSelection) < 1) {
       return;
     }
 
-    let renderer = this.renderer;
-    let meshData = this.meshData;
+    let renderer = this._renderer;
 
-    for (let i = 0, k = 36 * aNodeIndex; i < 36; i++) {
-      meshData.vertices[i + k] = 0;
+    let groupIndex = parseInt(aNodeIndex / MAX_GROUP_NODES);
+    let nodeIndex = parseInt((aNodeIndex + (groupIndex ? 1 : 0)) % MAX_GROUP_NODES);
+    let group = this._meshStacks[groupIndex];
+    let vertices = group.vertices.components;
+
+    for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
+      vertices[i + k] = 0;
     }
 
-    this.meshStacks.vertices = new renderer.VertexBuffer(meshData.vertices, 3);
-    this.highlight.disabled = true;
-    this.redraw = true;
+    group.vertices = new renderer.VertexBuffer(vertices, 3);
+    this._highlight.disabled = true;
+    this._redraw = true;
 
-    Services.obs.notifyObservers(null,
-      this.NOTIFICATIONS.NODE_REMOVED, null);
+    Services.obs.notifyObservers(null, this.NOTIFICATIONS.NODE_REMOVED, null);
   },
 
   /**
    * 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
@@ -811,17 +856,17 @@ TiltVisualizer.Presenter.prototype = {
    *      {Function} onfail: function to be called if no intersections
    */
   pickNode: function TVP_pickNode(x, y, aProperties)
   {
     // make sure the properties parameter is a valid object
     aProperties = aProperties || {};
 
     // if the mesh wasn't created yet, don't continue picking
-    if (!this.meshStacks || !this.meshWireframe) {
+    if (!this._meshStacks || !this._meshWireframe) {
       return;
     }
 
     let worker = new ChromeWorker(TILT_PICKER);
 
     worker.addEventListener("message", function TVP_onMessage(event) {
       if (event.data) {
         if ("function" === typeof aProperties.onpick) {
@@ -830,33 +875,32 @@ TiltVisualizer.Presenter.prototype = {
       } else {
         if ("function" === typeof aProperties.onfail) {
           aProperties.onfail();
         }
       }
     }, false);
 
     let zoom = this.chromeWindow.InspectorUI.highlighter.zoom;
-    let width = this.renderer.width * zoom;
-    let height = this.renderer.height * zoom;
-    let mesh = this.meshStacks;
+    let width = this._renderer.width * zoom;
+    let height = this._renderer.height * zoom;
     x *= zoom;
     y *= zoom;
 
     // create a ray following the mouse direction from the near clipping plane
     // to the far clipping plane, to check for intersections with the mesh,
     // and do all the heavy lifting in a separate thread
     worker.postMessage({
       thickness: STACK_THICKNESS,
-      vertices: mesh.vertices.components,
+      vertices: this._meshData.allVertices,
 
       // create the ray destined for 3D picking
       ray: vec3.createRay([x, y, 0], [x, y, 1], [0, 0, width, height],
-        mesh.mvMatrix,
-        mesh.projMatrix)
+        this._meshStacks.mvMatrix,
+        this._meshStacks.projMatrix)
     });
   },
 
   /**
    * Delegate translation method, used by the controller.
    *
    * @param {Array} aTranslation
    *                the new translation on the [x, y, z] axis
@@ -869,17 +913,17 @@ TiltVisualizer.Presenter.prototype = {
     let transforms = this.transforms;
 
     // only update the translation if it's not already set
     if (transforms.translation[0] !== x ||
         transforms.translation[1] !== y ||
         transforms.translation[2] !== z) {
 
       vec3.set(aTranslation, transforms.translation);
-      this.redraw = true;
+      this._redraw = true;
     }
   },
 
   /**
    * Delegate rotation method, used by the controller.
    *
    * @param {Array} aQuaternion
    *                the rotation quaternion, as [x, y, z, w]
@@ -894,113 +938,118 @@ TiltVisualizer.Presenter.prototype = {
 
     // only update the rotation if it's not already set
     if (transforms.rotation[0] !== x ||
         transforms.rotation[1] !== y ||
         transforms.rotation[2] !== z ||
         transforms.rotation[3] !== w) {
 
       quat4.set(aQuaternion, transforms.rotation);
-      this.redraw = true;
+      this._redraw = true;
     }
   },
 
   /**
    * Handles notifications at specific frame counts.
    */
-  handleKeyframeNotifications: function TV_handleKeyframeNotifications()
+  _handleKeyframeNotifications: function TV__handleKeyframeNotifications()
   {
-    if (!TiltVisualizer.Prefs.introTransition && !this.isExecutingDestruction) {
-      this.frames = INTRO_TRANSITION_DURATION;
+    if (!TiltVisualizer.Prefs.introTransition && !this._isExecutingDestruction) {
+      this._time = INTRO_TRANSITION_DURATION;
     }
-    if (!TiltVisualizer.Prefs.outroTransition && this.isExecutingDestruction) {
-      this.frames = OUTRO_TRANSITION_DURATION;
+    if (!TiltVisualizer.Prefs.outroTransition && this._isExecutingDestruction) {
+      this._time = OUTRO_TRANSITION_DURATION;
     }
 
-    if (this.frames === INTRO_TRANSITION_DURATION &&
-       !this.isExecutingDestruction) {
+    if (this._time >= INTRO_TRANSITION_DURATION &&
+       !this._isInitializationFinished &&
+       !this._isExecutingDestruction) {
 
+      this._isInitializationFinished = true;
       Services.obs.notifyObservers(null, this.NOTIFICATIONS.INITIALIZED, null);
 
-      if ("function" === typeof this.onInitializationFinished) {
-        this.onInitializationFinished();
+      if ("function" === typeof this._onInitializationFinished) {
+        this._onInitializationFinished();
       }
     }
 
-    if (this.frames === OUTRO_TRANSITION_DURATION &&
-        this.isExecutingDestruction) {
+    if (this._time >= OUTRO_TRANSITION_DURATION &&
+       !this._isDestructionFinished &&
+        this._isExecutingDestruction) {
 
+      this._isDestructionFinished = true;
       Services.obs.notifyObservers(null, this.NOTIFICATIONS.BEFORE_DESTROYED, null);
 
-      if ("function" === typeof this.onDestructionFinished) {
-        this.onDestructionFinished();
+      if ("function" === typeof this._onDestructionFinished) {
+        this._onDestructionFinished();
       }
     }
   },
 
   /**
    * Starts executing the destruction sequence and issues 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._isExecutingDestruction) {
+      this._isExecutingDestruction = true;
+      this._onDestructionFinished = aCallback;
 
       // if we execute the destruction after the initialization finishes,
       // proceed normally; otherwise, skip everything and immediately issue
       // the callback
 
-      if (this.frames > OUTRO_TRANSITION_DURATION) {
-        this.frames = 0;
-        this.redraw = true;
+      if (this._time > OUTRO_TRANSITION_DURATION) {
+        this._time = 0;
+        this._redraw = true;
       } else {
         aCallback();
       }
     }
   },
 
   /**
    * Checks if this object was initialized properly.
    *
    * @return {Boolean} true if the object was initialized properly
    */
   isInitialized: function TVP_isInitialized()
   {
-    return this.renderer && this.renderer.context;
+    return this._renderer && this._renderer.context;
   },
 
   /**
    * Function called when this object is destroyed.
    */
-  finalize: function TVP_finalize()
+  _finalize: function TVP__finalize()
   {
-    TiltUtils.destroyObject(this.visualizationProgram);
-    TiltUtils.destroyObject(this.texture);
+    TiltUtils.destroyObject(this._visualizationProgram);
+    TiltUtils.destroyObject(this._texture);
 
-    if (this.meshStacks) {
-      TiltUtils.destroyObject(this.meshStacks.vertices);
-      TiltUtils.destroyObject(this.meshStacks.texCoord);
-      TiltUtils.destroyObject(this.meshStacks.color);
-      TiltUtils.destroyObject(this.meshStacks.indices);
+    if (this._meshStacks) {
+      this._meshStacks.forEach(function(group) {
+        TiltUtils.destroyObject(group.vertices);
+        TiltUtils.destroyObject(group.texCoord);
+        TiltUtils.destroyObject(group.color);
+        TiltUtils.destroyObject(group.indices);
+      });
+    }
+    if (this._meshWireframe) {
+      this._meshWireframe.forEach(function(group) {
+        TiltUtils.destroyObject(group.indices);
+      });
     }
 
-    if (this.meshWireframe) {
-      TiltUtils.destroyObject(this.meshWireframe.indices);
-    }
+    TiltUtils.destroyObject(this._renderer);
 
-    TiltUtils.destroyObject(this.highlight);
-    TiltUtils.destroyObject(this.transforms);
-    TiltUtils.destroyObject(this.renderer);
-
-    this.contentWindow.removeEventListener("resize", this.onResize, false);
+    this.contentWindow.removeEventListener("resize", this._onResize, false);
   }
 };
 
 /**
  * A mouse and keyboard controller implementation.
  *
  * @param {HTMLCanvasElement} aCanvas
  *                            the visualization canvas element
@@ -1018,141 +1067,143 @@ TiltVisualizer.Controller = function TV_
    * Save a reference to the presenter to modify its model-view transforms.
    */
   this.presenter = aPresenter;
   this.presenter.controller = this;
 
   /**
    * The initial controller dimensions and offset, in pixels.
    */
-  this.zoom = aPresenter.transforms.zoom;
-  this.left = (aPresenter.contentWindow.pageXOffset || 0) * this.zoom;
-  this.top = (aPresenter.contentWindow.pageYOffset || 0) * this.zoom;
-  this.width = aCanvas.width;
-  this.height = aCanvas.height;
+  this._zoom = aPresenter.transforms.zoom;
+  this._left = (aPresenter.contentWindow.pageXOffset || 0) * this._zoom;
+  this._top = (aPresenter.contentWindow.pageYOffset || 0) * this._zoom;
+  this._width = aCanvas.width;
+  this._height = aCanvas.height;
 
   /**
    * Arcball used to control the visualization using the mouse.
    */
   this.arcball = new TiltVisualizer.Arcball(
-    this.presenter.chromeWindow, this.width, this.height, 0,
+    this.presenter.chromeWindow, this._width, this._height, 0,
     [
-      this.width + this.left < aPresenter.maxTextureSize ? -this.left : 0,
-      this.height + this.top < aPresenter.maxTextureSize ? -this.top : 0
+      this._width + this._left < aPresenter._maxTextureSize ? -this._left : 0,
+      this._height + this._top < aPresenter._maxTextureSize ? -this._top : 0
     ]);
 
   /**
    * Object containing the rotation quaternion and the translation amount.
    */
-  this.coordinates = null;
+  this._coordinates = null;
 
   // bind the owner object to the necessary functions
-  TiltUtils.bindObjectFunc(this, "update");
-  TiltUtils.bindObjectFunc(this, "^on");
+  TiltUtils.bindObjectFunc(this, "_update");
+  TiltUtils.bindObjectFunc(this, "^_on");
 
   // add the necessary event listeners
   this.addEventListeners();
 
   // attach this controller's update function to the presenter ondraw event
-  aPresenter.ondraw = this.update;
+  this.presenter._controllerUpdate = this._update;
 };
 
 TiltVisualizer.Controller.prototype = {
 
   /**
    * Adds 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("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("keypress", this.onKeyPress, true);
-    canvas.addEventListener("blur", this.onBlur, false);
+    canvas.addEventListener("mousedown", this._onMouseDown, false);
+    canvas.addEventListener("mouseup", this._onMouseUp, 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("keypress", this._onKeyPress, true);
+    canvas.addEventListener("blur", this._onBlur, false);
 
     // handle resize events to change the arcball dimensions
-    presenter.contentWindow.addEventListener("resize", this.onResize, false);
+    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("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("keypress", this.onKeyPress, true);
-    canvas.removeEventListener("blur", this.onBlur, false);
+    canvas.removeEventListener("mousedown", this._onMouseDown, false);
+    canvas.removeEventListener("mouseup", this._onMouseUp, 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("keypress", this._onKeyPress, true);
+    canvas.removeEventListener("blur", this._onBlur, false);
 
-    presenter.contentWindow.removeEventListener("resize", this.onResize,false);
+    presenter.contentWindow.removeEventListener("resize", this._onResize, false);
   },
 
   /**
    * Function called each frame, updating the visualization camera transforms.
    *
-   * @param {Number} aFrames
-   *                 the current animation frame count
+   * @param {Number} aTime
+   *                 total time passed since rendering started
+   * @param {Number} aDelta
+   *                 the current animation frame delta
    */
-  update: function TVC_update(aFrames)
+  _update: function TVC__update(aTime, aDelta)
   {
-    this.frames = aFrames;
-    this.coordinates = this.arcball.update();
+    this._time = aTime;
+    this._coordinates = this.arcball.update(aDelta);
 
-    this.presenter.setRotation(this.coordinates.rotation);
-    this.presenter.setTranslation(this.coordinates.translation);
+    this.presenter.setRotation(this._coordinates.rotation);
+    this.presenter.setTranslation(this._coordinates.translation);
   },
 
   /**
    * Called once after every time a mouse button is pressed.
    */
-  onMouseDown: function TVC_onMouseDown(e)
+  _onMouseDown: function TVC__onMouseDown(e)
   {
     e.target.focus();
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this._time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let button = e.which;
     this._downX = e.clientX - e.target.offsetLeft;
     this._downY = e.clientY - e.target.offsetTop;
 
     this.arcball.mouseDown(this._downX, this._downY, button);
   },
 
   /**
    * Called every time a mouse button is released.
    */
-  onMouseUp: function TVC_onMouseUp(e)
+  _onMouseUp: function TVC__onMouseUp(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this._time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let button = e.which;
     let upX = e.clientX - e.target.offsetLeft;
     let upY = e.clientY - e.target.offsetTop;
 
@@ -1165,123 +1216,131 @@ TiltVisualizer.Controller.prototype = {
     }
 
     this.arcball.mouseUp(upX, upY, button);
   },
 
   /**
    * Called every time the mouse moves.
    */
-  onMouseMove: function TVC_onMouseMove(e)
+  _onMouseMove: function TVC__onMouseMove(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
-    if (this.frames < MOUSE_INTRO_DELAY) {
+    if (this._time < MOUSE_INTRO_DELAY) {
       return;
     }
 
     // calculate x and y coordinates using using the client and target offset
     let moveX = e.clientX - e.target.offsetLeft;
     let moveY = e.clientY - e.target.offsetTop;
 
     this.arcball.mouseMove(moveX, moveY);
   },
 
   /**
    * Called when the mouse leaves the visualization bounds.
    */
-  onMouseOver: function TVC_onMouseOver(e)
+  _onMouseOver: function TVC__onMouseOver(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
     this.arcball.mouseOver();
   },
 
   /**
    * Called when the mouse leaves the visualization bounds.
    */
-  onMouseOut: function TVC_onMouseOut(e)
+  _onMouseOut: function TVC__onMouseOut(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
     this.arcball.mouseOut();
   },
 
   /**
    * Called when the mouse wheel is used.
    */
-  onMozScroll: function TVC_onMozScroll(e)
+  _onMozScroll: function TVC__onMozScroll(e)
   {
     e.preventDefault();
     e.stopPropagation();
 
     this.arcball.zoom(e.detail);
   },
 
   /**
    * Called when a key is pressed.
    */
-  onKeyDown: function TVC_onKeyDown(e)
+  _onKeyDown: function TVC__onKeyDown(e)
   {
     let code = e.keyCode || e.which;
 
     if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
       e.preventDefault();
       e.stopPropagation();
       this.arcball.keyDown(code);
     } else {
       this.arcball.cancelKeyEvents();
     }
   },
 
   /**
    * Called when a key is released.
    */
-  onKeyUp: function TVC_onKeyUp(e)
+  _onKeyUp: function TVC__onKeyUp(e)
   {
     let code = e.keyCode || e.which;
 
     if (code === e.DOM_VK_X) {
       this.presenter.deleteNode();
     }
+    if (code === e.DOM_VK_F) {
+      let highlight = this.presenter._highlight;
+      let zoom = this.presenter.transforms.zoom;
+
+      this.arcball.moveIntoView(vec3.lerp(
+        vec3.scale(highlight.v0, zoom, []),
+        vec3.scale(highlight.v1, zoom, []), 0.5));
+    }
     if (!e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey) {
       e.preventDefault();
       e.stopPropagation();
       this.arcball.keyUp(code);
     }
   },
 
   /**
    * Called when a key is pressed.
    */
-  onKeyPress: function TVC_onKeyPress(e)
+  _onKeyPress: function TVC__onKeyPress(e)
   {
     let tilt = this.presenter.chromeWindow.Tilt;
 
     if (e.keyCode === e.DOM_VK_ESCAPE) {
       e.preventDefault();
       e.stopPropagation();
       tilt.destroy(tilt.currentWindowId, true);
     }
   },
 
   /**
    * Called when the canvas looses focus.
    */
-  onBlur: function TVC_onBlur(e) {
+  _onBlur: function TVC__onBlur(e) {
     this.arcball.cancelKeyEvents();
   },
 
   /**
    * Called when the content window of the current browser is resized.
    */
-  onResize: function TVC_onResize(e)
+  _onResize: function TVC__onResize(e)
   {
     let zoom = this.presenter.chromeWindow.InspectorUI.highlighter.zoom;
     let width = e.target.innerWidth * zoom;
     let height = e.target.innerHeight * zoom;
 
     this.arcball.resize(width, height);
   },
 
@@ -1293,23 +1352,24 @@ TiltVisualizer.Controller.prototype = {
   isInitialized: function TVC_isInitialized()
   {
     return this.arcball ? true : false;
   },
 
   /**
    * Function called when this object is destroyed.
    */
-  finalize: function TVC_finalize()
+  _finalize: function TVC__finalize()
   {
     TiltUtils.destroyObject(this.arcball);
-    TiltUtils.destroyObject(this.coordinates);
+    TiltUtils.destroyObject(this._coordinates);
 
     this.removeEventListeners();
-    this.presenter.ondraw = null;
+    this.presenter.controller = null;
+    this.presenter._controllerUpdate = 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.
  *
@@ -1389,19 +1449,22 @@ TiltVisualizer.Arcball = function TV_Arc
 
 TiltVisualizer.Arcball.prototype = {
 
   /**
    * Call this function whenever you need the updated rotation quaternion
    * and the zoom amount. These values will be returned as "rotation" and
    * "translation" properties inside an object.
    *
+   * @param {Number} aDelta
+   *                 the current animation frame delta
+   *
    * @return {Object} the rotation quaternion and the translation amount
    */
-  update: function TVA_update()
+  update: function TVA_update(aDelta)
   {
     let mousePress = this._mousePress;
     let mouseRelease = this._mouseRelease;
     let mouseMove = this._mouseMove;
     let mouseLerp = this._mouseLerp;
     let mouseButton = this._mouseButton;
 
     // smoothly update the mouse coordinates
@@ -1429,17 +1492,17 @@ TiltVisualizer.Arcball.prototype = {
 
     // left mouse button handles rotation
     if (mouseButton === 1 || this._rotating) {
       // the rotation doesn't stop immediately after the left mouse button is
       // released, so add a flag to smoothly continue it until it ends
       this._rotating = true;
 
       // find the sphere coordinates of the mouse positions
-      this.pointToSphere(x, y, this.width, this.height, this.radius, endVec);
+      this._pointToSphere(x, y, this.width, this.height, this.radius, endVec);
 
       // compute the vector perpendicular to the start & end vectors
       vec3.cross(startVec, endVec, pVec);
 
       // if the begin and end vectors don't coincide
       if (vec3.length(pVec) > 0) {
         deltaRot[0] = pVec[0];
         deltaRot[1] = pVec[1];
@@ -1549,21 +1612,29 @@ TiltVisualizer.Arcball.prototype = {
       (additionalRot[2] - deltaAdditionalRot[2]) * 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(deltaAdditionalRot[0], deltaAdditionalRot[1], 0, deltaRot);
+    quat4.fromEuler(
+      deltaAdditionalRot[0],
+      deltaAdditionalRot[1],
+      deltaAdditionalRot[2], deltaRot);
 
     // create an additional translation based on the key events
     vec3.set([deltaAdditionalTrans[0], deltaAdditionalTrans[1], 0], deltaTrans);
 
+    // handle the reset animation steps if necessary
+    if (this._resetInProgress) {
+      this._nextResetStep(aDelta || 1);
+    }
+
     // return the current rotation and translation
     return {
       rotation: quat4.multiply(deltaRot, currentRot),
       translation: vec3.add(deltaTrans, currentTrans)
     };
   },
 
   /**
@@ -1578,21 +1649,21 @@ 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._cancelReset();
     this._save();
 
     // find the sphere coordinates of the mouse positions
-    this.pointToSphere(
+    this._pointToSphere(
       x, y, this.width, this.height, this.radius, this._startVec);
 
     quat4.set(this._currentRot, this._lastRot);
   },
 
   /**
    * Function handling the mouseUp event.
    * Call this when a mouse button was released.
@@ -1654,31 +1725,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._cancelReset();
     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._cancelReset();
     this._keyCode[aCode] = true;
   },
 
   /**
    * Function handling the keyUp event.
    * Call this when a key was released.
    *
    * @param {Number} aCode
@@ -1700,17 +1771,17 @@ TiltVisualizer.Arcball.prototype = {
    *                 the width of canvas
    * @param {Number} aHeight
    *                 the height of canvas
    * @param {Number} aRadius
    *                 optional, the radius of the arcball
    * @param {Array} aSphereVec
    *                a 3d vector to store the sphere coordinates
    */
-  pointToSphere: function TVA_pointToSphere(
+  _pointToSphere: function TVA__pointToSphere(
     x, y, aWidth, aHeight, aRadius, aSphereVec)
   {
     // adjust point coords and scale down to range of [-1..1]
     x = (x - aWidth * 0.5) / aRadius;
     y = (y - aHeight * 0.5) / aRadius;
 
     // compute the square length of the vector to the point from the center
     let normal = 0;
@@ -1746,16 +1817,42 @@ TiltVisualizer.Arcball.prototype = {
    */
   cancelMouseEvents: function TVA_cancelMouseEvents()
   {
     this._rotating = false;
     this._mouseButton = -1;
   },
 
   /**
+   * Incremental translation method.
+   *
+   * @param {Array} aTranslation
+   *                the translation ammount on the [x, y] axis
+   */
+  translate: function TVP_translate(aTranslation)
+  {
+    this._additionalTrans[0] += aTranslation[0];
+    this._additionalTrans[1] += aTranslation[1];
+  },
+
+  /**
+   * Incremental rotation method.
+   *
+   * @param {Array} aRotation
+   *                the rotation ammount along the [x, y, z] axis
+   */
+  rotate: function TVP_rotate(aRotation)
+  {
+    // explicitly rotate along y, x, z values because they're eulerian angles
+    this._additionalRot[0] += TiltMath.radians(aRotation[1]);
+    this._additionalRot[1] += TiltMath.radians(aRotation[0]);
+    this._additionalRot[2] += TiltMath.radians(aRotation[2]);
+  },
+
+  /**
    * Moves a target point into view only if it's outside the currently visible
    * area bounds (in which case it also resets any additional transforms).
    *
    * @param {Arary} aPoint
    *                the [x, y] point which should be brought into view
    */
   moveIntoView: function TVA_moveIntoView(aPoint) {
     let visiblePointX = -(this._currentTrans[0] + this._additionalTrans[0]);
@@ -1798,84 +1895,93 @@ TiltVisualizer.Arcball.prototype = {
    *
    * @param {Array} aFinalTranslation
    *                optional, final vector translation
    * @param {Array} aFinalRotation
    *                optional, final quaternion rotation
    */
   reset: function TVA_reset(aFinalTranslation, aFinalRotation)
   {
-    if ("function" === typeof this.onResetStart) {
-      this.onResetStart();
-      this.onResetStart = null;
+    if ("function" === typeof this._onResetStart) {
+      this._onResetStart();
+      this._onResetStart = null;
     }
 
-    let func = this._nextResetIntervalStep.bind(this);
-
     this.cancelMouseEvents();
     this.cancelKeyEvents();
-    this._cancelResetInterval();
+    this._cancelReset();
 
     this._save();
     this._resetFinalTranslation = vec3.create(aFinalTranslation);
     this._resetFinalRotation = quat4.create(aFinalRotation);
-    this._resetInterval =
-      this.chromeWindow.setInterval(func, ARCBALL_RESET_INTERVAL);
+    this._resetInProgress = true;
   },
 
   /**
    * Cancels the current arcball reset animation if there is one.
    */
-  _cancelResetInterval: function TVA__cancelResetInterval()
+  _cancelReset: function TVA__cancelReset()
   {
-    if (this._resetInterval) {
-      this.chromeWindow.clearInterval(this._resetInterval);
-
-      this._resetInterval = null;
+    if (this._resetInProgress) {
+      this._resetInProgress = false;
       this._save();
 
-      if ("function" === typeof this.onResetFinish) {
-        this.onResetFinish();
-        this.onResetFinish = null;
+      if ("function" === typeof this._onResetFinish) {
+        this._onResetFinish();
+        this._onResetFinish = null;
+        this._onResetStep = null;
       }
     }
   },
 
   /**
    * Executes the next step in the arcball reset animation.
+   *
+   * @param {Number} aDelta
+   *                 the current animation frame delta
    */
-  _nextResetIntervalStep: function TVA__nextResetIntervalStep()
+  _nextResetStep: function TVA__nextResetStep(aDelta)
   {
-    let fDelta = EPSILON * EPSILON;
+    // a very large animation frame delta (in case of seriously low framerate)
+    // would cause all the interpolations to become highly unstable
+    aDelta = TiltMath.clamp(aDelta, 1, 100);
+
+    let fNearZero = EPSILON * EPSILON;
+    let fInterpLin = ARCBALL_RESET_LINEAR_FACTOR * aDelta;
+    let fInterpSph = ARCBALL_RESET_SPHERICAL_FACTOR;
     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 / 4);
-    quat4.slerp(this._currentRot, r, 1 - ARCBALL_RESET_FACTOR);
+    vec3.lerp(this._currentTrans, t, fInterpLin);
+    quat4.slerp(this._currentRot, r, fInterpSph);
 
     // also reset any additional transforms by the keyboard or mouse
-    vec3.scale(this._additionalTrans, ARCBALL_RESET_FACTOR);
-    vec3.scale(this._additionalRot, ARCBALL_RESET_FACTOR);
-    this._zoomAmount *= ARCBALL_RESET_FACTOR;
+    vec3.scale(this._additionalTrans, fInterpLin);
+    vec3.scale(this._additionalRot, fInterpLin);
+    this._zoomAmount *= fInterpLin;
 
     // 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 &&
-        vec3.length(this._additionalRot) < fDelta &&
-        vec3.length(this._additionalTrans) < fDelta) {
+    if (vec3.length(vec3.subtract(this._lastRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._deltaRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._currentRot, fRot, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._lastTrans, fTran, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._deltaTrans, fTran, [])) < fNearZero &&
+        vec3.length(vec3.subtract(this._currentTrans, fTran, [])) < fNearZero &&
+        vec3.length(this._additionalRot) < fNearZero &&
+        vec3.length(this._additionalTrans) < fNearZero) {
 
-      this._cancelResetInterval();
+      this._cancelReset();
+    }
+
+    if ("function" === typeof this._onResetStep) {
+      this._onResetStep();
     }
   },
 
   /**
    * Loads the keys to control this arcball.
    */
   _loadKeys: function TVA__loadKeys()
   {
@@ -1922,19 +2028,19 @@ TiltVisualizer.Arcball.prototype = {
       this._mouseLerp[0] = x;
       this._mouseLerp[1] = y;
     }
   },
 
   /**
    * Function called when this object is destroyed.
    */
-  finalize: function TVA_finalize()
+  _finalize: function TVA__finalize()
   {
-    this._cancelResetInterval();
+    this._cancelReset();
   }
 };
 
 /**
  * Tilt configuration preferences.
  */
 TiltVisualizer.Prefs = {
 
--- a/browser/devtools/tilt/TiltWorkerCrafter.js
+++ b/browser/devtools/tilt/TiltWorkerCrafter.js
@@ -33,51 +33,65 @@
  * 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 *****/
 "use strict";
 
-const SIXTEEN_OVER_255 = 16 / 255;
-const ONE_OVER_255 = 1 / 255;
-
 /**
  * Given the initialization data (thickness, sizes and information about
  * each DOM node) this worker sends back the arrays representing
  * vertices, texture coords, colors, indices and all the needed data for
  * rendering the DOM visualization mesh.
  *
  * Used in the TiltVisualization.Presenter object.
  */
 self.onmessage = function TWC_onMessage(event)
 {
   let data = event.data;
+  let maxGroupNodes = parseInt(data.maxGroupNodes);
   let thickness = data.thickness;
   let style = data.style;
   let texWidth = data.texWidth;
   let texHeight = data.texHeight;
   let nodesInfo = data.nodesInfo;
 
-  // create the arrays used to construct the 3D mesh data
-  let vertices = [];
-  let texCoord = [];
-  let color = [];
-  let stacksIndices = [];
-  let wireframeIndices = [];
-  let meshWidth = 0;
-  let meshHeight = 0;
+  let mesh = {
+    allVertices: [],
+    groups: [],
+    width: 0,
+    height: 0
+  };
+
+  let vertices;
+  let texCoord;
+  let color;
+  let stacksIndices;
+  let wireframeIndices;
+  let index;
 
   // seed the random function to get the same values each time
   // we're doing this to avoid ugly z-fighting with overlapping nodes
   self.random.seed(0);
 
   // go through all the dom nodes and compute the verts, texcoord etc.
-  for (let n = 0, i = 0, len = nodesInfo.length; n < len; n++) {
+  for (let n = 0, len = nodesInfo.length; n < len; n++) {
+
+    // check if we need to start creating a new group
+    if (n % maxGroupNodes === 0) {
+      vertices = []; // recreate the arrays used to construct the 3D mesh data
+      texCoord = [];
+      color = [];
+      stacksIndices = [];
+      wireframeIndices = [];
+      index = 0;
+    }
+
     let info = nodesInfo[n];
     let depth = info.depth;
     let coord = info.coord;
 
     // calculate the stack x, y, z, width and height coordinates
     let z = depth * thickness;
     let y = coord.top;
     let x = coord.left;
@@ -150,16 +164,17 @@ self.onmessage = function TWC_onMessage(
                g10, g11, g12,
                g10, g11, g12,
                g10, g11, g12,
                g20, g21, g22,
                g20, g21, g22,
                g20, g21, g22,
                g20, g21, g22);
 
+    let i = index; // number of vertex points, used to create the indices array
     let ip1 = i + 1;
     let ip2 = ip1 + 1;
     let ip3 = ip2 + 1;
     let ip4 = ip3 + 1;
     let ip5 = ip4 + 1;
     let ip6 = ip5 + 1;
     let ip7 = ip6 + 1;
     let ip8 = ip7 + 1;
@@ -177,33 +192,38 @@ self.onmessage = function TWC_onMessage(
     // compute the wireframe indices
     if (depth !== 0) {
       wireframeIndices.unshift(i,    ip1, ip1,  ip2,
                                ip2,  ip3, ip3,  i,
                                ip8,  i,   ip9,  ip1,
                                ip11, ip3, ip10, ip2);
     }
 
-    // number of vertex points, used for creating the indices array
-    i += 12; // a vertex has 3 coords: x, y and z
+    // there are 12 vertices in a stack representing a node
+    index += 12;
 
     // set the maximum mesh width and height to calculate the center offset
-    meshWidth = Math.max(w, meshWidth);
-    meshHeight = Math.max(h, meshHeight);
+    mesh.width = Math.max(w, mesh.width);
+    mesh.height = Math.max(h, mesh.height);
+
+    // check if we need to save the currently active group; this happens after
+    // we filled all the "slots" in a group or there aren't any remaining nodes
+    if (((n + 1) % maxGroupNodes === 0) || (n === len - 1)) {
+      mesh.groups.push({
+        vertices: vertices,
+        texCoord: texCoord,
+        color: color,
+        stacksIndices: stacksIndices,
+        wireframeIndices: wireframeIndices
+      });
+      mesh.allVertices = mesh.allVertices.concat(vertices);
+    }
   }
 
-  self.postMessage({
-    vertices: vertices,
-    texCoord: texCoord,
-    color: color,
-    stacksIndices: stacksIndices,
-    wireframeIndices: wireframeIndices,
-    meshWidth: meshWidth,
-    meshHeight: meshHeight
-  });
+  self.postMessage(mesh);
   close();
 };
 
 /**
  * Utility functions for generating random numbers using the Alea algorithm.
  */
 self.random = {
 
--- a/browser/devtools/tilt/test/Makefile.in
+++ b/browser/devtools/tilt/test/Makefile.in
@@ -75,16 +75,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_tilt_math06.js \
 	browser_tilt_math07.js \
 	browser_tilt_picking.js \
 	browser_tilt_picking_delete.js \
 	browser_tilt_picking_highlight01-offs.js \
 	browser_tilt_picking_highlight01.js \
 	browser_tilt_picking_highlight02.js \
 	browser_tilt_picking_highlight03.js \
+	browser_tilt_picking_miv.js \
 	browser_tilt_utils01.js \
 	browser_tilt_utils02.js \
 	browser_tilt_utils03.js \
 	browser_tilt_utils04.js \
 	browser_tilt_utils05.js \
 	browser_tilt_utils06.js \
 	browser_tilt_visualizer.js \
 	browser_tilt_zoom.js \
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset-typeahead.js
@@ -57,21 +57,33 @@ function performTest(canvas, arcball, ca
       EventUtils.synthesizeKey("VK_S", { type: "keyup" });
       EventUtils.synthesizeKey("VK_RIGHT", { type: "keyup" });
 
       // ok, transformations finished, we can now try to reset the model view
 
       window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
-        arcball.onResetStart = function() {
+        arcball._onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
-        arcball.onResetFinish = function() {
+        arcball._onResetStep = function() {
+          info("\nlastRot: " + quat4.str(arcball._lastRot) +
+               "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+               "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+               "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+               "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+               "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+               "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+               "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+               "\nzoomAmount: " + arcball._zoomAmount);
+        };
+
+        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]),
@@ -84,18 +96,20 @@ function performTest(canvas, arcball, ca
           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();
+          executeSoon(function() {
+            info("Finishing arcball reset test.");
+            callback();
+          });
         };
 
         EventUtils.synthesizeKey("VK_R", { type: "keydown" });
 
       }, Math.random() * 1000); // leave enough time for transforms to happen
     }, Math.random() * 1000);
   }, Math.random() * 1000);
 }
--- a/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball-reset.js
@@ -55,21 +55,33 @@ function performTest(canvas, arcball, ca
       EventUtils.synthesizeKey("VK_W", { type: "keyup" });
       EventUtils.synthesizeKey("VK_LEFT", { type: "keyup" });
 
       // ok, transformations finished, we can now try to reset the model view
 
       window.setTimeout(function() {
         info("Synthesizing arcball reset key press.");
 
-        arcball.onResetStart = function() {
+        arcball._onResetStart = function() {
           info("Starting arcball reset animation.");
         };
 
-        arcball.onResetFinish = function() {
+        arcball._onResetStep = function() {
+          info("\nlastRot: " + quat4.str(arcball._lastRot) +
+               "\ndeltaRot: " + quat4.str(arcball._deltaRot) +
+               "\ncurrentRot: " + quat4.str(arcball._currentRot) +
+               "\nlastTrans: " + vec3.str(arcball._lastTrans) +
+               "\ndeltaTrans: " + vec3.str(arcball._deltaTrans) +
+               "\ncurrentTrans: " + vec3.str(arcball._currentTrans) +
+               "\nadditionalRot: " + vec3.str(arcball._additionalRot) +
+               "\nadditionalTrans: " + vec3.str(arcball._additionalTrans) +
+               "\nzoomAmount: " + arcball._zoomAmount);
+        };
+
+        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]),
@@ -82,18 +94,20 @@ function performTest(canvas, arcball, ca
           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();
+          executeSoon(function() {
+            info("Finishing arcball reset test.");
+            callback();
+          });
         };
 
         EventUtils.synthesizeKey("VK_R", { type: "keydown" });
 
       }, Math.random() * 1000); // leave enough time for transforms to happen
     }, Math.random() * 1000);
   }, Math.random() * 1000);
 }
--- a/browser/devtools/tilt/test/browser_tilt_arcball.js
+++ b/browser/devtools/tilt/test/browser_tilt_arcball.js
@@ -43,20 +43,20 @@ function test() {
     "The second arcball height wasn't set correctly.");
   is(arcball2.radius, 654,
     "The second arcball radius wasn't implicitly set correctly.");
 
 
   let arcball3 = new TiltVisualizer.Arcball(window, 512, 512);
 
   let sphereVec = vec3.create();
-  arcball3.pointToSphere(123, 456, 256, 512, 512, sphereVec);
+  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.");
+    "The _pointToSphere() function didn't map the coordinates correctly.");
 
   let stack1 = [];
   let expect1 = [
     { rotation: [
       -0.08877250552177429, 0.0242881178855896,
       -0.04222869873046875, -0.9948599338531494],
       translation: [0, 0, 0] },
     { rotation: [
--- a/browser/devtools/tilt/test/browser_tilt_controller.js
+++ b/browser/devtools/tilt/test/browser_tilt_controller.js
@@ -42,40 +42,40 @@ function test() {
 
 
         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();
+          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.");
 
           save();
 
 
           cancellingEvent();
-          instance.controller.update();
+          instance.controller._update();
 
           ok(!isEqualVec(tran(), prev_tran),
             "Even if the canvas lost focus, the vector has some inertia.");
           ok(!isEqualVec(rot(), prev_rot),
             "Even if the canvas lost focus, the quaternion has some inertia.");
 
           save();
 
 
           while (!isEqualVec(tran(), prev_tran) ||
                  !isEqualVec(rot(), prev_rot)) {
-            instance.controller.update();
+            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.");
--- a/browser/devtools/tilt/test/browser_tilt_picking.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking.js
@@ -16,25 +16,23 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         let presenter = instance.presenter;
         let canvas = presenter.canvas;
 
-        presenter.onSetupMesh = function() {
+        presenter._onSetupMesh = function() {
 
           presenter.pickNode(canvas.width / 2, 10, {
             onpick: function(data)
             {
               ok(data.index > 0,
                 "Simply picking a node didn't work properly.");
-              ok(!presenter.highlight.disabled,
-                "After only picking a node, it shouldn't be highlighted.");
 
               Services.obs.addObserver(cleanup, DESTROYED, false);
               InspectorUI.closeInspectorUI();
             }
           });
         };
       }
     });
--- a/browser/devtools/tilt/test/browser_tilt_picking_delete.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_delete.js
@@ -18,45 +18,45 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         presenter = instance.presenter;
         Services.obs.addObserver(whenNodeRemoved, NODE_REMOVED, false);
 
-        presenter.onSetupMesh = function() {
+        presenter._onSetupMesh = function() {
           presenter.highlightNodeAt(presenter.canvas.width / 2, 10, {
             onpick: function()
             {
               ok(presenter._currentSelection > 0,
                 "Highlighting a node didn't work properly.");
-              ok(!presenter.highlight.disabled,
+              ok(!presenter._highlight.disabled,
                 "After highlighting a node, it should be highlighted. D'oh.");
 
               presenter.deleteNode();
             }
           });
         };
       }
     });
   });
 }
 
 function whenNodeRemoved() {
   ok(presenter._currentSelection > 0,
     "Deleting a node shouldn't change the current selection.");
-  ok(presenter.highlight.disabled,
+  ok(presenter._highlight.disabled,
     "After deleting a node, it shouldn't be highlighted.");
 
   let nodeIndex = presenter._currentSelection;
-  let meshData = presenter.meshData;
+  let vertices = presenter._meshStacks[0].vertices.components;
 
   for (let i = 0, k = 36 * nodeIndex; i < 36; i++) {
-    is(meshData.vertices[i + k], 0,
+    is(vertices[i + k], 0,
       "The stack vertices weren't degenerated properly.");
   }
 
   executeSoon(function() {
     Services.obs.addObserver(cleanup, DESTROYED, false);
     InspectorUI.closeInspectorUI();
   });
 }
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01-offs.js
@@ -19,45 +19,45 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         presenter = instance.presenter;
         Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
-        presenter.onInitializationFinished = function() {
+        presenter._onInitializationFinished = function() {
           let contentDocument = presenter.contentWindow.document;
           let div = contentDocument.getElementById("far-far-away");
 
           presenter.highlightNode(div, "moveIntoView");
         };
       }
     });
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
-  ok(!presenter.highlight.disabled,
+  ok(!presenter._highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
-  ok(presenter.controller.arcball._resetInterval,
+  ok(presenter.controller.arcball._resetInProgress,
     "Highlighting a node that's not already visible should trigger a reset!");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNode(null);
   });
 }
 
 function whenUnhighlighting() {
   ok(presenter._currentSelection < 0,
     "Unhighlighting a should remove the current selection.");
-  ok(presenter.highlight.disabled,
+  ok(presenter._highlight.disabled,
     "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(cleanup, DESTROYED, false);
     InspectorUI.closeInspectorUI();
   });
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight01.js
@@ -18,45 +18,45 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         presenter = instance.presenter;
         Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
-        presenter.onSetupMesh = function() {
+        presenter._onSetupMesh = function() {
           let contentDocument = presenter.contentWindow.document;
           let div = contentDocument.getElementById("first-law");
 
           presenter.highlightNode(div, "moveIntoView");
         };
       }
     });
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
-  ok(!presenter.highlight.disabled,
+  ok(!presenter._highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
-  ok(!presenter.controller.arcball._resetInterval,
+  ok(!presenter.controller.arcball._resetInProgress,
     "Highlighting a node that's already visible shouldn't trigger a reset.");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNode(null);
   });
 }
 
 function whenUnhighlighting() {
   ok(presenter._currentSelection < 0,
     "Unhighlighting a should remove the current selection.");
-  ok(presenter.highlight.disabled,
+  ok(presenter._highlight.disabled,
     "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(cleanup, DESTROYED, false);
     InspectorUI.closeInspectorUI();
   });
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight02.js
@@ -18,40 +18,40 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         presenter = instance.presenter;
         Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
-        presenter.onSetupMesh = function() {
+        presenter._onSetupMesh = function() {
           presenter.highlightNodeAt(presenter.canvas.width / 2, 10);
         };
       }
     });
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
-  ok(!presenter.highlight.disabled,
+  ok(!presenter._highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNodeAt(-1, -1);
   });
 }
 
 function whenUnhighlighting() {
   ok(presenter._currentSelection < 0,
     "Unhighlighting a should remove the current selection.");
-  ok(presenter.highlight.disabled,
+  ok(presenter._highlight.disabled,
     "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(cleanup, DESTROYED, false);
     InspectorUI.closeInspectorUI();
   });
 }
 
--- a/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
+++ b/browser/devtools/tilt/test/browser_tilt_picking_highlight03.js
@@ -18,40 +18,40 @@ function test() {
 
   createTab(function() {
     createTilt({
       onTiltOpen: function(instance)
       {
         presenter = instance.presenter;
         Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
 
-        presenter.onSetupMesh = function() {
+        presenter._onSetupMesh = function() {
           presenter.highlightNodeFor(5); // 1 = html, 2 = body, 3 = first div
         };
       }
     });
   });
 }
 
 function whenHighlighting() {
   ok(presenter._currentSelection > 0,
     "Highlighting a node didn't work properly.");
-  ok(!presenter.highlight.disabled,
+  ok(!presenter._highlight.disabled,
     "After highlighting a node, it should be highlighted. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(whenUnhighlighting, UNHIGHLIGHTING, false);
     presenter.highlightNodeFor(-1);
   });
 }
 
 function whenUnhighlighting() {
   ok(presenter._currentSelection < 0,
     "Unhighlighting a should remove the current selection.");
-  ok(presenter.highlight.disabled,
+  ok(presenter._highlight.disabled,
     "After unhighlighting a node, it shouldn't be highlighted anymore. D'oh.");
 
   executeSoon(function() {
     Services.obs.addObserver(cleanup, DESTROYED, false);
     InspectorUI.closeInspectorUI();
   });
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/tilt/test/browser_tilt_picking_miv.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+let presenter;
+
+function test() {
+  if (!isTiltEnabled()) {
+    info("Skipping highlight test because Tilt isn't enabled.");
+    return;
+  }
+  if (!isWebGLSupported()) {
+    info("Skipping highlight test because WebGL isn't supported.");
+    return;
+  }
+
+  requestLongerTimeout(10);
+  waitForExplicitFinish();
+
+  createTab(function() {
+    createTilt({
+      onTiltOpen: function(instance)
+      {
+        presenter = instance.presenter;
+        Services.obs.addObserver(whenHighlighting, HIGHLIGHTING, false);
+
+        presenter._onInitializationFinished = function() {
+          let contentDocument = presenter.contentWindow.document;
+          let div = contentDocument.getElementById("far-far-away");
+
+          presenter.highlightNode(div);
+        };
+      }
+    });
+  });
+}
+
+function whenHighlighting() {
+  ok(presenter._currentSelection > 0,
+    "Highlighting a node didn't work properly.");
+  ok(!presenter._highlight.disabled,
+    "After highlighting a node, it should be highlighted. D'oh.");
+  ok(!presenter.controller.arcball._resetInProgress,
+    "Highlighting a node that's not already visible shouldn't trigger a reset " +
+    "without this being explicitly requested!");
+
+  EventUtils.sendKey("F");
+  executeSoon(whenBringingIntoView);
+}
+
+function whenBringingIntoView() {
+  ok(presenter._currentSelection > 0,
+    "The node should still be selected.");
+  ok(!presenter._highlight.disabled,
+    "The node should still be highlighted");
+  ok(presenter.controller.arcball._resetInProgress,
+    "Highlighting a node that's not already visible should trigger a reset " +
+    "when this is being explicitly requested!");
+
+  executeSoon(function() {
+    Services.obs.addObserver(cleanup, DESTROYED, false);
+    InspectorUI.closeInspectorUI();
+  });
+}
+
+function cleanup() {
+  Services.obs.removeObserver(whenHighlighting, HIGHLIGHTING);
+  Services.obs.removeObserver(cleanup, DESTROYED);
+  gBrowser.removeCurrentTab();
+  finish();
+}
--- a/browser/devtools/tilt/test/browser_tilt_utils05.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils05.js
@@ -1,16 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
-let tmp = {};
-Cu.import("resource:///modules/devtools/LayoutHelpers.jsm", tmp);
-let LayoutHelpers = tmp.LayoutHelpers;
-
 function init(callback) {
   let iframe = gBrowser.ownerDocument.createElement("iframe");
 
   iframe.addEventListener("load", function onLoad() {
     iframe.removeEventListener("load", onLoad, true);
     callback(iframe);
 
     gBrowser.parentNode.removeChild(iframe);
--- a/browser/devtools/tilt/test/browser_tilt_utils06.js
+++ b/browser/devtools/tilt/test/browser_tilt_utils06.js
@@ -6,17 +6,17 @@ let someObject = {
   a: 1,
   func: function()
   {
     this.b = 2;
   }
 };
 
 let anotherObject = {
-  finalize: function()
+  _finalize: function()
   {
     someObject.c = 3;
   }
 };
 
 function test() {
   ok(TiltUtils, "The TiltUtils object doesn't exist.");
 
--- a/browser/devtools/tilt/test/browser_tilt_visualizer.js
+++ b/browser/devtools/tilt/test/browser_tilt_visualizer.js
@@ -14,17 +14,16 @@ function test() {
 
   let webGLError = false;
   let webGLLoad = false;
 
   let visualizer = new TiltVisualizer({
     chromeWindow: window,
     contentWindow: gBrowser.selectedBrowser.contentWindow,
     parentNode: gBrowser.selectedBrowser.parentNode,
-    requestAnimationFrame: window.mozRequestAnimationFrame,
     inspectorUI: window.InspectorUI,
 
     onError: function onWebGLError()
     {
       webGLError = true;
     },
 
     onLoad: function onWebGLLoad()
@@ -66,59 +65,59 @@ function test() {
     "The visualizer presenter wasn't destroyed.");
   is(visualizer.controller, undefined,
     "The visualizer controller wasn't destroyed.");
   is(visualizer.canvas, undefined,
     "The visualizer canvas wasn't destroyed.");
 }
 
 function testPresenter(presenter) {
-  ok(presenter.renderer,
+  ok(presenter._renderer,
     "The presenter renderer wasn't initialized properly.");
-  ok(presenter.visualizationProgram,
+  ok(presenter._visualizationProgram,
     "The presenter visualizationProgram wasn't initialized properly.");
-  ok(presenter.texture,
+  ok(presenter._texture,
     "The presenter texture wasn't initialized properly.");
-  ok(!presenter.meshStacks,
+  ok(!presenter._meshStacks,
     "The presenter meshStacks shouldn't be initialized yet.");
-  ok(!presenter.meshWireframe,
+  ok(!presenter._meshWireframe,
     "The presenter meshWireframe shouldn't be initialized yet.");
-  ok(presenter.traverseData,
+  ok(presenter._traverseData,
     "The presenter nodesInformation wasn't initialized properly.");
-  ok(presenter.highlight,
+  ok(presenter._highlight,
     "The presenter highlight wasn't initialized properly.");
-  ok(presenter.highlight.disabled,
-    "The presenter highlight should be initially disabled");
-  ok(isApproxVec(presenter.highlight.v0, [0, 0, 0]),
+  ok(presenter._highlight.disabled,
+    "The presenter highlight should be initially disabled.");
+  ok(isApproxVec(presenter._highlight.v0, [0, 0, 0]),
     "The presenter highlight first vertex should be initially zeroed.");
-  ok(isApproxVec(presenter.highlight.v1, [0, 0, 0]),
+  ok(isApproxVec(presenter._highlight.v1, [0, 0, 0]),
     "The presenter highlight second vertex should be initially zeroed.");
-  ok(isApproxVec(presenter.highlight.v2, [0, 0, 0]),
+  ok(isApproxVec(presenter._highlight.v2, [0, 0, 0]),
     "The presenter highlight third vertex should be initially zeroed.");
-  ok(isApproxVec(presenter.highlight.v3, [0, 0, 0]),
+  ok(isApproxVec(presenter._highlight.v3, [0, 0, 0]),
     "The presenter highlight fourth vertex should be initially zeroed.");
   ok(presenter.transforms,
     "The presenter transforms wasn't initialized properly.");
-  ok(isApproxVec(presenter.transforms.zoom, 1),
+  is(presenter.transforms.zoom, 1,
     "The presenter transforms zoom should be initially 1.");
   ok(isApproxVec(presenter.transforms.offset, [0, 0, 0]),
     "The presenter transforms offset should be initially zeroed.");
   ok(isApproxVec(presenter.transforms.translation, [0, 0, 0]),
     "The presenter transforms translation should be initially zeroed.");
   ok(isApproxVec(presenter.transforms.rotation, [0, 0, 0, 1]),
     "The presenter transforms rotation should be initially set to identity.");
 
   presenter.setTranslation([1, 2, 3]);
   presenter.setRotation([5, 6, 7, 8]);
 
   ok(isApproxVec(presenter.transforms.translation, [1, 2, 3]),
     "The presenter transforms translation wasn't modified as it should");
   ok(isApproxVec(presenter.transforms.rotation, [5, 6, 7, 8]),
     "The presenter transforms rotation wasn't modified as it should");
-  ok(presenter.redraw,
+  ok(presenter._redraw,
     "The new transforms should have issued a redraw request.");
 }
 
 function testController(controller) {
   ok(controller.arcball,
     "The controller arcball wasn't initialized properly.");
   ok(!controller.coordinates,
     "The presenter meshWireframe shouldn't be initialized yet.");
--- a/browser/devtools/tilt/test/browser_tilt_zoom.js
+++ b/browser/devtools/tilt/test/browser_tilt_zoom.js
@@ -30,17 +30,17 @@ function test() {
 
         ok(isApprox(instance.presenter.transforms.zoom, ZOOM),
           "The presenter transforms zoom wasn't initially set correctly.");
 
         let contentWindow = gBrowser.selectedBrowser.contentWindow;
         let initialWidth = contentWindow.innerWidth;
         let initialHeight = contentWindow.innerHeight;
 
-        let renderer = instance.presenter.renderer;
+        let renderer = instance.presenter._renderer;
         let arcball = instance.controller.arcball;
 
         ok(isApprox(contentWindow.innerWidth * ZOOM, renderer.width, 1),
           "The renderer width wasn't set correctly before the resize.");
         ok(isApprox(contentWindow.innerHeight * ZOOM, renderer.height, 1),
           "The renderer height wasn't set correctly before the resize.");
 
         ok(isApprox(contentWindow.innerWidth * ZOOM, arcball.width, 1),
--- a/browser/devtools/tilt/test/head.js
+++ b/browser/devtools/tilt/test/head.js
@@ -2,25 +2,27 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 let tempScope = {};
 Components.utils.import("resource:///modules/devtools/TiltGL.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltMath.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltUtils.jsm", tempScope);
 Components.utils.import("resource:///modules/devtools/TiltVisualizer.jsm", tempScope);
+Components.utils.import("resource:///modules/devtools/LayoutHelpers.jsm", tempScope);
 let TiltGL = tempScope.TiltGL;
 let EPSILON = tempScope.EPSILON;
 let TiltMath = tempScope.TiltMath;
 let vec3 = tempScope.vec3;
 let mat3 = tempScope.mat3;
 let mat4 = tempScope.mat4;
 let quat4 = tempScope.quat4;
 let TiltUtils = tempScope.TiltUtils;
 let TiltVisualizer = tempScope.TiltVisualizer;
+let LayoutHelpers = tempScope.LayoutHelpers;
 
 
 const DEFAULT_HTML = "data:text/html," +
   "<DOCTYPE html>" +
   "<html>" +
     "<head>" +
       "<title>Three Laws</title>" +
     "</head>" +
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webconsole/GcliTiltCommands.jsm
@@ -0,0 +1,224 @@
+/* ***** 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 GCLI Commands.
+ *
+ * The Initial Developer of the Original Code is
+ * The Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Victor Porof <vporof@mozilla.com> (original author)
+ *
+ * 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 ***** */
+
+
+let EXPORTED_SYMBOLS = [ "GcliCommands" ];
+
+Components.utils.import("resource:///modules/gcli.jsm");
+Components.utils.import("resource:///modules/HUDService.jsm");
+
+
+/**
+ * 'tilt' command
+ */
+gcli.addCommand({
+  name: 'tilt',
+  description: gcli.lookup("tiltDesc"),
+  manual: gcli.lookup("tiltManual")
+});
+
+
+/**
+ * 'tilt open' command
+ */
+gcli.addCommand({
+  name: 'tilt open',
+  description: gcli.lookup("tiltOpenDesc"),
+  manual: gcli.lookup("tiltOpenManual"),
+  params: [
+    {
+      name: "node",
+      type: "node",
+      defaultValue: null,
+      description: gcli.lookup("inspectNodeDesc"),
+      manual: gcli.lookup("inspectNodeManual")
+    }
+  ],
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let InspectorUI = chromeWindow.InspectorUI;
+    let Tilt = chromeWindow.Tilt;
+
+    if (Tilt.currentInstance) {
+      Tilt.update(args.node);
+    } else {
+      let hudId = chromeWindow.HUDConsoleUI.getOpenHUD();
+      let hud = HUDService.getHudReferenceById(hudId);
+
+      if (hud && !hud.consolePanel) {
+        HUDService.deactivateHUDForContext(chromeWindow.gBrowser.selectedTab);
+      }
+      InspectorUI.openInspectorUI(args.node);
+      Tilt.initialize();
+    }
+  }
+});
+
+
+/**
+ * 'tilt translate' command
+ */
+gcli.addCommand({
+  name: 'tilt translate',
+  description: gcli.lookup("tiltTranslateDesc"),
+  manual: gcli.lookup("tiltTranslateManual"),
+  params: [
+    {
+      name: "x",
+      type: "number",
+      defaultValue: 0,
+      description: gcli.lookup("tiltTranslateXDesc"),
+      manual: gcli.lookup("tiltTranslateXManual")
+    },
+    {
+      name: "y",
+      type: "number",
+      defaultValue: 0,
+      description: gcli.lookup("tiltTranslateYDesc"),
+      manual: gcli.lookup("tiltTranslateYManual")
+    }
+  ],
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let Tilt = chromeWindow.Tilt;
+
+    if (Tilt.currentInstance) {
+      Tilt.currentInstance.controller.arcball.translate([args.x, args.y]);
+    }
+  }
+});
+
+
+/**
+ * 'tilt rotate' command
+ */
+gcli.addCommand({
+  name: 'tilt rotate',
+  description: gcli.lookup("tiltRotateDesc"),
+  manual: gcli.lookup("tiltRotateManual"),
+  params: [
+    {
+      name: "x",
+      type: { name: 'number', min: -360, max: 360, step: 10 },
+      defaultValue: 0,
+      description: gcli.lookup("tiltRotateXDesc"),
+      manual: gcli.lookup("tiltRotateXManual")
+    },
+    {
+      name: "y",
+      type: { name: 'number', min: -360, max: 360, step: 10 },
+      defaultValue: 0,
+      description: gcli.lookup("tiltRotateYDesc"),
+      manual: gcli.lookup("tiltRotateYManual")
+    },
+    {
+      name: "z",
+      type: { name: 'number', min: -360, max: 360, step: 10 },
+      defaultValue: 0,
+      description: gcli.lookup("tiltRotateZDesc"),
+      manual: gcli.lookup("tiltRotateZManual")
+    }
+  ],
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let Tilt = chromeWindow.Tilt;
+
+    if (Tilt.currentInstance) {
+      Tilt.currentInstance.controller.arcball.rotate([args.x, args.y, args.z]);
+    }
+  }
+});
+
+
+/**
+ * 'tilt zoom' command
+ */
+gcli.addCommand({
+  name: 'tilt zoom',
+  description: gcli.lookup("tiltZoomDesc"),
+  manual: gcli.lookup("tiltZoomManual"),
+  params: [
+    {
+      name: "zoom",
+      type: { name: 'number' },
+      description: gcli.lookup("tiltZoomAmountDesc"),
+      manual: gcli.lookup("tiltZoomAmountManual")
+    }
+  ],
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let Tilt = chromeWindow.Tilt;
+
+    if (Tilt.currentInstance) {
+      Tilt.currentInstance.controller.arcball.zoom(-args.zoom);
+    }
+  }
+});
+
+
+/**
+ * 'tilt reset' command
+ */
+gcli.addCommand({
+  name: 'tilt reset',
+  description: gcli.lookup("tiltResetDesc"),
+  manual: gcli.lookup("tiltResetManual"),
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let Tilt = chromeWindow.Tilt;
+
+    if (Tilt.currentInstance) {
+      Tilt.currentInstance.controller.arcball.reset();
+    }
+  }
+});
+
+
+/**
+ * 'tilt close' command
+ */
+gcli.addCommand({
+  name: 'tilt close',
+  description: gcli.lookup("tiltCloseDesc"),
+  manual: gcli.lookup("tiltCloseManual"),
+  exec: function(args, context) {
+    let chromeWindow = context.environment.chromeDocument.defaultView;
+    let Tilt = chromeWindow.Tilt;
+
+    Tilt.destroy(Tilt.currentWindowId);
+  }
+});
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -159,16 +159,17 @@ function LogFactory(aMessagePrefix)
  * modules. In general there is no reason when JSMs need to export symbols
  * except when they need the host environment to inform them of things like the
  * current window/document/etc.
  */
 function loadCommands() {
   let commandExports = {};
 
   Cu.import("resource:///modules/GcliCommands.jsm", commandExports);
+  Cu.import("resource:///modules/GcliTiltCommands.jsm", commandExports);
 
   return commandExports;
 }
 
 let log = LogFactory("*** HUDService:");
 
 const HUD_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 
--- a/browser/devtools/webconsole/Makefile.in
+++ b/browser/devtools/webconsole/Makefile.in
@@ -45,16 +45,17 @@ VPATH		= @srcdir@
 include $(DEPTH)/config/autoconf.mk
 
 EXTRA_JS_MODULES = \
 		PropertyPanel.jsm \
 		NetworkHelper.jsm \
 		AutocompletePopup.jsm \
 		gcli.jsm \
 		GcliCommands.jsm \
+		GcliTiltCommands.jsm \
 		$(NULL)
 
 EXTRA_PP_JS_MODULES = \
 		HUDService.jsm \
 		$(NULL)
 
 TEST_DIRS = test
 
--- a/browser/devtools/webconsole/test/browser_gcli_web.js
+++ b/browser/devtools/webconsole/test/browser_gcli_web.js
@@ -1098,17 +1098,16 @@ exports.testBlank = function() {
 };
 
 exports.testIncompleteMultiMatch = function() {
   update({ typed: 't', cursor: { start: 1, end: 1 } });
   test.is(        'I', statuses);
   test.is(Status.ERROR, status);
   test.is(-1, assignC.paramIndex);
   test.ok(assignC.getPredictions().length > 0);
-  test.ok(assignC.getPredictions().length < 20); // could break ...
   verifyPredictionsContains('tsv', assignC.getPredictions());
   verifyPredictionsContains('tsr', assignC.getPredictions());
   test.is(null, requ.commandAssignment.getValue());
 };
 
 exports.testIncompleteSingleMatch = function() {
   update({ typed: 'tselar', cursor: { start: 6, end: 6 } });
   test.is(        'IIIIII', statuses);
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -208,21 +208,29 @@ can reach it easily. -->
   -  "Scratchpad" in your locale. You should feel free to find a close
   -  approximation to it or choose a word (or words) that means
   -  "simple discardable text editor". -->
 <!ENTITY scratchpad.label             "Scratchpad">
 <!ENTITY scratchpad.accesskey         "s">
 <!ENTITY scratchpad.keycode           "VK_F4">
 <!ENTITY scratchpad.keytext           "F4">
 
-<!ENTITY inspectPanelTitle.label        "HTML">
 <!ENTITY inspectButton.label            "Inspect">
 <!ENTITY inspectButton.accesskey        "I">
 <!ENTITY inspectCloseButton.tooltiptext "Close Inspector">
 
+<!ENTITY inspectorHTMLCopyInner.label       "Copy Inner HTML">
+<!ENTITY inspectorHTMLCopyInner.accesskey   "I">
+
+<!ENTITY inspectorHTMLCopyOuter.label       "Copy Outer HTML">
+<!ENTITY inspectorHTMLCopyOuter.accesskey   "O">
+
+<!ENTITY inspectorHTMLDelete.label          "Delete Node">
+<!ENTITY inspectorHTMLDelete.accesskey      "D">
+
 <!-- LOCALIZATION NOTE (inspect3DViewButton.label): This button shows an
   -  alternate view for the Inspector, creating a 3D visualization of the
   -  webpage. -->
 <!ENTITY inspect3DViewButton.label     "3D View">
 <!ENTITY inspect3DViewButton.accesskey "W">
 
 <!ENTITY inspectStyleButton.label     "Style">
 <!ENTITY inspectStyleButton.accesskey "S">
@@ -594,8 +602,20 @@ just addresses the organization to follo
 <!ENTITY syncSetup.label              "Set Up &syncBrand.shortName.label;…">
 <!ENTITY syncSetup.accesskey          "Y">
 <!ENTITY syncSyncNowItem.label        "Sync Now">
 <!ENTITY syncSyncNowItem.accesskey    "S">
 <!ENTITY syncToolbarButton.label      "Sync">
 
 <!ENTITY addonBarCloseButton.tooltip  "Close Add-on Bar">
 <!ENTITY toggleAddonBarCmd.key        "/">
+
+<!-- LOCALIZATION NOTE (htmlPanel.label): This is a label for a button that
+activates the Web Developer->Inspect UI's HTML Tree Panel. -->
+<!ENTITY htmlPanel.label              "HTML">
+
+<!-- LOCALIZATION NOTE (htmlPanel.tooltiptext): The text that appears when a user
+hovers over the HTML panel's toolbar button. -->
+<!ENTITY htmlPanel.tooltiptext        "HTML panel">
+
+<!-- LOCALIZATION NOTE (htmlPanel.accesskey): The key bound to the HTML panel's
+toolbar button -->
+<!ENTITY htmlPanel.accesskey          "H">
--- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties
@@ -51,16 +51,146 @@ inspectManual=Investigate the dimensions
 # when the user is using this command.
 inspectNodeDesc=CSS selector
 
 # LOCALIZATION NOTE (inspectNodeManual) A fuller description of the 'node'
 # parameter to the 'inspect' command, displayed when the user asks for help
 # on what it does.
 inspectNodeManual=A CSS selector for use with Document.querySelector which identifies a single element
 
+# LOCALIZATION NOTE (tiltDesc) A very short description of the 'tilt'
+# command. See tiltManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltDesc=Visualize the webpage in 3D
+
+# LOCALIZATION NOTE (tiltManual) A fuller description of the 'tilt'
+# command, displayed when the user asks for help on what it does.
+tiltManual=Investigate the relationship between various parts of a webpage and their ancestors in a 3D environment
+
+# LOCALIZATION NOTE (tiltOpenDesc) A very short description of the 'tilt inspect'
+# command. See tiltOpenManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltOpenDesc=Open the Inspector 3D view
+
+# LOCALIZATION NOTE (tiltOpenManual) A fuller description of the 'tilt translate'
+# command, displayed when the user asks for help on what it does.
+tiltOpenManual=Initialize the 3D page inspector and optionally highlight a node using a CSS selector
+
+# LOCALIZATION NOTE (tiltTranslateDesc) A very short description of the 'tilt translate'
+# command. See tiltTranslateManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltTranslateDesc=Move the webpage mesh
+
+# LOCALIZATION NOTE (tiltTranslateManual) A fuller description of the 'tilt translate'
+# command, displayed when the user asks for help on what it does.
+tiltTranslateManual=Incrementally translate the webpage mesh in a certain direction
+
+# LOCALIZATION NOTE (tiltTranslateXDesc) A very short string to describe the
+# 'x' parameter to the 'tilt translate' command, which is displayed in a dialog
+# when the user is using this command.
+tiltTranslateXDesc=X (pixels)
+
+# LOCALIZATION NOTE (tiltTranslateXManual) A fuller description of the 'x'
+# parameter to the 'translate' command, displayed when the user asks for help
+# on what it does.
+tiltTranslateXManual=The ammount in pixels to translate the webpage mesh on the X axis
+
+# LOCALIZATION NOTE (tiltTranslateYDesc) A very short string to describe the
+# 'y' parameter to the 'tilt translate' command, which is displayed in a dialog
+# when the user is using this command.
+tiltTranslateYDesc=Y (pixels)
+
+# LOCALIZATION NOTE (tiltTranslateYManual) A fuller description of the 'y'
+# parameter to the 'translate' command, displayed when the user asks for help
+# on what it does.
+tiltTranslateYManual=The ammount in pixels to translate the webpage mesh on the Y axis
+
+# LOCALIZATION NOTE (tiltRotateDesc) A very short description of the 'tilt rotate'
+# command. See tiltRotateManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltRotateDesc=Spin the webpage mesh
+
+# LOCALIZATION NOTE (tiltRotateManual) A fuller description of the 'tilt rotate'
+# command, displayed when the user asks for help on what it does.
+tiltRotateManual=Incrementally rotate the webpage mesh in a certain direction
+
+# LOCALIZATION NOTE (tiltRotateXDesc) A very short string to describe the
+# 'x' parameter to the 'tilt rotate' command, which is displayed in a dialog
+# when the user is using this command.
+tiltRotateXDesc=X (degrees)
+
+# LOCALIZATION NOTE (tiltRotateXManual) A fuller description of the 'x'
+# parameter to the 'rotate' command, displayed when the user asks for help
+# on what it does.
+tiltRotateXManual=The ammount in degrees to rotate the webpage mesh along the X axis
+
+# LOCALIZATION NOTE (tiltRotateYDesc) A very short string to describe the
+# 'y' parameter to the 'tilt rotate' command, which is displayed in a dialog
+# when the user is using this command.
+tiltRotateYDesc=Y (degrees)
+
+# LOCALIZATION NOTE (tiltRotateYManual) A fuller description of the 'y'
+# parameter to the 'rotate' command, displayed when the user asks for help
+# on what it does.
+tiltRotateYManual=The ammount in degrees to rotate the webpage mesh along the Y axis
+
+# LOCALIZATION NOTE (tiltRotateZDesc) A very short string to describe the
+# 'z' parameter to the 'tilt rotate' command, which is displayed in a dialog
+# when the user is using this command.
+tiltRotateZDesc=Z (degrees)
+
+# LOCALIZATION NOTE (tiltRotateZManual) A fuller description of the 'z'
+# parameter to the 'rotate' command, displayed when the user asks for help
+# on what it does.
+tiltRotateZManual=The ammount in degrees to rotate the webpage mesh along the Z axis
+
+# LOCALIZATION NOTE (tiltZoomDesc) A very short description of the 'tilt zoom'
+# command. See tiltZoomManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltZoomDesc=Move away or towards the webpage mesh
+
+# LOCALIZATION NOTE (tiltZoomManual) A fuller description of the 'tilt zoom'
+# command, displayed when the user asks for help on what it does.
+tiltZoomManual=Incrementally move the webpage mesh in a certain direction along the Z axis
+
+# LOCALIZATION NOTE (tiltZoomAmountDesc) A very short string to describe the
+# 'zoom' parameter to the 'tilt zoom' command, which is displayed in a dialog
+# when the user is using this command.
+tiltZoomAmountDesc=Zoom (pixels)
+
+# LOCALIZATION NOTE (tiltZoomAmmuntManual) A fuller description of the 'zoom'
+# parameter to the 'zoom' command, displayed when the user asks for help
+# on what it does.
+tiltZoomAmountManual=The amount in pixels to translate the webpage mesh along the Z axis
+
+# LOCALIZATION NOTE (tiltResetDesc) A very short description of the 'tilt reset'
+# command. See tiltResetManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltResetDesc=Reset the translation, rotation and zoom
+
+# LOCALIZATION NOTE (tiltResetManual) A fuller description of the 'tilt reset'
+# command, displayed when the user asks for help on what it does.
+tiltResetManual=Resets any transformations applied to the webpage mesh modelview matrix
+
+# LOCALIZATION NOTE (tiltCloseDesc) A very short description of the 'tilt close'
+# command. See tiltCloseManual for a fuller description of what it does. This
+# string is designed to be shown in a menu alongside the command name, which
+# is why it should be as short as possible.
+tiltCloseDesc=Close the visualization if open
+
+# LOCALIZATION NOTE (tiltCloseManual) A fuller description of the 'tilt close'
+# command, displayed when the user asks for help on what it does.
+tiltCloseManual=Close the visualization and switch back to the Inspector default highlighter
+
 # LOCALIZATION NOTE (breakDesc) A very short string used to describe the
 # function of the break command.
 breakDesc=Manage breakpoints
 
 # LOCALIZATION NOTE (breakManual) A longer description describing the
 # set of commands that control breakpoints.
 breakManual=Commands to list, add and remove breakpoints
 
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -15,25 +15,13 @@ confirmNavigationAway.buttonLeave=Leave 
 confirmNavigationAway.buttonLeaveAccesskey=L
 confirmNavigationAway.buttonStay=Stay on Page
 confirmNavigationAway.buttonStayAccesskey=S
 
 breadcrumbs.siblings=Siblings
 # LOCALIZATION NOTE (htmlPanel): Used in the Inspector tool's openInspectorUI
 # method when registering the HTML panel.
 
-# LOCALIZATION NOTE (htmlPanel.label): This is a lable for a button that
-# activates the Web Developer->Inspect UI's HTML Tree Panel.
-htmlPanel.label=HTML
-
-# LOCALIZATION NOTE (htmlPanel.tooltiptext): The text that appears when a user
-# hovers over the HTML panel's toolbar button.
-htmlPanel.tooltiptext=HTML panel
-
-# LOCALIZATION NOTE (htmlPanel.accesskey): The key bound to the HTML panel's
-# toolbar button.
-htmlPanel.accesskey=H
-
 # LOCALIZATION NOTE (ruleView.*): Button label, accesskey and tooltip text
 # associated with the Highlighter's CSS Rule View in the Style Sidebar.
 ruleView.label=Rules
 ruleView.accesskey=R
-ruleView.tooltiptext=View and Edit CSS
\ No newline at end of file
+ruleView.tooltiptext=View and Edit CSS
--- a/browser/themes/gnomestripe/browser.css
+++ b/browser/themes/gnomestripe/browser.css
@@ -1987,56 +1987,47 @@ panel[dimmed="true"] {
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   border-top: 1px solid hsla(210, 8%, 5%, .65);
 }
 
-#inspector-toolbar[treepanel-open] {
-  padding-top: 0;
-}
-
 #devtools-side-splitter {
   -moz-appearance: none;
   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 resizer */
-
-#inspector-top-resizer {
-  -moz-appearance: none;
-  cursor: n-resize;
-  background: none;
-  height: 4px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 html|*#highlighter-nodeinfobar-tagname {
   color: white;
 }
 
 html|*#highlighter-nodeinfobar-id {
   color: hsl(90, 79%, 52%);
 }
 
+html|*#highlighter-nodeinfobar-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 /* Highlighter - Node Infobar - box & arrow */
 
 #highlighter-nodeinfobar {
   color: hsl(200, 100%, 65%);
   border: 1px solid hsla(210, 19%, 63%, .5);
   border-radius: 3px;
   padding: 8px 16px;
   background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box;
@@ -2131,21 +2122,29 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag {
   color: hsl(208,100%,60%);
 }
 
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id {
   color: hsl(205,100%,70%);
 }
 
+.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 .inspector-breadcrumbs-id,
 .inspector-breadcrumbs-classes {
   color: #8d99a6;
 }
 
+.inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 85%);
+}
+
 /* Highlighter toolbar - breadcrumbs - LTR */
 
 .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {
   margin-left: 0;
 }
 
 .inspector-breadcrumbs-button {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 1 13 2 13 fill stretch;
@@ -2255,8 +2254,20 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 1 13 2 13 fill stretch;
 }
 
 .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl),
 .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 1 13 2 13 fill stretch;
 }
+
+/* Highlighter toolbar - HTML Tree */
+
+#inspector-tree-splitter {
+  -moz-appearance: none;
+  border-top: 1px solid black;
+  border-bottom-width: 0;
+  min-height: 3px;
+  height: 3px;
+  margin-bottom: -3px;
+  position: relative;
+}
--- a/browser/themes/gnomestripe/devtools/debugger.css
+++ b/browser/themes/gnomestripe/devtools/debugger.css
@@ -243,16 +243,39 @@ a {
   -moz-margin-end: 5px;
 }
 
 .arrow[open] {
   -moz-appearance: treetwistyopen;
 }
 
 /**
+ * Animations
+ */
+
+.details[open] {
+  -moz-animation-duration: 0.25s;
+  -moz-animation-name: showblock;
+}
+
+@-moz-keyframes showblock {
+  from {
+    opacity: 0;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(0);
+  }
+
+  to {
+    opacity: 1;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(1);
+  }
+}
+
+/**
  * Display helpers
  */
 
 .unselectable {
   padding-top: 2px;
   padding-bottom: 2px;
 }
 
--- a/browser/themes/gnomestripe/inspector.css
+++ b/browser/themes/gnomestripe/inspector.css
@@ -54,17 +54,17 @@ html {
   background-color: -moz-dialog;
 }
 
 body {
   margin: 0;
   overflow: auto;
   font-family: Lucida Grande, sans-serif;
   font-size: 11px;
-  border-top: 1px solid #BBB9BA;
+  padding-top: 5px;
 }
 
 h1 {
   font-size: 17px;
   border-bottom: 1px solid threedlightshadow;
 }
 
 a {
--- a/browser/themes/pinstripe/browser.css
+++ b/browser/themes/pinstripe/browser.css
@@ -2734,58 +2734,47 @@ panel[dimmed="true"] {
   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-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;
   background-color: transparent;
   -moz-margin-end: -3px;
   position: relative;
 }
 
 #devtools-sidebar-box {
   background-color: -moz-Field;
 }
 
-/* Highlighter - toolbar resizer */
-
-#inspector-top-resizer {
-  -moz-appearance: none;
-  cursor: n-resize;
-  background: none;
-  height: 4px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 html|*#highlighter-nodeinfobar-tagname {
   color: white;
 }
 
 html|*#highlighter-nodeinfobar-id {
   color: hsl(90, 79%, 52%);
 }
 
+html|*#highlighter-nodeinfobar-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 /* Highlighter - Node Infobar - box & arrow */
 
 #highlighter-nodeinfobar {
   color: hsl(200, 100%, 65%);
   border: 1px solid hsla(210, 19%, 63%, .5);
   border-radius: 3px;
   padding: 8px 16px;
   background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box;
@@ -2874,21 +2863,29 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag {
   color: hsl(208,100%,60%);
 }
 
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id {
   color: hsl(205,100%,70%);
 }
 
+.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 .inspector-breadcrumbs-id,
 .inspector-breadcrumbs-classes {
   color: #8d99a6;
 }
 
+.inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 85%);
+}
+
 /* Highlighter toolbar - breadcrumbs - LTR */
 
 .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {
   margin-left: 0;
 }
 
 .inspector-breadcrumbs-button {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 1 13 2 13 fill stretch;
@@ -2998,8 +2995,20 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 1 13 2 13 fill stretch;
 }
 
 .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl),
 .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 1 13 2 13 fill stretch;
 }
+
+/* Highlighter toolbar - HTML Tree */
+
+#inspector-tree-splitter {
+  -moz-appearance: none;
+  border-top: 1px solid black;
+  border-bottom-width: 0;
+  min-height: 3px;
+  height: 3px;
+  margin-bottom: -3px;
+  position: relative;
+}
--- a/browser/themes/pinstripe/devtools/debugger.css
+++ b/browser/themes/pinstripe/devtools/debugger.css
@@ -241,16 +241,39 @@ a {
   -moz-appearance: treetwisty;
 }
 
 .arrow[open] {
   -moz-appearance: treetwistyopen;
 }
 
 /**
+ * Animations
+ */
+
+.details[open] {
+  -moz-animation-duration: 0.25s;
+  -moz-animation-name: showblock;
+}
+
+@-moz-keyframes showblock {
+  from {
+    opacity: 0;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(0);
+  }
+
+  to {
+    opacity: 1;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(1);
+  }
+}
+
+/**
  * Display helpers
  */
 
 .unselectable {
   padding-top: 4px;
 }
 
 .info {
--- a/browser/themes/pinstripe/inspector.css
+++ b/browser/themes/pinstripe/inspector.css
@@ -54,17 +54,17 @@ html {
   background-color: -moz-dialog;
 }
 
 body {
   margin: 0;
   overflow: auto;
   font-family: Lucida Grande, sans-serif;
   font-size: 11px;
-  border-top: 1px solid #BBB9BA;
+  padding-top: 5px;
 }
 
 h1 {
   font-size: 17px;
   border-bottom: 1px solid threedlightshadow;
 }
 
 a {
--- a/browser/themes/winstripe/browser.css
+++ b/browser/themes/winstripe/browser.css
@@ -2682,55 +2682,46 @@ panel[dimmed="true"] {
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   border-top: 1px solid hsla(211,68%,6%,.65) !important;
 }
 
-#inspector-toolbar[treepanel-open] {
-  padding-top: 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 resizer */
-
-#inspector-top-resizer {
-  -moz-appearance: none;
-  cursor: n-resize;
-  background: none;
-  height: 4px;
-}
-
 /* Highlighter - Node Infobar */
 
 /* Highlighter - Node Infobar - text */
 
 html|*#highlighter-nodeinfobar-tagname {
   color: white;
 }
 
 html|*#highlighter-nodeinfobar-id {
   color: hsl(90, 79%, 52%);
 }
 
+html|*#highlighter-nodeinfobar-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 /* Highlighter - Node Infobar - box & arrow */
 
 #highlighter-nodeinfobar {
   color: hsl(200, 100%, 65%);
   border: 1px solid hsla(210, 19%, 63%, .5);
   border-radius: 3px;
   padding: 8px 16px;
   background: -moz-linear-gradient(hsl(209, 18%, 30%), hsl(210, 24%, 16%)) no-repeat padding-box;
@@ -2825,21 +2816,29 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-tag {
   color: hsl(200,100%,60%);
 }
 
 .inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-id {
   color: hsl(200,100%,70%);
 }
 
+.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 70%);
+}
+
 .inspector-breadcrumbs-id,
 .inspector-breadcrumbs-classes {
   color: #8d99a6;
 }
 
+.inspector-breadcrumbs-pseudo-classes {
+  color: hsl(20, 100%, 85%);
+}
+
 /* Highlighter toolbar - breadcrumbs - LTR */
 
 .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {
   margin-left: 0;
 }
 
 .inspector-breadcrumbs-button {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/ltr-middle.png") 2 13 2 13 fill stretch;
@@ -2949,8 +2948,20 @@ html|*#highlighter-nodeinfobar-id {
 .inspector-breadcrumbs-button:last-of-type[checked]:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected.png") 2 13 2 13 fill stretch;
 }
 
 .inspector-breadcrumbs-button[siblings-menu-open]:last-of-type[checked]:-moz-locale-dir(rtl),
 .inspector-breadcrumbs-button:last-of-type[checked]:hover:active:-moz-locale-dir(rtl) {
   -moz-border-image: url("chrome://browser/skin/devtools/breadcrumbs/rtl-end-selected-pressed.png") 2 13 2 13 fill stretch;
 }
+
+/* Highlighter toolbar - HTML Tree */
+
+#inspector-tree-splitter {
+  -moz-appearance: none;
+  border-top: 1px solid black;
+  border-bottom-width: 0;
+  min-height: 3px;
+  height: 3px;
+  margin-bottom: -3px;
+  position: relative;
+}
--- a/browser/themes/winstripe/devtools/debugger.css
+++ b/browser/themes/winstripe/devtools/debugger.css
@@ -245,16 +245,39 @@ a {
   background: url("chrome://global/skin/tree/twisty-clsd.png") center center no-repeat;
 }
 
 .arrow[open] {
   background-image: url("chrome://global/skin/tree/twisty-open.png");
 }
 
 /**
+ * Animations
+ */
+
+.details[open] {
+  -moz-animation-duration: 0.25s;
+  -moz-animation-name: showblock;
+}
+
+@-moz-keyframes showblock {
+  from {
+    opacity: 0;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(0);
+  }
+
+  to {
+    opacity: 1;
+    -moz-transform-origin: top;
+    -moz-transform: scaleY(1);
+  }
+}
+
+/**
  * Display helpers
  */
 
 .unselectable {
   padding-top: 2px;
   padding-bottom: 2px;
 }
 
--- a/browser/themes/winstripe/inspector.css
+++ b/browser/themes/winstripe/inspector.css
@@ -54,16 +54,17 @@ html {
   background-color: -moz-dialog;
 }
 
 body {
   margin: 0;
   overflow: auto;
   font-family: Lucida Grande, sans-serif;
   font-size: 11px;
+  padding-top: 5px;
 }
 
 h1 {
   font-size: 17px;
   border-bottom: 1px solid threedlightshadow;
 }
 
 a {
--- a/build/automation.py.in
+++ b/build/automation.py.in
@@ -749,17 +749,17 @@ user_pref("camino.use_system_proxy_setti
         # We should have a "crashinject" program in our utility path
         crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
         if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
           return
       #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
     self.log.info("Can't trigger Breakpad, just killing process")
     proc.kill()
 
-  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
+  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger):
     """ Look for timeout or crashes and return the status after the process terminates """
     stackFixerProcess = None
     stackFixerFunction = None
     didTimeout = False
     hitMaxTime = False
     if proc.stdout is None:
       self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
     else:
@@ -783,16 +783,18 @@ user_pref("camino.use_system_proxy_setti
         # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
         stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
                                          stdin=logsource,
                                          stdout=subprocess.PIPE)
         logsource = stackFixerProcess.stdout
 
       (line, didTimeout) = self.readWithTimeout(logsource, timeout)
       while line != "" and not didTimeout:
+        if logger:
+          logger.log(line)
         if "TEST-START" in line and "|" in line:
           self.lastTestSeen = line.split("|")[1].strip()
         if stackFixerFunction:
           line = stackFixerFunction(line)
         self.log.info(line.rstrip().decode("UTF-8", "ignore"))
         if not debuggerInfo and not self.haveDumpedScreen and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
           self.dumpScreen(utilityPath)
 
@@ -872,17 +874,17 @@ user_pref("camino.use_system_proxy_setti
           self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
           self.killPid(processPID)
 
   def checkForCrashes(self, profileDir, symbolsPath):
     automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath, self.lastTestSeen)
 
   def runApp(self, testURL, env, app, profileDir, extraArgs,
              runSSLTunnel = False, utilityPath = None,
-             xrePath = None, certPath = None,
+             xrePath = None, certPath = None, logger = None,
              debuggerInfo = None, symbolsPath = None,
              timeout = -1, maxTime = None):
     """
     Run the app, log the duration it took to execute, return the status code.
     Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
     """
 
     if utilityPath == None:
@@ -931,17 +933,17 @@ user_pref("camino.use_system_proxy_setti
     self.lastTestSeen = "automation.py"
     proc = self.Process([cmd] + args,
                  env = self.environment(env, xrePath = xrePath,
                                    crashreporter = not debuggerInfo),
                  stdout = outputPipe,
                  stderr = subprocess.STDOUT)
     self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
 
-    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
+    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, logger)
     self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
 
     # Do a final check for zombie child processes.
     self.checkForZombies(processLog)
     self.checkForCrashes(profileDir, symbolsPath)
 
     if os.path.exists(processLog):
       os.unlink(processLog)
--- a/build/automationutils.py
+++ b/build/automationutils.py
@@ -35,28 +35,30 @@
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK ***** */
 
 from __future__ import with_statement
 import glob, logging, os, platform, shutil, subprocess, sys, tempfile, urllib2, zipfile
 import re
 from urlparse import urlparse
+from operator import itemgetter
 
 __all__ = [
   "ZipFileReader",
   "addCommonOptions",
   "checkForCrashes",
   "dumpLeakLog",
   "isURL",
   "processLeakLog",
   "getDebuggerInfo",
   "DEBUGGER_INFO",
   "replaceBackSlashes",
   "wrapCommand",
+  "ShutdownLeakLogger"
   ]
 
 # Map of debugging programs to information about them, like default arguments
 # and whether or not they are interactive.
 DEBUGGER_INFO = {
   # gdb requires that you supply the '--args' flag in order to pass arguments
   # after the executable name to the executable.
   "gdb": {
@@ -445,8 +447,111 @@ def wrapCommand(cmd):
   binary.
   """
   if platform.system() == "Darwin" and \
      hasattr(platform, 'mac_ver') and \
      platform.mac_ver()[0][:4] < '10.6':
     return ["arch", "-arch", "i386"] + cmd
   # otherwise just execute the command normally
   return cmd
+
+class ShutdownLeakLogger(object):
+  """
+  Parses the mochitest run log when running a debug build, assigns all leaked
+  DOM windows (that are still around after test suite shutdown, despite running
+  the GC) to the tests that created them and prints leak statistics.
+  """
+  MAX_LEAK_COUNT = 120
+
+  def __init__(self, logger):
+    self.logger = logger
+    self.tests = []
+    self.leakedWindows = {}
+    self.leakedDocShells = set()
+    self.currentTest = None
+    self.seenShutdown = False
+
+  def log(self, line):
+    if line[2:11] == "DOMWINDOW":
+      self._logWindow(line)
+    elif line[2:10] == "DOCSHELL":
+      self._logDocShell(line)
+    elif line.startswith("TEST-START"):
+      fileName = line.split(" ")[-1].strip().replace("chrome://mochitests/content/browser/", "")
+      self.currentTest = {"fileName": fileName, "windows": set(), "docShells": set()}
+    elif line.startswith("INFO TEST-END"):
+      # don't track a test if no windows or docShells leaked
+      if self.currentTest["windows"] and self.currentTest["docShells"]:
+        self.tests.append(self.currentTest)
+      self.currentTest = None
+    elif line.startswith("INFO TEST-START | Shutdown"):
+      self.seenShutdown = True
+
+  def parse(self):
+    leakingTests = self._parseLeakingTests()
+
+    if leakingTests:
+      totalWindows = sum(len(test["leakedWindows"]) for test in leakingTests)
+      totalDocShells = sum(len(test["leakedDocShells"]) for test in leakingTests)
+      msgType = "INFO" if totalWindows + totalDocShells < self.MAX_LEAK_COUNT else "UNEXPECTED-FAIL"
+      self.logger.info("TEST-%s | ShutdownLeaks | leaked %d DOMWindow(s) and %d DocShell(s) until shutdown", msgType, totalWindows, totalDocShells)
+
+    for test in leakingTests:
+      self.logger.info("\n[%s]", test["fileName"])
+
+      for url, count in self._zipLeakedWindows(test["leakedWindows"]):
+        self.logger.info("  %d window(s) [url = %s]", count, url)
+
+      if test["leakedDocShells"]:
+        self.logger.info("  %d docShell(s)", len(test["leakedDocShells"]))
+
+  def _logWindow(self, line):
+    created = line[:2] == "++"
+    id = self._parseValue(line, "serial")
+
+    if self.currentTest:
+      windows = self.currentTest["windows"]
+      if created:
+        windows.add(id)
+      else:
+        windows.discard(id)
+    elif self.seenShutdown and not created:
+      self.leakedWindows[id] = self._parseValue(line, "url")
+
+  def _logDocShell(self, line):
+    created = line[:2] == "++"
+    id = self._parseValue(line, "id")
+
+    if self.currentTest:
+      docShells = self.currentTest["docShells"]
+      if created:
+        docShells.add(id)
+      else:
+        docShells.discard(id)
+    elif self.seenShutdown and not created:
+      self.leakedDocShells.add(id)
+
+  def _parseValue(self, line, name):
+    return re.search("\[%s = (.+?)\]" % name, line).group(1)
+
+  def _parseLeakingTests(self):
+    leakingTests = []
+
+    for test in self.tests:
+      test["leakedWindows"] = [self.leakedWindows[id] for id in test["windows"] if id in self.leakedWindows]
+      test["leakedDocShells"] = [id for id in test["docShells"] if id in self.leakedDocShells]
+      test["leakCount"] = len(test["leakedWindows"]) + len(test["leakedDocShells"])
+
+      if test["leakCount"]:
+        leakingTests.append(test)
+
+    return sorted(leakingTests, key=itemgetter("leakCount"), reverse=True)
+
+  def _zipLeakedWindows(self, leakedWindows):
+    counts = []
+    counted = set()
+
+    for url in leakedWindows:
+      if not url in counted:
+        counts.append((url, leakedWindows.count(url)))
+        counted.add(url)
+
+    return sorted(counts, key=itemgetter(1), reverse=True)
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -91,17 +91,17 @@ class RemoteAutomation(Automation):
         if crashreporter:
             env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
             env['MOZ_CRASHREPORTER'] = '1'
         else:
             env['MOZ_CRASHREPORTER_DISABLE'] = '1'
 
         return env
 
-    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir):
+    def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsDir, logger):
         # maxTime is used to override the default timeout, we should honor that
         status = proc.wait(timeout = maxTime)
 
         print proc.stdout
 
         if (status == 1 and self._devicemanager.processExist(proc.procName)):
             # Then we timed out, make sure Fennec is dead
             proc.kill()
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -780,17 +780,18 @@ nsDocShell::nsDocShell():
     if (gDocShellLeakLog)
         PR_LOG(gDocShellLeakLog, PR_LOG_DEBUG, ("DOCSHELL %p created\n", this));
 #endif
 
 #ifdef DEBUG
   // We're counting the number of |nsDocShells| to help find leaks
   ++gNumberOfDocShells;
   if (!PR_GetEnv("MOZ_QUIET")) {
-      printf("++DOCSHELL %p == %ld\n", (void*) this, gNumberOfDocShells);
+      printf("++DOCSHELL %p == %ld [id = %ld]\n", (void*) this,
+             gNumberOfDocShells, mHistoryID);
   }
 #endif
 }
 
 nsDocShell::~nsDocShell()
 {
     Destroy();
 
@@ -808,17 +809,18 @@ nsDocShell::~nsDocShell()
     if (gDocShellLeakLog)
         PR_LOG(gDocShellLeakLog, PR_LOG_DEBUG, ("DOCSHELL %p destroyed\n", this));
 #endif
 
 #ifdef DEBUG
     // We're counting the number of |nsDocShells| to help find leaks
     --gNumberOfDocShells;
     if (!PR_GetEnv("MOZ_QUIET")) {
-        printf("--DOCSHELL %p == %ld\n", (void*) this, gNumberOfDocShells);
+        printf("--DOCSHELL %p == %ld [id = %ld]\n", (void*) this,
+               gNumberOfDocShells, mHistoryID);
     }
 #endif
 }
 
 nsresult
 nsDocShell::Init()
 {
     nsresult rv = nsDocLoader::Init();
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -670,43 +670,54 @@ class Mochitest(object):
     # then again to actually run mochitest
     if options.timeout:
       timeout = options.timeout + 30
     elif not options.autorun:
       timeout = None
     else:
       timeout = 330.0 # default JS harness timeout is 300 seconds
 
+    # it's a debug build, we can parse leaked DOMWindows and docShells
+    if Automation.IS_DEBUG_BUILD:
+      logger = ShutdownLeakLogger(self.automation.log)
+    else:
+      logger = None
+
     if options.vmwareRecording:
       self.startVMwareRecording(options);
 
     self.automation.log.info("INFO | runtests.py | Running tests: start.\n")
     try:
       status = self.automation.runApp(testURL, browserEnv, options.app,
                                   options.profilePath, options.browserArgs,
                                   runSSLTunnel = self.runSSLTunnel,
                                   utilityPath = options.utilityPath,
                                   xrePath = options.xrePath,
                                   certPath=options.certPath,
                                   debuggerInfo=debuggerInfo,
                                   symbolsPath=options.symbolsPath,
+                                  logger = logger,
                                   timeout = timeout)
     except KeyboardInterrupt:
       self.automation.log.info("INFO | runtests.py | Received keyboard interrupt.\n");
       status = -1
     except:
       self.automation.log.info("INFO | runtests.py | Received unexpected exception while running application '%s'\n" % (sys.exc_info()[1]))
       status = 1
 
     if options.vmwareRecording:
       self.stopVMwareRecording();
 
     self.stopWebServer(options)
     self.stopWebSocketServer(options)
     processLeakLog(self.leak_report_file, options.leakThreshold)
+
+    if logger:
+      logger.parse()
+
     self.automation.log.info("\nINFO | runtests.py | Running tests: end.")
 
     if manifest is not None:
       self.cleanup(manifest, options)
     return status
 
   def makeTestConfig(self, options):
     "Creates a test configuration file for customizing test execution."
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -629,20 +629,25 @@
 
                 terminateEventListeners : function () {
                     if (this.statsInterval) {
                         clearInterval(this.statsInterval);
                         this.statsInterval = null;
                     }
                     for each (let event in this.videoEvents)
                         this.video.removeEventListener(event, this, false);
+
+                    for each(let element in this.controlListeners)
+                        element.item.removeEventListener(element.event, element.func, false);
+                    
+                    delete this.controlListeners;
+
                     this.video.removeEventListener("media-showStatistics", this._handleCustomEventsBound, false);
                     delete this._handleCustomEventsBound;
-                    this.video.ownerDocument.removeEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false);
-                    delete this._setFullscreenButtonStateBound;
+
                     this.log("--- videocontrols terminated ---");
                 },
 
                 hasError : function () {
                     return (this.video.error != null || this.video.networkState == this.video.NETWORK_NO_SOURCE);
                 },
 
                 updateErrorText : function () {
@@ -926,16 +931,17 @@
                     element.hidden = true;
                 },
 
                 _triggeredByControls: false,
 
                 togglePause : function () {
                     if (this.video.paused || this.video.ended) {
                         this._triggeredByControls = true;
+                        this.hideClickToPlay();
                         this.video.play();
                     } else {
                         this.video.pause();
                     }
 
                     // We'll handle style changes in the event listener for
                     // the "play" and "pause" events, same as if content
                     // script was controlling video playback.
@@ -970,32 +976,43 @@
                     this.fullscreenButton.setAttribute("aria-label", value);
 
                     if (this.isVideoInFullScreen())
                         this.fullscreenButton.setAttribute("fullscreened", "true");
                     else
                         this.fullscreenButton.removeAttribute("fullscreened");
                 },
 
-                handleClickToPlay : function () {
+                clickToPlayClickHandler : function(e) {
+                    if (e.button != 0 || this.hasError())
+                        return;
+                    // Read defaultPrevented asynchronously, since Web content
+                    // may want to consume the "click" event but will only
+                    // receive it after us.
+                    let self = this;
+                    setTimeout(function clickToPlayCallback() {
+                        if (!e.defaultPrevented)
+                            self.togglePause();
+                    }, 0);
+                },
+                hideClickToPlay : function () {
                     let videoHeight = this.video.clientHeight;
                     let videoWidth = this.video.clientWidth;
 
                     // The play button will animate to 3x its size. This
                     // shows the animation unless the video is too small
                     // to show 2/3 of the animation.
                     let animationScale = 2;
                     if (this._overlayPlayButtonHeight * animationScale > (videoHeight - this._controlBarHeight)||
                         this._overlayPlayButtonWidth * animationScale > videoWidth) {
                         this.clickToPlay.setAttribute("immediate", "true");
                     } else {
                         this.clickToPlay.removeAttribute("immediate");
                     }
                     this.clickToPlay.setAttribute("fadeout", "true");
-                    this.togglePause();
                 },
 
                 setPlayButtonState : function(aPaused) {
                   if (aPaused)
                       this.playButton.setAttribute("paused", "true");
                   else
                       this.playButton.removeAttribute("paused");
 
@@ -1325,57 +1342,46 @@
 
                     // Use the handleEvent() callback for all media events.
                     // The "error" event listener must capture, so that it can trap error events
                     // from the <source> children, which don't bubble.
                     for each (let event in this.videoEvents)
                         this.video.addEventListener(event, this, (event == "error") ? true : false);
 
                     var self = this;
-                    this.muteButton.addEventListener("command", function() { self.toggleMute(); }, false);
-                    this.playButton.addEventListener("command", function() { self.togglePause(); }, false);
-                    this.fullscreenButton.addEventListener("command", function() { self.toggleFullscreen(); }, false );
-                    this.clickToPlay.addEventListener("click", function clickToPlayClickHandler(e) {
-                      if (e.button != 0 || self.hasError())
-                        return;
-                      // Read defaultPrevented asynchronously, since Web content
-                      // may want to consume the "click" event but will only
-                      // receive it after us.
-                      setTimeout(function clickToPlayCallback() {
-                        if (!e.defaultPrevented)
-                          self.handleClickToPlay();
-                      }, 0);
-                    }, false);
+
+                    this.controlListeners = [];
 
-                    this.controlsSpacer.addEventListener("click", function spacerClickHandler(e) {
-                      if (e.button != 0 || self.hasError())
-                        return;
-                      // Read defaultPrevented asynchronously, since Web content
-                      // may want to consume the "click" event but will only
-                      // receive it after us (bug 693014).
-                      setTimeout(function togglePauseCallback() {
-                        if (!e.defaultPrevented)
-                          self.togglePause();
-                      }, 0);
-                    }, false);
+                    // Helper function to add an event listener to the given element
+                    function addListener(elem, eventName, func) {
+                      let boundFunc = func.bind(self);
+                      self.controlListeners.push({ item: elem, event: eventName, func: boundFunc });
+                      elem.addEventListener(eventName, boundFunc, false);
+                    }
+
+                    addListener(this.muteButton, "command", this.toggleMute);
+                    addListener(this.playButton, "command", this.togglePause);
+                    addListener(this.fullscreenButton, "command", this.toggleFullscreen);
+                    addListener(this.clickToPlay, "click", this.clickToPlayClickHandler);
+                    addListener(this.controlsSpacer, "click", this.clickToPlayClickHandler);
 
                     if (!this.isAudioOnly) {
-                      this.muteButton.addEventListener("mouseover",  function(e) { self.onVolumeMouseInOut(e); }, false);
-                      this.muteButton.addEventListener("mouseout",   function(e) { self.onVolumeMouseInOut(e); }, false);
-                      this.volumeStack.addEventListener("mouseover", function(e) { self.onVolumeMouseInOut(e); }, false);
-                      this.volumeStack.addEventListener("mouseout",  function(e) { self.onVolumeMouseInOut(e); }, false);
+                      addListener(this.muteButton, "mouseover", this.onVolumeMouseInOut);
+                      addListener(this.muteButton, "mouseout", this.onVolumeMouseInOut);
+                      addListener(this.volumeStack, "mouseover", this.onVolumeMouseInOut);
+                      addListener(this.volumeStack, "mouseout", this.onVolumeMouseInOut);
                     }
 
-                    this.videocontrols.addEventListener("transitionend", function(e) { self.onTransitionEnd(e); }, false);
-                    this._setFullscreenButtonStateBound = this.setFullscreenButtonState.bind(this);
-                    this.video.ownerDocument.addEventListener("mozfullscreenchange", this._setFullscreenButtonStateBound, false);
+                    addListener(this.videocontrols, "transitionend", this.onTransitionEnd);
+                    addListener(this.video.ownerDocument, "mozfullscreenchange", this.setFullscreenButtonState);
 
                     // Make the <video> element keyboard accessible.
                     this.video.setAttribute("tabindex", 0);
-                    this.video.addEventListener("keypress", function (e) { self.keyHandler(e) }, false);
+
+                    addListener(this.video, "keypress", this.keyHandler);
 
                     this.log("--- videocontrols initialized ---");
                 }
             }) ]]>
         </field>
 
         <field readonly="true" name="isTouchControl">false</field>
 
--- a/toolkit/devtools/debugger/server/dbg-browser-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-browser-actors.js
@@ -62,24 +62,29 @@ function createRootActor(aConnection)
 function BrowserRootActor(aConnection)
 {
   this.conn = aConnection;
   this._tabActors = new WeakMap();
   this._tabActorPool = null;
   this._actorFactories = null;
 
   this.onTabClosed = this.onTabClosed.bind(this);
+  this._onWindowCreated = this.onWindowCreated.bind(this);
   windowMediator.addListener(this);
 }
 
 BrowserRootActor.prototype = {
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function BRA_sayHello() {
+    // Create the tab actor for the selected tab right away so that it gets a
+    // chance to listen to onNewScript notifications.
+    this._preInitTabActor();
+
     return { from: "root",
              applicationType: "browser",
              traits: [] };
   },
 
   /**
    * Disconnects the actor from the browser window.
    */
@@ -109,20 +114,16 @@ BrowserRootActor.prototype = {
     let actorList = [];
 
     // Walk over open browser windows.
     let e = windowMediator.getEnumerator("navigator:browser");
     let selected;
     while (e.hasMoreElements()) {
       let win = e.getNext();
 
-      // Watch the window for tab closes so we can invalidate
-      // actors as needed.
-      this.watchWindow(win);
-
       // List the tabs in this browser.
       let selectedBrowser = win.getBrowser().selectedBrowser;
       let browsers = win.getBrowser().browsers;
       for each (let browser in browsers) {
         if (browser == selectedBrowser) {
           selected = actorList.length;
         }
         let actor = this._tabActors.get(browser);
@@ -174,33 +175,78 @@ BrowserRootActor.prototype = {
    * When a tab is closed, exit its tab actor.  The actor
    * will be dropped at the next listTabs request.
    */
   onTabClosed: function BRA_onTabClosed(aEvent) {
     this.exitTabActor(aEvent.target.linkedBrowser);
   },
 
   /**
+   * Handle location changes, by preinitializing a tab actor.
+   */
+  onWindowCreated: function BRA_onWindowCreated(evt) {
+    if (evt.target === this.browser.contentDocument) {
+      this._preInitTabActor();
+    }
+  },
+
+  /**
    * Exit the tab actor of the specified tab.
    */
   exitTabActor: function BRA_exitTabActor(aWindow) {
+    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
     let actor = this._tabActors.get(aWindow);
     if (actor) {
       actor.exit();
     }
   },
 
+  /**
+   * Create the tab actor in the selected tab right away so that it gets a
+   * chance to listen to onNewScript notifications.
+   */
+  _preInitTabActor: function BRA__preInitTabActor() {
+    let actorPool = new ActorPool(this.conn);
+
+    // Walk over open browser windows.
+    let e = windowMediator.getEnumerator("navigator:browser");
+    while (e.hasMoreElements()) {
+      let win = e.getNext();
+
+      // Watch the window for tab closes so we can invalidate
+      // actors as needed.
+      this.watchWindow(win);
+
+      this.browser = win.getBrowser().selectedBrowser;
+      let actor = this._tabActors.get(this.browser);
+      if (actor) {
+        actor._detach();
+      }
+      actor = new BrowserTabActor(this.conn, this.browser);
+      actor.parentID = this.actorID;
+      this._tabActors.set(this.browser, actor);
+
+      actorPool.addActor(actor);
+    }
+
+    this._tabActorPool = actorPool;
+    this.conn.addActorPool(this._tabActorPool);
+
+    // Watch for globals being created in this tab.
+    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
+  },
+
   // nsIWindowMediatorListener
   onWindowTitleChange: function BRA_onWindowTitleChange(aWindow, aTitle) { },
   onOpenWindow: function BRA_onOpenWindow(aWindow) { },
   onCloseWindow: function BRA_onCloseWindow(aWindow) {
     if (aWindow.getBrowser) {
       this.unwatchWindow(aWindow);
     }
-  },
+  }
 }
 
 /**
  * The request types this actor can handle.
  */
 BrowserRootActor.prototype.requestTypes = {
   "listTabs": BrowserRootActor.prototype.onListTabs
 };
@@ -215,16 +261,17 @@ BrowserRootActor.prototype.requestTypes 
  *        The browser instance that contains this tab.
  */
 function BrowserTabActor(aConnection, aBrowser)
 {
   this.conn = aConnection;
   this._browser = aBrowser;
 
   this._onWindowCreated = this.onWindowCreated.bind(this);
+  this._attach();
 }
 
 // XXX (bug 710213): BrowserTabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 BrowserTabActor.prototype = {
   get browser() { return this._browser; },
 
@@ -285,17 +332,17 @@ BrowserTabActor.prototype = {
     dbg_assert(!this._tabPool, "Shouldn't have a tab pool if we weren't attached.");
     this._tabPool = new ActorPool(this.conn);
     this.conn.addActorPool(this._tabPool);
 
     // ... and a pool for context-lifetime actors.
     this._pushContext();
 
     // Watch for globals being created in this tab.
-    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
+    this.browser.addEventListener("DOMWindowCreated", this._onWindowCreated, false);
 
     this._attached = true;
   },
 
   /**
    * Creates a thread actor and a pool for context-lifetime actors. It then sets
    * up the content window for debugging.
    */
@@ -326,17 +373,17 @@ BrowserTabActor.prototype = {
   /**
    * Does the actual work of detaching from a tab.
    */
   _detach: function BTA_detach() {
     if (!this.attached) {
       return;
     }
 
-    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, true);
+    this.browser.removeEventListener("DOMWindowCreated", this._onWindowCreated, false);
 
     this._popContext();
 
     // Shut down actors that belong to this tab's pool.
     this.conn.removeActorPool(this._tabPool);
     this._tabPool = null;
 
     this._attached = false;
@@ -392,18 +439,17 @@ BrowserTabActor.prototype = {
    */
   onWindowCreated: function BTA_onWindowCreated(evt) {
     if (evt.target === this.browser.contentDocument) {
       if (this._attached) {
         this.conn.send({ from: this.actorID, type: "tabNavigated",
                          url: this.browser.contentDocument.URL });
       }
     }
-  },
-
+  }
 };
 
 /**
  * The request types this actor can handle.
  */
 BrowserTabActor.prototype.requestTypes = {
   "attach": BrowserTabActor.prototype.onAttach,
   "detach": BrowserTabActor.prototype.onDetach
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -94,22 +94,29 @@ ThreadActor.prototype = {
     // for aGlobal's compartment.  Ideally this won't be necessary
     // medium- to long-term, and will be managed by the engine
     // instead.
 
     if (!this._dbg) {
       this._dbg = new Debugger();
     }
 
+    // TODO: Remove this horrible hack when bug 723563 is fixed.
+    // Make sure that a chrome window is not added as a debuggee when opening
+    // the debugger in an empty tab or during tests.
+    if (aGlobal.location &&
+        (aGlobal.location.protocol == "about:" ||
+         aGlobal.location.protocol == "chrome:")) {
+      return;
+    }
+
     this.dbg.addDebuggee(aGlobal);
     this.dbg.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
     this.dbg.onDebuggerStatement = this.onDebuggerStatement.bind(this);
     this.dbg.onNewScript = this.onNewScript.bind(this);
-    // Keep the debugger disabled until a client attaches.
-    this.dbg.enabled = false;
   },
 
   /**
    * Remove a debuggee global from the JSInspector.
    */
   removeDebugee: function TA_removeDebuggee(aGlobal) {
     try {
       this.dbg.removeDebuggee(aGlobal);
--- a/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
@@ -141,16 +141,17 @@ add_test(function() {
 
     check_contextmenu(false, true, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on enabled extension item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   isnot(el, null, "Should have found addon element");
   el.mAddon.userDisabled = true;
@@ -160,16 +161,17 @@ add_test(function() {
 
     check_contextmenu(false, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on newly disabled extension item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   isnot(el, null, "Should have found addon element");
   el.mAddon.userDisabled = false;
@@ -179,16 +181,17 @@ add_test(function() {
 
     check_contextmenu(false, true, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on newly enabled extension item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
 
   gContextMenu.addEventListener("popupshown", function() {
@@ -196,36 +199,38 @@ add_test(function() {
 
     check_contextmenu(false, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on disabled extension item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   gManagerWindow.loadView("addons://list/theme");
   wait_for_view_load(gManagerWindow, function() {
     var el = get_addon_element(gManagerWindow, "theme1@tests.mozilla.org");
 
     gContextMenu.addEventListener("popupshown", function() {
       gContextMenu.removeEventListener("popupshown", arguments.callee, false);
 
-    check_contextmenu(true, true, false, false, false);
+      check_contextmenu(true, true, false, false, false);
 
       gContextMenu.hidePopup();
       run_next_test();
     }, false);
 
     info("Opening context menu on enabled theme item");
+    el.parentNode.ensureElementIsVisible(el);
     EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
     EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
   });
 });
 
 
 add_test(function() {
   var el = get_addon_element(gManagerWindow, "theme2@tests.mozilla.org");
@@ -235,16 +240,17 @@ add_test(function() {
 
     check_contextmenu(true, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on disabled theme item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   LightweightThemeManager.currentTheme = gLWTheme;
 
@@ -255,16 +261,17 @@ add_test(function() {
 
     check_contextmenu(true, true, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on enabled LW theme item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   LightweightThemeManager.currentTheme = null;
 
@@ -275,16 +282,17 @@ add_test(function() {
 
     check_contextmenu(true, false, false, false, false);
 
     gContextMenu.hidePopup();
     run_next_test();
   }, false);
 
   info("Opening context menu on disabled LW theme item");
+  el.parentNode.ensureElementIsVisible(el);
   EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
   EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 });
 
 
 add_test(function() {
   LightweightThemeManager.currentTheme = gLWTheme;
 
@@ -462,16 +470,17 @@ add_test(function() {
 
         check_contextmenu(false, false, true, false, false);
 
         gContextMenu.hidePopup();
         run_next_test();
       }, false);
 
       info("Opening context menu on remote extension item");
+      el.parentNode.ensureElementIsVisible(el);
       EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
       EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
 
     });
   });
 });
 
 
--- a/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js
@@ -8,16 +8,22 @@ function test() {
     gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
     waitForFocus(page_loaded, gBrowser.contentWindow);
   }, true);
   gBrowser.loadURI(TESTROOT + "bug638292.html");
 }
 
 function check_load(aCallback) {
   gBrowser.addEventListener("load", function(aEvent) {
+    // SeaMonkey needs to deal with intermediate "about:blank" document(s).
+    if (!aEvent.target.location) {
+      info("Ignoring about:blank load. (Expected (a few times) on SeaMonkey only.)");
+      return;
+    }
+
     gBrowser.removeEventListener("load", arguments.callee, true);
 
     // Let the load handler complete
     executeSoon(function() {
       var doc = gBrowser.browsers[2].contentDocument;
       is(doc.getElementById("enabled").textContent, "true", "installTrigger should have been enabled");
 
       // Focus the old tab
--- a/toolkit/themes/pinstripe/global/media/videocontrols.css
+++ b/toolkit/themes/pinstripe/global/media/videocontrols.css
@@ -72,17 +72,17 @@
   min-width: 16px;
   min-height: 11px;
 }
 
 .volumeBackgroundBar {
   /* margin left/right: make bar 8px wide (control width = 28, minus 2 * 10 margin) */
   margin: 0 10px;
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px 4px;
+  border-radius: 2.5px;
 }
 
 .durationBox {
   -moz-box-pack: center;
 }
 
 .durationLabel {
   margin-left: -22px; /* 1/2 of scrubber thumb width, for overhang. */
@@ -97,17 +97,17 @@
   display: none;
 }
 
 .backgroundBar {
   /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */
   /* margin left/right: 1/2 of scrubber thumb width, for overhang. */
   margin: 10px 22px;
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px;
+  border-radius: 2.5px;
 }
 
 .bufferBar,
 .progressBar {
   /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */
   /* margin left/right: 1/2 of scrubber thumb width, for overhang. */
   margin: 10px 22px;
   -moz-appearance: none;
@@ -117,22 +117,22 @@
 /* .progress-bar is an element inside the <progressmeter> implementation. */
 .bufferBar .progress-bar {
   /*
    * Note that this is drawn on top of the .backgroundBar. So although this
    * has the same background-color specified, the semitransparent
    * compositing gives it a different visual appearance.
    */
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px;
+  border-radius: 2.5px;
 }
 
 .progressBar .progress-bar {
   background-color: white;
-  border-radius: 4px 0 0 4px;
+  border-radius: 2.5px;
 }
 
 /* .scale-slider is an element inside the <scale> implementation. */
 .scrubber .scale-slider,
 .volumeControl .scale-slider {
   /* Hide the default horizontal bar. */
   -moz-appearance: none;
   background: none;
--- a/toolkit/themes/winstripe/global/media/videocontrols.css
+++ b/toolkit/themes/winstripe/global/media/videocontrols.css
@@ -74,17 +74,17 @@
   min-width: 16px;
   min-height: 11px;
 }
 
 .volumeBackgroundBar {
   /* margin left/right: make bar 8px wide (control width = 28, minus 2 * 10 margin) */
   margin: 0 10px;
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px 4px;
+  border-radius: 2.5px;
 }
 
 .durationBox {
   -moz-box-pack: center;
 }
 
 .durationLabel {
   margin-left: -22px; /* 1/2 of scrubber thumb width, for overhang. */
@@ -99,17 +99,17 @@
   display: none;
 }
 
 .backgroundBar {
   /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */
   /* margin left/right: 1/2 of scrubber thumb width, for overhang. */
   margin: 10px 22px;
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px;
+  border-radius: 2.5px;
 }
 
 .bufferBar,
 .progressBar {
   /* margin top/bottom: make bar 8px tall (control height = 28, minus 2 * 10 margin) */
   /* margin left/right: 1/2 of scrubber thumb width, for overhang. */
   margin: 10px 22px;
   -moz-appearance: none;
@@ -122,23 +122,23 @@
 /* .progress-bar is an element inside the <progressmeter> implementation. */
 .bufferBar .progress-bar {
   /*
    * Note that this is drawn on top of the .backgroundBar. So although this
    * has the same background-color specified, the semitransparent
    * compositing gives it a different visual appearance.
    */
   background-color: rgba(255,255,255,.5);
-  border-radius: 4px;
+  border-radius: 2.5px;
   -moz-appearance: none;
 }
 
 .progressBar .progress-bar {
   background-color: white;
-  border-radius: 4px 0 0 4px;
+  border-radius: 2.5px;
   -moz-appearance: none;
 }
 
 /* .scale-slider is an element inside the <scale> implementation. */
 .scrubber .scale-slider,
 .volumeControl .scale-slider {
   /* Hide the default horizontal bar. */
   -moz-appearance: none;
--- a/widget/android/nsWindow.cpp
+++ b/widget/android/nsWindow.cpp
@@ -967,18 +967,18 @@ nsWindow::OnGlobalAndroidEvent(AndroidGe
         case AndroidGeckoEvent::MOTION_EVENT: {
             win->UserActivity();
             if (!gTopLevelWindows.IsEmpty()) {
                 nsIntPoint pt(0,0);
                 nsTArray<nsIntPoint> points = ae->Points();
                 if (points.Length() > 0) {
                     pt = points[0];
                 }
-                pt.x = clamped(pt.x, 0, gAndroidBounds.width - 1);
-                pt.y = clamped(pt.y, 0, gAndroidBounds.height - 1);
+                pt.x = clamped(pt.x, 0, PR_MAX(gAndroidBounds.width - 1, 0));
+                pt.y = clamped(pt.y, 0, PR_MAX(gAndroidBounds.height - 1, 0));
                 nsWindow *target = win->FindWindowForPoint(pt);
 #if 0
                 ALOG("MOTION_EVENT %f,%f -> %p (visible: %d children: %d)", pt.x, pt.y, (void*)target,
                      target ? target->mIsVisible : 0,
                      target ? target->mChildren.Length() : 0);
 
                 DumpWindows();
 #endif