Bug 796006 - Don't fully expand nodes with large amounts of children in markup panel. r=jwalker
authorDave Camp <dcamp>
Tue, 18 Dec 2012 17:14:00 +0100
changeset 116639 6b168eb45722e7082cbbdba612674abf2e615181
parent 116437 bfd85c9652fa63023a338c5d425d5ab319418d45
child 116640 59e6f831b60bf9f6df599850c3d23028e16a0c4c
push idunknown
push userunknown
push dateunknown
reviewersjwalker
bugs796006
milestone20.0a1
Bug 796006 - Don't fully expand nodes with large amounts of children in markup panel. r=jwalker
browser/devtools/markupview/MarkupView.jsm
browser/devtools/markupview/markup-view.xhtml
browser/devtools/markupview/test/Makefile.in
browser/devtools/markupview/test/browser_inspector_markup_subset.html
browser/devtools/markupview/test/browser_inspector_markup_subset.js
browser/locales/en-US/chrome/browser/devtools/inspector.properties
browser/themes/gnomestripe/devtools/markup-view.css
browser/themes/pinstripe/devtools/markup-view.css
browser/themes/winstripe/devtools/markup-view.css
--- a/browser/devtools/markupview/MarkupView.jsm
+++ b/browser/devtools/markupview/MarkupView.jsm
@@ -7,24 +7,26 @@
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 
 // Page size for pageup/pagedown
 const PAGE_SIZE = 10;
 
 const PREVIEW_AREA = 700;
+const DEFAULT_MAX_CHILDREN = 100;
 
 this.EXPORTED_SYMBOLS = ["MarkupView"];
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource:///modules/devtools/CssRuleView.jsm");
 Cu.import("resource:///modules/devtools/Templater.jsm");
 Cu.import("resource:///modules/devtools/Undo.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  * Node - A content node.
  * object.elt - A UI element in the markup panel.
@@ -41,16 +43,22 @@ Cu.import("resource://gre/modules/Servic
  */
 this.MarkupView = function MarkupView(aInspector, aFrame, aControllerWindow)
 {
   this._inspector = aInspector;
   this._frame = aFrame;
   this.doc = this._frame.contentDocument;
   this._elt = this.doc.querySelector("#root");
 
+  try {
+    this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
+  } catch(ex) {
+    this.maxChildren = DEFAULT_MAX_CHILDREN;
+  }
+
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
   this._containers = new WeakMap();
 
   this._observer = new this.doc.defaultView.MutationObserver(this._mutationObserver.bind(this));
 
   this._boundOnNewSelection = this._onNewSelection.bind(this);
@@ -64,17 +72,17 @@ this.MarkupView = function MarkupView(aI
   this._frame.addEventListener("focus", this._boundFocus, false);
 
   this._initPreview();
 }
 
 MarkupView.prototype = {
   _selectedContainer: null,
 
-  template: function MT_template(aName, aDest, aOptions)
+  template: function MT_template(aName, aDest, aOptions={stack: "markup-view.xhtml"})
   {
     let node = this.doc.getElementById("template-" + aName).cloneNode(true);
     node.removeAttribute("id");
     template(node, aDest, aOptions);
     return node;
   },
 
   /**
@@ -283,32 +291,34 @@ MarkupView.prototype = {
       attributes: true,
       childList: true,
       characterData: true,
     });
 
     let walker = documentWalker(aNode);
     let parent = walker.parentNode();
     if (parent) {
-      // Make sure parents of this node are imported too.
       var container = new MarkupContainer(this, aNode);
     } else {
       var container = new RootContainer(this, aNode);
       this._elt.appendChild(container.elt);
       this._rootNode = aNode;
       aNode.addEventListener("load", function MP_watch_contentLoaded(aEvent) {
         // Fake a childList mutation here.
         this._mutationObserver([{target: aEvent.target, type: "childList"}]);
       }.bind(this), true);
-
     }
 
     this._containers.set(aNode, container);
+    // FIXME: set an expando to prevent the the wrapper from disappearing
+    // See bug 819131 for details.
+    aNode.__preserveHack = true;
     container.expanded = aExpand;
 
+    container.childrenDirty = true;
     this._updateChildren(container);
 
     if (parent) {
       this.importNode(parent, true);
     }
     return container;
   },
 
@@ -322,32 +332,35 @@ MarkupView.prototype = {
       if (!container) {
         // Container might not exist if this came from a load event for an iframe
         // we're not viewing.
         continue;
       }
       if (mutation.type === "attributes" || mutation.type === "characterData") {
         container.update();
       } else if (mutation.type === "childList") {
+        container.childrenDirty = true;
         this._updateChildren(container);
       }
     }
     this._inspector.emit("markupmutation");
   },
 
   /**
    * Make sure the given node's parents are expanded and the
    * node is scrolled on to screen.
    */
   showNode: function MT_showNode(aNode, centered)
   {
-    this.importNode(aNode);
+    let container = this.importNode(aNode);
+    this._updateChildren(container);
     let walker = documentWalker(aNode);
     let parent;
     while (parent = walker.parentNode()) {
+      this._updateChildren(this.getContainer(parent));
       this.expandNode(parent);
     }
     LayoutHelpers.scrollIntoViewIfNeeded(this._containers.get(aNode).editor.elt, centered);
   },
 
   /**
    * Expand the container's children.
    */
@@ -416,20 +429,44 @@ MarkupView.prototype = {
     if (this._selectedContainer) {
       this._selectedContainer.selected = false;
     }
     this._selectedContainer = container;
     if (aNode) {
       this._selectedContainer.selected = true;
     }
 
+    this._ensureSelectionVisible();
+    this._selectedContainer.focus();
+
     return true;
   },
 
   /**
+   * Make sure that every ancestor of the selection are updated
+   * and included in the list of visible children.
+   */
+  _ensureSelectionVisible: function MT_ensureSelectionVisible()
+  {
+    let node = this._selectedContainer.node;
+    let walker = documentWalker(node);
+    while (node) {
+      let container = this._containers.get(node);
+      let parent = walker.parentNode();
+      if (!container.elt.parentNode) {
+        let parentContainer = this._containers.get(parent);
+        parentContainer.childrenDirty = true;
+        this._updateChildren(parentContainer, node);
+      }
+
+      node = parent;
+    }
+  },
+
+  /**
    * Unmark selected node (no node selected).
    */
   unmarkSelectedNode: function MT_unmarkSelectedNode()
   {
     if (this._selectedContainer) {
       this._selectedContainer.selected = false;
       this._selectedContainer = null;
     }
@@ -443,39 +480,149 @@ MarkupView.prototype = {
     if (aNode === this._inspector.selection) {
       this._inspector.change("markupview");
     }
   },
 
   /**
    * Make sure all children of the given container's node are
    * imported and attached to the container in the right order.
+   * @param aCentered If provided, this child will be included
+   *        in the visible subset, and will be roughly centered
+   *        in that list.
    */
-  _updateChildren: function MT__updateChildren(aContainer)
+  _updateChildren: function MT__updateChildren(aContainer, aCentered)
   {
+    if (!aContainer.childrenDirty) {
+      return false;
+    }
+
     // Get a tree walker pointing at the first child of the node.
     let treeWalker = documentWalker(aContainer.node);
     let child = treeWalker.firstChild();
     aContainer.hasChildren = !!child;
-    if (aContainer.expanded) {
-      let lastContainer = null;
-      while (child) {
-        let container = this.importNode(child, false);
+
+    if (!aContainer.expanded) {
+      return;
+    }
+
+    aContainer.childrenDirty = false;
+
+    let children = this._getVisibleChildren(aContainer, aCentered);
+    let fragment = this.doc.createDocumentFragment();
+
+    for (child of children.children) {
+      let container = this.importNode(child, false);
+      fragment.appendChild(container.elt);
+    }
+
+    while (aContainer.children.firstChild) {
+      aContainer.children.removeChild(aContainer.children.firstChild);
+    }
 
-        // Make sure children are in the right order.
-        let before = lastContainer ? lastContainer.nextSibling : aContainer.children.firstChild;
-        aContainer.children.insertBefore(container.elt, before);
-        lastContainer = container.elt;
-        child = treeWalker.nextSibling();
+    if (!(children.hasFirst && children.hasLast)) {
+      let data = {
+        showing: this.strings.GetStringFromName("markupView.more.showing"),
+        showAll: this.strings.formatStringFromName(
+                  "markupView.more.showAll",
+                  [aContainer.node.children.length.toString()], 1),
+        allButtonClick: function() {
+          aContainer.maxChildren = -1;
+          aContainer.childrenDirty = true;
+          this._updateChildren(aContainer);
+        }.bind(this)
+      };
+
+      if (!children.hasFirst) {
+        let span = this.template("more-nodes", data);
+        fragment.insertBefore(span, fragment.firstChild);
       }
-
-      while (aContainer.children.lastChild != lastContainer) {
-        aContainer.children.removeChild(aContainer.children.lastChild);
+      if (!children.hasLast) {
+        let span = this.template("more-nodes", data);
+        fragment.appendChild(span);
       }
     }
+
+    aContainer.children.appendChild(fragment);
+
+    return true;
+  },
+
+  /**
+   * Return a list of the children to display for this container.
+   */
+  _getVisibleChildren: function MV__getVisibleChildren(aContainer, aCentered)
+  {
+    let maxChildren = aContainer.maxChildren || this.maxChildren;
+    if (maxChildren == -1) {
+      maxChildren = Number.MAX_VALUE;
+    }
+    let firstChild = documentWalker(aContainer.node).firstChild();
+    let lastChild = documentWalker(aContainer.node).lastChild();
+
+    if (!firstChild) {
+      // No children, we're done.
+      return { hasFirst: true, hasLast: true, children: [] };
+    }
+
+    // By default try to put the selected child in the middle of the list.
+    let start = aCentered || firstChild;
+
+    // Start by reading backward from the starting point....
+    let nodes = [];
+    let backwardWalker = documentWalker(start);
+    if (backwardWalker.previousSibling()) {
+      let backwardCount = Math.floor(maxChildren / 2);
+      let backwardNodes = this._readBackward(backwardWalker, backwardCount);
+      nodes = backwardNodes;
+    }
+
+    // Then read forward by any slack left in the max children...
+    let forwardWalker = documentWalker(start);
+    let forwardCount = maxChildren - nodes.length;
+    nodes = nodes.concat(this._readForward(forwardWalker, forwardCount));
+
+    // If there's any room left, it means we've run all the way to the end.
+    // In that case, there might still be more items at the front.
+    let remaining = maxChildren - nodes.length;
+    if (remaining > 0 && nodes[0] != firstChild) {
+      let firstNodes = this._readBackward(backwardWalker, remaining);
+
+      // Then put it all back together.
+      nodes = firstNodes.concat(nodes);
+    }
+
+    return {
+      hasFirst: nodes[0] == firstChild,
+      hasLast: nodes[nodes.length - 1] == lastChild,
+      children: nodes
+    };
+  },
+
+  _readForward: function MV__readForward(aWalker, aCount)
+  {
+    let ret = [];
+    let node = aWalker.currentNode;
+    do {
+      ret.push(node);
+      node = aWalker.nextSibling();
+    } while (node && --aCount);
+    return ret;
+  },
+
+  _readBackward: function MV__readBackward(aWalker, aCount)
+  {
+    let ret = [];
+    let node = aWalker.currentNode;
+    do {
+      ret.push(node);
+      node = aWalker.previousSibling();
+    } while(node && --aCount);
+    ret.reverse();
+    return ret;
   },
 
   /**
    * Tear down the markup panel.
    */
   destroy: function MT_destroy()
   {
     this.undo.destroy();
@@ -613,19 +760,17 @@ function MarkupContainer(aMarkupView, aN
     this.editor = new GenericEditor(this.markup, aNode);
   }
 
   // The template will fill the following properties
   this.elt = null;
   this.expander = null;
   this.codeBox = null;
   this.children = null;
-  let options = { stack: "markup-view.xhtml" };
-  this.markup.template("container", this, options);
-
+  this.markup.template("container", this);
   this.elt.container = this;
 
   this.expander.addEventListener("click", function() {
     this.markup.navigate(this);
 
     if (this.expanded) {
       this.markup.collapseNode(this.node);
     } else {
@@ -729,17 +874,17 @@ MarkupContainer.prototype = {
    * Try to put keyboard focus on the current editor.
    */
   focus: function MC_focus()
   {
     let focusable = this.editor.elt.querySelector("[tabindex]");
     if (focusable) {
       focusable.focus();
     }
-  }
+  },
 }
 
 /**
  * Dummy container node used for the root document element.
  */
 function RootContainer(aMarkupView, aNode)
 {
   this.doc = aMarkupView.doc;
@@ -836,23 +981,22 @@ function ElementEditor(aContainer, aNode
   this.attrs = [];
 
   // The templates will fill the following properties
   this.elt = null;
   this.tag = null;
   this.attrList = null;
   this.newAttr = null;
   this.closeElt = null;
-  let options = { stack: "markup-view.xhtml" };
 
   // Create the main editor
-  this.template("element", this, options);
+  this.template("element", this);
 
   // Create the closing tag
-  this.template("elementClose", this, options);
+  this.template("elementClose", this);
 
   // Make the tag name editable (unless this is a document element)
   if (aNode != aNode.ownerDocument.documentElement) {
     this.tag.setAttribute("tabindex", "0");
     _editableField({
       element: this.tag,
       trigger: "dblclick",
       stopOnReturn: true,
@@ -922,18 +1066,17 @@ ElementEditor.prototype = {
       var attr = this.attrs[aAttr.name];
       var name = attr.querySelector(".attrname");
       var val = attr.querySelector(".attrvalue");
     } else {
       // Create the template editor, which will save some variables here.
       let data = {
         attrName: aAttr.name,
       };
-      let options = { stack: "markup-view.xhtml" };
-      this.template("attribute", data, options);
+      this.template("attribute", data);
       var {attr, inner, name, val} = data;
 
       // Figure out where we should place the attribute.
       let before = aBefore || null;
       if (aAttr.name == "id") {
         before = this.attrList.firstChild;
       } else if (aAttr.name == "class") {
         let idNode = this.attrs["id"];
@@ -1255,8 +1398,13 @@ function whitespaceTextFilter(aNode)
 {
     if (aNode.nodeType == Ci.nsIDOMNode.TEXT_NODE &&
         !/[^\s]/.exec(aNode.nodeValue)) {
       return Ci.nsIDOMNodeFilter.FILTER_SKIP;
     } else {
       return Ci.nsIDOMNodeFilter.FILTER_ACCEPT;
     }
 }
+
+XPCOMUtils.defineLazyGetter(MarkupView.prototype, "strings", function () {
+  return Services.strings.createBundle(
+          "chrome://browser/locale/devtools/inspector.properties");
+});
--- a/browser/devtools/markupview/markup-view.xhtml
+++ b/browser/devtools/markupview/markup-view.xhtml
@@ -11,16 +11,18 @@
   <link rel="stylesheet" href="chrome://browser/skin/devtools/markup-view.css" type="text/css"/>
   <link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
 </head>
 <body class="devtools-theme-background devtools-monospace" role="application">
   <div id="root"></div>
   <div id="templates" style="display:none">
     <ul>
       <li id="template-container" save="${elt}" class="container"><span save="${expander}" class="expander"></span><span save="${codeBox}" class="codebox"><ul save="${children}" class="children"></ul></span></li>
+
+      <li id="template-more-nodes" class="more-nodes devtools-class-comment" save="${elt}"><span>${showing}</span> <button href="#" onclick="${allButtonClick}">${showAll}</button></li>
     </ul>
 
     <span id="template-element" save="${elt}" class="editor"><span>&lt;</span><span save="${tag}" class="tagname devtools-theme-tagname"></span><span save="${attrList}"></span><span save="${newAttr}" class="newattr" tabindex="0"></span>&gt;</span>
 
     <span id="template-attribute" save="${attr}" data-attr="${attrName}" class="attreditor" style="display:none"> <span class="editable" save="${inner}" tabindex="0"><span save="${name}" class="attrname devtools-theme-attrname"></span>=&quot;<span save="${val}" class="attrvalue devtools-theme-attrvalue"></span>&quot;</span></span>
 
     <span id="template-text" save="${elt}" class="editor text">
       <pre save="${value}" style="display:inline-block;" tabindex="0"></pre>
--- a/browser/devtools/markupview/test/Makefile.in
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -12,14 +12,16 @@ include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_FILES = \
 		browser_inspector_markup_navigation.html \
 		browser_inspector_markup_navigation.js \
 		browser_inspector_markup_mutation.html \
 		browser_inspector_markup_mutation.js \
 		browser_inspector_markup_edit.html \
-		browser_inspector_markup_edit.js \
+    browser_inspector_markup_edit.js \
+    browser_inspector_markup_subset.html \
+    browser_inspector_markup_subset.js \
 		head.js \
 		$(NULL)
 
 libs::	$(_BROWSER_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_subset.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+
+<html class="html">
+  <body class="body">
+    <div id="a"></div>
+    <div id="b"></div>
+    <div id="c"></div>
+    <div id="d"></div>
+    <div id="e"></div>
+    <div id="f"></div>
+    <div id="g"></div>
+    <div id="h"></div>
+    <div id="i"></div>
+    <div id="j"></div>
+    <div id="k"></div>
+    <div id="l"></div>
+    <div id="m"></div>
+    <div id="n"></div>
+    <div id="o"></div>
+    <div id="p"></div>
+    <div id="q"></div>
+    <div id="r"></div>
+    <div id="s"></div>
+    <div id="t"></div>
+    <div id="u"></div>
+    <div id="v"></div>
+    <div id="w"></div>
+    <div id="x"></div>
+    <div id="y"></div>
+    <div id="z"></div>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_inspector_markup_subset.js
@@ -0,0 +1,145 @@
+/* Any copyright", " is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the markup view loads only as many nodes as specified
+ * by the devtools.markup.pagesize preference.
+ */
+
+registerCleanupFunction(function() {
+  Services.prefs.clearUserPref("devtools.markup.pagesize");
+});
+Services.prefs.setIntPref("devtools.markup.pagesize", 5);
+
+
+function test() {
+  waitForExplicitFinish();
+
+  // Will hold the doc we're viewing
+  let doc;
+
+  let inspector;
+
+  // Holds the MarkupTool object we're testing.
+  let markup;
+
+  function assertChildren(expected)
+  {
+    let container = markup.getContainer(doc.querySelector("body"));
+    let found = [];
+    for (let child of container.children.children) {
+      if (child.classList.contains("more-nodes")) {
+        found += "*more*";
+      } else {
+        found += child.container.node.getAttribute("id");
+      }
+    }
+    is(expected, found, "Got the expected children.");
+  }
+
+  function forceReload()
+  {
+    let container = markup.getContainer(doc.querySelector("body"));
+    container.childrenDirty = true;
+  }
+
+  let selections = [
+    {
+      desc: "Select the first item",
+      selector: "#a",
+      before: function() {
+      },
+      after: function() {
+        assertChildren("abcde*more*");
+      }
+    },
+    {
+      desc: "Select the last item",
+      selector: "#z",
+      before: function() {},
+      after: function() {
+        assertChildren("*more*vwxyz");
+      }
+    },
+    {
+      desc: "Select an already-visible item",
+      selector: "#v",
+      before: function() {},
+      after: function() {
+        // Because "v" was already visible, we shouldn't have loaded
+        // a different page.
+        assertChildren("*more*vwxyz");
+      },
+    },
+    {
+      desc: "Verify childrenDirty reloads the page",
+      selector: "#w",
+      before: function() {
+        forceReload();
+      },
+      after: function() {
+        // But now that we don't already have a loaded page, selecting
+        // w should center around w.
+        assertChildren("*more*uvwxy*more*");
+      },
+    },
+  ];
+
+  // Create the helper tab for parsing...
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_subset.html";
+
+  function setupTest() {
+    var target = TargetFactory.forTab(gBrowser.selectedTab);
+    let toolbox = gDevTools.openToolboxForTab(target, "inspector");
+    toolbox.once("inspector-selected", function SE_selected(id, aInspector) {
+      inspector = aInspector;
+      markup = inspector.markup;
+      runNextSelection();
+    });
+  }
+
+  function runTests() {
+    inspector.selection.once("new-node", startTests);
+    executeSoon(function() {
+      inspector.selection.setNode(doc.body);
+    });
+  }
+
+  function runNextSelection() {
+    let selection = selections.shift();
+    if (!selection) {
+      clickMore();
+      return;
+    }
+
+    info(selection.desc);
+    selection.before();
+    inspector.selection.once("new-node", function() {
+      selection.after();
+      runNextSelection();
+    });
+    inspector.selection.setNode(doc.querySelector(selection.selector));
+  }
+
+  function clickMore() {
+    info("Check that clicking more loads the whole thing.");
+    // Make sure that clicking the "more" button loads all the nodes.
+    let container = markup.getContainer(doc.querySelector("body"));
+    let button = container.elt.querySelector("button");
+    button.click();
+    assertChildren("abcdefghijklmnopqrstuvwxyz");
+    finishUp();
+  }
+
+  function finishUp() {
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/locales/en-US/chrome/browser/devtools/inspector.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/inspector.properties
@@ -28,8 +28,14 @@ breadcrumbs.siblings=Siblings
 nodeMenu.tooltiptext=Node operations
 
 
 # LOCALIZATION NOTE (inspector.*)
 # Used for the menuitem in the tool menu
 inspector.label=Inspector
 inspector.commandkey=I
 inspector.accesskey=I
+
+# LOCALIZATION NOTE (markupView.more.*)
+# When there are too many nodes to load at once, we will offer to
+# show all the nodes.
+markupView.more.showing=Some nodes were hidden.
+markupView.more.showAll=Show All %S Nodes
--- a/browser/themes/gnomestripe/devtools/markup-view.css
+++ b/browser/themes/gnomestripe/devtools/markup-view.css
@@ -44,11 +44,15 @@ li.container {
   -moz-appearance: treetwisty;
   padding: 11px 0;
 }
 
 .expander[expanded] {
   -moz-appearance: treetwistyopen;
 }
 
+.more-nodes {
+  padding-left: 16px;
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
 }
--- a/browser/themes/pinstripe/devtools/markup-view.css
+++ b/browser/themes/pinstripe/devtools/markup-view.css
@@ -47,11 +47,15 @@ li.container {
   width: 14px;
   height: 14px;
 }
 
 .expander[expanded] {
   -moz-appearance: treetwistyopen;
 }
 
+.more-nodes {
+  padding-left: 16px;
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
 }
--- a/browser/themes/winstripe/devtools/markup-view.css
+++ b/browser/themes/winstripe/devtools/markup-view.css
@@ -49,11 +49,15 @@ li.container {
   background-position: center;
   background-image: url("chrome://global/skin/tree/twisty-clsd.png");
 }
 
 .expander[expanded] {
   background-image: url("chrome://global/skin/tree/twisty-open.png");
 }
 
+.more-nodes {
+  padding-left: 16px;
+}
+
 .styleinspector-propertyeditor {
   border: 1px solid #CCC;
 }