Merge m-c to m-i.
authorKyle Huey <khuey@kylehuey.com>
Wed, 02 Nov 2011 08:53:04 -0400
changeset 79564 631b19db4eb5a58084fc3153a46a968fcbf80dd6
parent 79552 be8056caccb4db8b1ff89dba474847d2a65b9184 (current diff)
parent 79563 06292dba305205f823d4043be55f35988806fc1d (diff)
child 79565 a64b3abb31c4053aa4e79b3b6cf973f802f13fa7
push id3053
push userkhuey@mozilla.com
push dateWed, 02 Nov 2011 12:53:44 +0000
treeherdermozilla-inbound@80af665378fd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone10.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 m-c to m-i.
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -982,26 +982,36 @@
              nowindowdrag="true"
              hidden="true">
       <vbox flex="1">
         <resizer id="inspector-top-resizer" flex="1" 
                  class="inspector-resizer"
                  dir="top" disabled="true"
                  element="inspector-tree-box"/>
         <hbox>
+#ifdef XP_MACOSX
+          <toolbarbutton id="highlighter-closebutton"
+                         oncommand="InspectorUI.closeInspectorUI(false);"
+                         tooltiptext="&inspectCloseButton.tooltiptext;"/>
+#endif
           <toolbarbutton id="inspector-inspect-toolbutton"
                          label="&inspectButton.label;"
                          accesskey="&inspectButton.accesskey;"
                          command="Inspector:Inspect"/>
           <arrowscrollbox id="inspector-breadcrumbs"
                           flex="1" orient="horizontal"
                           clicktoscroll="true"/>
           <hbox id="inspector-tools">
             <!-- registered tools go here -->
           </hbox>
+#ifndef XP_MACOSX
+          <toolbarbutton id="highlighter-closebutton"
+                         oncommand="InspectorUI.closeInspectorUI(false);"
+                         tooltiptext="&inspectCloseButton.tooltiptext;"/>
+#endif
           <resizer id="inspector-end-resizer"
                    class="inspector-resizer"
                    dir="top" disabled="true"
                    element="inspector-tree-box"/>
         </hbox>
       </vbox>
     </toolbar>
     <toolbar id="addon-bar"
--- a/browser/base/content/highlighter.css
+++ b/browser/base/content/highlighter.css
@@ -25,24 +25,27 @@
 #highlighter-veil-rightbox {
   -moz-box-flex: 1;
 }
 
 #highlighter-veil-middlebox:-moz-locale-dir(rtl) {
   -moz-box-direction: reverse;
 }
 
-#highlighter-close-button {
-  position: absolute;
-  pointer-events: auto;
-  z-index: 1;
+.inspector-breadcrumbs-button {
+  direction: ltr;
 }
 
-.inspector-breadcrumbs-button {
-  direction: ltr;
+.inspector-resizer {
+  display: none;
+}
+
+#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer,
+#inspector-toolbar[treepanel-open] > vbox > hbox > #inspector-end-resizer {
+  display: -moz-box;
 }
 
 /*
  * Node Infobar
  */
 
 #highlighter-nodeinfobar-container {
   position: absolute;
--- a/browser/devtools/highlighter/TreePanel.jsm
+++ b/browser/devtools/highlighter/TreePanel.jsm
@@ -226,22 +226,17 @@ TreePanel.prototype = {
       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.minHeight = 10;
     treeBox.flex = 1;
     toolbarParent.insertBefore(treeBox, toolbar);
 
-    let resizerTop =
-      this.IUI.browser.ownerDocument.getElementById("inspector-top-resizer");
-    let resizerEnd =
-      this.IUI.browser.ownerDocument.getElementById("inspector-end-resizer");
-    resizerTop.removeAttribute("disabled");
-    resizerEnd.removeAttribute("disabled");
+    this.IUI.toolbar.setAttribute("treepanel-open", "true");
 
     treeBox.appendChild(this.treeIFrame);
 
     let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel()
     {
       this.treeIFrame.removeEventListener("load",
         boundLoadedInitializeTreePanel, true);
       this.initializeIFrame();
@@ -259,22 +254,17 @@ TreePanel.prototype = {
   },
 
   /**
    * Close the TreePanel.
    */
   close: function TP_close()
   {
     if (this.openInDock) {
-      let resizerTop = 
-        this.IUI.browser.ownerDocument.getElementById("inspector-top-resizer");
-      let resizerEnd = 
-        this.IUI.browser.ownerDocument.getElementById("inspector-end-resizer");
-      resizerTop.setAttribute("disabled", "true");
-      resizerEnd.setAttribute("disabled", "true");
+      this.IUI.toolbar.removeAttribute("treepanel-open");
 
       let treeBox = this.container;
       let treeBoxParent = treeBox.parentNode;
       treeBoxParent.removeChild(treeBox);
     } else {
       this.container.hidePopup();
     }
 
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -119,30 +119,30 @@ Highlighter.prototype = {
     this._highlighting = false;
 
     this.highlighterContainer = this.chromeDoc.createElement("stack");
     this.highlighterContainer.id = "highlighter-container";
 
     this.veilContainer = this.chromeDoc.createElement("vbox");
     this.veilContainer.id = "highlighter-veil-container";
 
+    // The controlsBox will host the different interactive
+    // elements of the highlighter (buttons, toolbars, ...).
     let controlsBox = this.chromeDoc.createElement("box");
     controlsBox.id = "highlighter-controls";
     this.highlighterContainer.appendChild(this.veilContainer);
     this.highlighterContainer.appendChild(controlsBox);
 
     stack.appendChild(this.highlighterContainer);
 
     // The veil will make the whole page darker except
     // for the region of the selected box.
     this.buildVeil(this.veilContainer);
 
-    // The controlsBox will host the different interactive
-    // elements of the highlighter (buttons, toolbars, ...).
-    this.buildControls(controlsBox);
+    this.buildInfobar(controlsBox);
 
     this.browser.addEventListener("resize", this, true);
     this.browser.addEventListener("scroll", this, true);
 
     this.handleResize();
   },
 
   /**
@@ -196,30 +196,16 @@ Highlighter.prototype = {
     this.veilMiddleBox.appendChild(veilRightBox);
 
     aParent.appendChild(this.veilTopBox);
     aParent.appendChild(this.veilMiddleBox);
     aParent.appendChild(veilBottomBox);
   },
 
   /**
-   * Build the controls:
-   *
-   * <box id="highlighter-close-button"/>
-   *
-   * @param nsIDOMElement aParent
-   *        The container of the controls elements.
-   */
-  buildControls: function Highlighter_buildControls(aParent)
-  {
-    this.buildCloseButton(aParent);
-    this.buildInfobar(aParent);
-  },
-
-  /**
    * Build the node Infobar.
    *
    * <box id="highlighter-nodeinfobar-container">
    *   <box id="Highlighter-nodeinfobar-arrow-top"/>
    *   <vbox id="highlighter-nodeinfobar">
    *     <label id="highlighter-nodeinfobar-tagname"/>
    *     <label id="highlighter-nodeinfobar-id"/>
    *     <vbox id="highlighter-nodeinfobar-classes"/>
@@ -275,48 +261,23 @@ Highlighter.prototype = {
       idLabel: idLabel,
       classesBox: classesBox,
       container: container,
       barHeight: barHeight,
     };
   },
 
   /**
-   * Build the close button.
-   *
-   * @param nsIDOMElement aParent
-   *        The container of the close-button.
-   */
-  buildCloseButton: function Highlighter_buildCloseButton(aParent)
-  {
-    let closeButton = this.chromeDoc.createElement("box");
-    closeButton.id = "highlighter-close-button";
-    closeButton.appendChild(this.chromeDoc.createElement("image"));
-
-    let boundCloseEventHandler = this.IUI.closeInspectorUI.bind(this.IUI, false);
-
-    closeButton.addEventListener("click", boundCloseEventHandler, false);
-
-    aParent.appendChild(closeButton);
-
-    this.boundCloseEventHandler = boundCloseEventHandler;
-    this.closeButton = closeButton;
-  },
-
-  /**
    * Destroy the nodes.
    */
   destroy: function Highlighter_destroy()
   {
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("resize", this, true);
-    this.closeButton.removeEventListener("click", this.boundCloseEventHandler, false);
     this.boundCloseEventHandler = null;
-    this.closeButton.parentNode.removeChild(this.closeButton);
-    this.closeButton = null;
     this._contentRect = null;
     this._highlightRect = null;
     this._highlighting = false;
     this.veilTopBox = null;
     this.veilLeftBox = null;
     this.veilMiddleBox = null;
     this.veilTransparentBox = null;
     this.veilContainer = null;
--- a/browser/devtools/highlighter/test/browser_inspector_bug_690361.js
+++ b/browser/devtools/highlighter/test/browser_inspector_bug_690361.js
@@ -77,17 +77,17 @@ function runInspectorTests()
   ok(!InspectorUI.toolbar.hidden, "toolbar is visible");
   ok(InspectorUI.inspecting, "Inspector is inspecting");
   ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
   ok(InspectorUI.highlighter, "Highlighter is up");
 
   salutation = doc.getElementById("salutation");
   InspectorUI.inspectNode(salutation);
 
-  let button = document.getElementById("highlighter-close-button");
+  let button = document.getElementById("highlighter-closebutton");
   button.click();
 }
 
 function closeInspectorTests()
 {
   Services.obs.removeObserver(closeInspectorTests,
     InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED);
   Services.obs.addObserver(inspectorOpenedTrap,
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -1,10 +1,11 @@
 browser.jar:
 *   content/browser/inspector.html                (highlighter/inspector.html)
     content/browser/NetworkPanel.xhtml            (webconsole/NetworkPanel.xhtml)
 *   content/browser/scratchpad.xul                (scratchpad/scratchpad.xul)
 *   content/browser/scratchpad.js                 (scratchpad/scratchpad.js)
     content/browser/csshtmltree.xhtml             (styleinspector/csshtmltree.xhtml)
+    content/browser/devtools/cssruleview.xhtml    (styleinspector/cssruleview.xhtml)
+    content/browser/devtools/styleinspector.css   (styleinspector/styleinspector.css)
     content/browser/orion.js                      (sourceeditor/orion/orion.js)
     content/browser/orion.css                     (sourceeditor/orion/orion.css)
     content/browser/orion-mozilla.css             (sourceeditor/orion/mozilla.css)
-
--- a/browser/devtools/sourceeditor/orion/mozilla.css
+++ b/browser/devtools/sourceeditor/orion/mozilla.css
@@ -1,11 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+.viewContainer {
+  font-size: inherit; /* inherit browser's default monospace font size */
+}
 
 .rulerLines {
   background: -moz-Dialog;
   color: -moz-DialogText;
   min-width: 1.4em;
   padding-left: 4px;
   padding-right: 4px;
   text-align: end;
--- a/browser/devtools/styleinspector/CssLogic.jsm
+++ b/browser/devtools/styleinspector/CssLogic.jsm
@@ -816,16 +816,46 @@ CssLogic.sheetMediaAllowed = function Cs
   } else {
     result = true;
   }
 
   return result;
 };
 
 /**
+ * Return a shortened version of a style sheet's source.
+ *
+ * @param {CSSStyleSheet} aSheet the DOM object for the style sheet.
+ */
+CssLogic.shortSource = function CssLogic_shortSource(aSheet)
+{
+    // Use a string like "inline" if there is no source href
+    if (!aSheet || !aSheet.href) {
+      return CssLogic.l10n("rule.sourceInline");
+    }
+
+    // We try, in turn, the filename, filePath, query string, whole thing
+    let url = Services.io.newURI(aSheet.href, null, null);
+    url = url.QueryInterface(Ci.nsIURL);
+    if (url.fileName) {
+      return url.fileName;
+    }
+
+    if (url.filePath) {
+      return url.filePath;
+    }
+
+    if (url.query) {
+      return url.query;
+    }
+
+    return this.domSheet.href;
+}
+
+/**
  * A safe way to access cached bits of information about a stylesheet.
  *
  * @constructor
  * @param {CssLogic} aCssLogic pointer to the CssLogic instance working with
  * this CssSheet object.
  * @param {CSSStyleSheet} aDomSheet reference to a DOM CSSStyleSheet object.
  * @param {boolean} aSystemSheet tells if the stylesheet is system-provided.
  * @param {number} aIndex tells the index/position of the stylesheet within the
@@ -880,41 +910,17 @@ CssSheet.prototype = {
    * @return {string} the shorthand source of the stylesheet.
    */
   get shortSource()
   {
     if (this._shortSource) {
       return this._shortSource;
     }
 
-    // Use a string like "inline" if there is no source href
-    if (!this.domSheet.href) {
-      this._shortSource = CssLogic.l10n("rule.sourceInline");
-      return this._shortSource;
-    }
-
-    // We try, in turn, the filename, filePath, query string, whole thing
-    let url = Services.io.newURI(this.domSheet.href, null, null);
-    url = url.QueryInterface(Ci.nsIURL);
-    if (url.fileName) {
-      this._shortSource = url.fileName;
-      return this._shortSource;
-    }
-
-    if (url.filePath) {
-      this._shortSource = url.filePath;
-      return this._shortSource;
-    }
-
-    if (url.query) {
-      this._shortSource = url.query;
-      return this._shortSource;
-    }
-
-    this._shortSource = this.domSheet.href;
+    this._shortSource = CssLogic.shortSource(this.domSheet);
     return this._shortSource;
   },
 
   /**
    * Tells if the sheet is allowed or not by the current CssLogic.sourceFilter.
    *
    * @return {boolean} true if the stylesheet is allowed by the sourceFilter, or
    * false otherwise.
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -0,0 +1,1248 @@
+/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* ***** 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 the Mozilla Inspector Module.
+ *
+ * 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):
+ *   Dave Camp (dcamp@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 ***** */
+
+"use strict"
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
+const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
+
+/**
+ * These regular expressions are adapted from firebug's css.js, and are
+ * used to parse CSSStyleDeclaration's cssText attribute.
+ */
+
+// Used to split on css line separators
+const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
+
+// Used to parse a single property line.
+const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource:///modules/devtools/CssLogic.jsm");
+
+var EXPORTED_SYMBOLS = ["CssRuleView",
+                        "_ElementStyle",
+                        "_editableField"];
+
+/**
+ * Our model looks like this:
+ *
+ * ElementStyle:
+ *   Responsible for keeping track of which properties are overridden.
+ *   Maintains a list of Rule objects that apply to the element.
+ * Rule:
+ *   Manages a single style declaration or rule.
+ *   Responsible for applying changes to the properties in a rule.
+ *   Maintains a list of TextProperty objects.
+ * TextProperty:
+ *   Manages a single property from the cssText attribute of the
+ *     relevant declaration.
+ *   Maintains a list of computed properties that come from this
+ *     property declaration.
+ *   Changes to the TextProperty are sent to its related Rule for
+ *     application.
+ */
+
+/**
+ * ElementStyle maintains a list of Rule objects for a given element.
+ *
+ * @constructor
+ */
+function ElementStyle(aElement)
+{
+  this.element = aElement;
+  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();
+}
+// We're exporting _ElementStyle for unit tests.
+var _ElementStyle = ElementStyle;
+
+ElementStyle.prototype = {
+
+  // The element we're looking at.
+  element: null,
+
+  // Empty, unconnected element of the same type as this node, used
+  // to figure out how shorthand properties will be parsed.
+  dummyElement: null,
+
+  domUtils: Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils),
+
+  /**
+   * 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()
+  {
+    this.rules = [];
+
+    // Include the element's style first.
+    this.rules.push(new Rule(this, {
+      style: this.element.style,
+      selectorText: CssLogic.l10n("rule.sourceElement")
+    }));
+
+    // Get the styles that apply to the element.
+    try {
+      var domRules = this.domUtils.getCSSStyleRules(this.element);
+    } catch (ex) {
+      Services.console.logStringMessage("ElementStyle_populate error: " + ex);
+      return;
+    }
+
+    // getCSStyleRules returns ordered from least-specific to
+    // most-specific.
+    for (let i = domRules.Count() - 1; i >= 0; i--) {
+      let domRule = domRules.GetElementAt(i);
+
+      // XXX: Optionally provide access to system sheets.
+      let systemSheet = CssLogic.isSystemStyleSheet(domRule.parentStyleSheet);
+      if (systemSheet) {
+        continue;
+      }
+
+      // XXX: non-style rules.
+      if (domRule.type === Ci.nsIDOMCSSRule.STYLE_RULE) {
+        this.rules.push(new Rule(this, { domRule: domRule }));
+      }
+    }
+
+    // Mark overridden computed styles.
+    this.markOverridden();
+  },
+
+  /**
+   * Mark the properties listed in this.rules with an overridden flag
+   * if an earlier property overrides it.
+   */
+  markOverridden: function ElementStyle_markOverridden()
+  {
+    // Gather all the text properties applied by these rules, ordered
+    // from more- to less-specific.
+    let textProps = [];
+    for each (let rule in this.rules) {
+      textProps = textProps.concat(rule.textProps.slice(0).reverse());
+    }
+
+    // Gather all the computed properties applied by those text
+    // properties.
+    let computedProps = [];
+    for each (let textProp in textProps) {
+      computedProps = computedProps.concat(textProp.computed);
+    };
+
+    // Walk over the computed properties.  As we see a property name
+    // for the first time, mark that property's name as taken by this
+    // property.
+    //
+    // If we come across a property whose name is already taken, check
+    // its priority against the property that was found first:
+    //
+    //   If the new property is a higher priority, mark the old
+    //   property overridden and mark the property name as taken by
+    //   the new property.
+    //
+    //   If the new property is a lower or equal priority, mark it as
+    //   overridden.
+    //
+    // _overriddenDirty will be set on each prop, indicating whether its
+    // dirty status changed during this pass.
+    let taken = {};
+    for each (let computedProp in computedProps) {
+      let earlier = taken[computedProp.name];
+      let overridden;
+      if (earlier
+          && computedProp.priority === "important"
+          && earlier.priority !== "important") {
+        // New property is higher priority.  Mark the earlier property
+        // overridden (which will reverse its dirty state).
+        earlier._overriddenDirty = !earlier._overriddenDirty;
+        earlier.overridden = true;
+        overridden = false;
+      } else {
+        overridden = !!earlier;
+      }
+
+      computedProp._overriddenDirty = (!!computedProp.overridden != overridden);
+      computedProp.overridden = overridden;
+      if (!computedProp.overridden) {
+        taken[computedProp.name] = computedProp;
+      }
+    }
+
+    // For each TextProperty, mark it overridden if all of its
+    // computed properties are marked overridden.  Update the text
+    // property's associated editor, if any.  This will clear the
+    // _overriddenDirty state on all computed properties.
+    for each (let textProp in textProps) {
+      // _updatePropertyOverridden will return true if the
+      // overridden state has changed for the text property.
+      if (this._updatePropertyOverridden(textProp)) {
+        textProp.updateEditor();
+      }
+    }
+  },
+
+  /**
+   * Mark a given TextProperty as overridden or not depending on the
+   * state of its computed properties.  Clears the _overriddenDirty state
+   * on all computed properties.
+   *
+   * @param {TextProperty} aProp
+   *        The text property to update.
+   *
+   * @return True if the TextProperty's overridden state (or any of its
+   *         computed properties overridden state) changed.
+   */
+  _updatePropertyOverridden: function ElementStyle_updatePropertyOverridden(aProp)
+  {
+    let overridden = true;
+    let dirty = false;
+    for each (let computedProp in aProp.computed) {
+      if (!computedProp.overridden) {
+        overridden = false;
+      }
+      dirty = computedProp._overriddenDirty || dirty;
+      delete computedProp._overriddenDirty;
+    }
+
+    dirty = (!!aProp.overridden != overridden) || dirty;
+    aProp.overridden = overridden;
+    return dirty;
+  }
+}
+
+/**
+ * A single style rule or declaration.
+ *
+ * @param {ElementStyle} aElementStyle
+ *        The ElementStyle to which this rule belongs.
+ * @param {object} aOptions
+ *        The information used to construct this rule.  Properties include:
+ *          domRule: the nsIDOMCSSStyleRule to view, if any.
+ *          style: the nsIDOMCSSStyleDeclaration to view.  If omitted,
+ *            the domRule's style will be used.
+ *          selectorText: selector text to display.  If omitted, the domRule's
+ *            selectorText will be used.
+ * @constructor
+ */
+function Rule(aElementStyle, aOptions)
+{
+  this.elementStyle = aElementStyle;
+  this.domRule = aOptions.domRule || null;
+  this.style = aOptions.style || this.domRule.style;
+  this.selectorText = aOptions.selectorText || this.domRule.selectorText;
+
+  this._getTextProperties();
+}
+
+Rule.prototype = {
+  get title()
+  {
+    if (this._title) {
+      return this._title;
+    }
+    let sheet = this.domRule ? this.domRule.parentStyleSheet : null;
+    this._title = CssLogic.shortSource(sheet);
+    if (this.domRule) {
+      let line = this.elementStyle.domUtils.getRuleLine(this.domRule);
+      this._title += ":" + line;
+    }
+    return this._title;
+  },
+
+  /**
+   * Create a new TextProperty to include in the rule.
+   *
+   * @param {string} aName
+   *        The text property name (such as "background" or "border-top").
+   * @param {string} aValue
+   *        The property's value (not including priority).
+   * @param {string} aPriority
+   *        The property's priority (either "important" or an empty string).
+   */
+  createProperty: function Rule_createProperty(aName, aValue, aPriority)
+  {
+    let prop = new TextProperty(this, aName, aValue, aPriority);
+    this.textProps.push(prop);
+    this.applyProperties();
+    return prop;
+  },
+
+  /**
+   * Reapply all the properties in this rule, and update their
+   * computed styles.  Will re-mark overridden properties.
+   */
+  applyProperties: function Rule_applyProperties()
+  {
+    for each (let prop in this.textProps) {
+      if (!prop.enabled) {
+        continue;
+      }
+
+      this.style.setProperty(prop.name, prop.value, prop.priority);
+      // Refresh the property's value from the style, to reflect
+      // any changes made during parsing.
+      prop.value = this.style.getPropertyValue(prop.name);
+      prop.priority = this.style.getPropertyPriority(prop.name);
+      prop.updateComputed();
+    }
+
+    this.elementStyle.markOverridden();
+  },
+
+  /**
+   * Renames a property.
+   *
+   * @param {TextProperty} aProperty
+   *        The property to rename.
+   * @param {string} aName
+   *        The new property name (such as "background" or "border-top").
+   */
+  setPropertyName: function Rule_setPropertyName(aProperty, aName)
+  {
+    if (aName === aProperty.name) {
+      return;
+    }
+    this.style.removeProperty(aProperty.name);
+    aProperty.name = aName;
+    this.applyProperties();
+  },
+
+  /**
+   * Sets the value and priority of a property.
+   *
+   * @param {TextProperty} aProperty
+   *        The property to manipulate.
+   * @param {string} aValue
+   *        The property's value (not including priority).
+   * @param {string} aPriority
+   *        The property's priority (either "important" or an empty string).
+   */
+  setPropertyValue: function Rule_setPropertyValue(aProperty, aValue, aPriority)
+  {
+    if (aValue === aProperty.value && aPriority === aProperty.priority) {
+      return;
+    }
+    aProperty.value = aValue;
+    aProperty.priority = aPriority;
+    this.applyProperties();
+  },
+
+  /**
+   * Disables or enables given TextProperty.
+   */
+  setPropertyEnabled: function Rule_enableProperty(aProperty, aValue)
+  {
+    aProperty.enabled = !!aValue;
+    if (!aProperty.enabled) {
+      this.style.removeProperty(aProperty.name);
+    }
+    this.applyProperties();
+  },
+
+  /**
+   * Remove a given TextProperty from the rule and update the rule
+   * accordingly.
+   */
+  removeProperty: function Rule_removeProperty(aProperty)
+  {
+    this.textProps = this.textProps.filter(function(prop) prop != aProperty);
+    this.style.removeProperty(aProperty);
+    // Need to re-apply properties in case removing this TextProperty
+    // exposes another one.
+    this.applyProperties();
+  },
+
+  /**
+   * Get the list of TextProperties from the style.  Needs
+   * to parse the style's cssText.
+   */
+  _getTextProperties: function Rule_getTextProperties()
+  {
+    this.textProps = [];
+    let lines = this.style.cssText.match(CSS_LINE_RE);
+    for each (let line in lines) {
+      let matches = CSS_PROP_RE.exec(line);
+      if(!matches || !matches[2])
+        continue;
+
+      let prop = new TextProperty(this, matches[1], matches[2], matches[3] || "");
+      this.textProps.push(prop);
+    }
+  },
+}
+
+/**
+ * A single property in a rule's cssText.
+ *
+ * @param {Rule} aRule
+ *        The rule this TextProperty came from.
+ * @param {string} aName
+ *        The text property name (such as "background" or "border-top").
+ * @param {string} aValue
+ *        The property's value (not including priority).
+ * @param {string} aPriority
+ *        The property's priority (either "important" or an empty string).
+ *
+ */
+function TextProperty(aRule, aName, aValue, aPriority)
+{
+  this.rule = aRule;
+  this.name = aName;
+  this.value = aValue;
+  this.priority = aPriority;
+  this.enabled = true;
+  this.updateComputed();
+}
+
+TextProperty.prototype = {
+  /**
+   * Update the editor associated with this text property,
+   * if any.
+   */
+  updateEditor: function TextProperty_updateEditor()
+  {
+    if (this.editor) {
+      this.editor.update();
+    }
+  },
+
+  /**
+   * Update the list of computed properties for this text property.
+   */
+  updateComputed: function TextProperty_updateComputed()
+  {
+    if (!this.name) {
+      return;
+    }
+
+    // This is a bit funky.  To get the list of computed properties
+    // for this text property, we'll set the property on a dummy element
+    // and see what the computed style looks like.
+    let dummyElement = this.rule.elementStyle.dummyElement;
+    let dummyStyle = dummyElement.style;
+    dummyStyle.cssText = "";
+    dummyStyle.setProperty(this.name, this.value, this.priority);
+
+    this.computed = [];
+    for (let i = 0, n = dummyStyle.length; i < n; i++) {
+      let prop = dummyStyle.item(i);
+      this.computed.push({
+        name: prop,
+        value: dummyStyle.getPropertyValue(prop),
+        priority: dummyStyle.getPropertyPriority(prop),
+      });
+    }
+  },
+
+  setValue: function TextProperty_setValue(aValue, aPriority)
+  {
+    this.rule.setPropertyValue(this, aValue, aPriority);
+    this.updateEditor();
+  },
+
+  setName: function TextProperty_setName(aName)
+  {
+    this.rule.setPropertyName(this, aName);
+    this.updateEditor();
+  },
+
+  setEnabled: function TextProperty_setEnabled(aValue)
+  {
+    this.rule.setPropertyEnabled(this, aValue);
+    this.updateEditor();
+  },
+
+  remove: function TextProperty_remove()
+  {
+    this.rule.removeProperty(this);
+  }
+}
+
+
+/**
+ * View hierarchy mostly follows the model hierarchy.
+ *
+ * CssRuleView:
+ *   Owns an ElementStyle and creates a list of RuleEditors for its
+ *    Rules.
+ * RuleEditor:
+ *   Owns a Rule object and creates a list of TextPropertyEditors
+ *     for its TextProperties.
+ *   Manages creation of new text properties.
+ * TextPropertyEditor:
+ *   Owns a TextProperty object.
+ *   Manages changes to the TextProperty.
+ *   Can be expanded to display computed properties.
+ *   Can mark a property disabled or enabled.
+ */
+
+/**
+ * CssRuleView is a view of the style rules and declarations that
+ * apply to a given element.  After construction, the 'element'
+ * property will be available with the user interface.
+ *
+ * @param Document aDocument
+ *        The document that will contain the rule view.
+ * @constructor
+ */
+function CssRuleView(aDoc)
+{
+  this.doc = aDoc;
+
+  this.element = this.doc.createElementNS(HTML_NS, "div");
+  this.element.setAttribute("tabindex", "0");
+  this.element.classList.add("ruleview");
+}
+
+CssRuleView.prototype = {
+  // The element that we're inspecting.
+  _viewedElement: null,
+
+  /**
+   * Update the highlighted element.
+   *
+   * @param {nsIDOMElement} aElement
+   *        The node whose style rules we'll inspect.
+   */
+  highlight: function CssRuleView_highlight(aElement)
+  {
+    if (this._viewedElement === aElement) {
+      return;
+    }
+
+    this.clear();
+
+    this._viewedElement = aElement;
+    if (!this._viewedElement) {
+      return;
+    }
+
+    this._elementStyle = new ElementStyle(aElement);
+    this._createEditors();
+  },
+
+  /**
+   * Clear the rule view.
+   */
+  clear: function CssRuleView_clear()
+  {
+    while (this.element.hasChildNodes()) {
+      this.element.removeChild(this.element.lastChild);
+    }
+    this._viewedElement = null;
+    this._elementStyle = null;
+  },
+
+  /**
+   * Creates editor UI for each of the rules in _elementStyle.
+   */
+  _createEditors: function CssRuleView_createEditors()
+  {
+    for each (let rule in this._elementStyle.rules) {
+      // Don't hold a reference to this editor beyond the one held
+      // by the node.
+      let editor = new RuleEditor(this.doc, rule);
+      this.element.appendChild(editor.element);
+    }
+  },
+};
+
+/**
+ * Create a RuleEditor.
+ *
+ * @param object aDoc
+ *        The document holding this rule editor.
+ * @param Rule aRule
+ *        The Rule object we're editing.
+ * @constructor
+ */
+function RuleEditor(aDoc, aRule)
+{
+  this.doc = aDoc;
+  this.rule = aRule;
+
+  this._onNewProperty = this._onNewProperty.bind(this);
+
+  this._create();
+}
+
+RuleEditor.prototype = {
+  _create: function RuleEditor_create()
+  {
+    this.element = this.doc.createElementNS(HTML_NS, "div");
+    this.element._ruleEditor = this;
+
+    // Add the source link.
+    let source = createChild(this.element, "div", {
+      class: "ruleview-rule-source",
+      textContent: this.rule.title
+    });
+
+    let code = createChild(this.element, "div", {
+      class: "ruleview-code"
+    });
+
+    let header = createChild(code, "div", {});
+
+    let selectors = createChild(header, "span", {
+      class: "ruleview-selector",
+      textContent: this.rule.selectorText
+    });
+    appendText(header, " {");
+
+    this.propertyList = createChild(code, "ul", {
+      class: "ruleview-propertylist"
+    });
+
+    for each (let prop in this.rule.textProps) {
+      let propEditor = new TextPropertyEditor(this, prop);
+      this.propertyList.appendChild(propEditor.element);
+    }
+
+    this.closeBrace = createChild(code, "div", {
+      class: "ruleview-ruleclose",
+      tabindex: "0",
+      textContent: "}"
+    });
+
+    // We made the close brace focusable, tabbing to it
+    // or clicking on it should start the new property editor.
+    this.closeBrace.addEventListener("focus", function() {
+      this.newProperty();
+    }.bind(this), true);
+  },
+
+  /**
+   * Create a text input for a property name.  If a non-empty property
+   * name is given, we'll create a real TextProperty and add it to the
+   * rule.
+   */
+  newProperty: function RuleEditor_newProperty()
+  {
+    // While we're editing a new property, it doesn't make sense to
+    // start a second new property editor, so disable focusing the
+    // close brace for now.
+    this.closeBrace.removeAttribute("tabindex");
+
+    this.newPropItem = createChild(this.propertyList, "li", {
+      class: "ruleview-property ruleview-newproperty",
+    });
+
+    this.newPropSpan = createChild(this.newPropItem, "span", {
+      class: "ruleview-propertyname"
+    });
+
+    new InplaceEditor({
+      element: this.newPropSpan,
+      done: this._onNewProperty,
+      advanceChars: ":"
+    });
+  },
+
+  _onNewProperty: function RuleEditor_onNewProperty(aValue, aCommit)
+  {
+    // We're done, make the close brace focusable again.
+    this.closeBrace.setAttribute("tabindex", "0");
+
+    this.propertyList.removeChild(this.newPropItem);
+    delete this.newPropItem;
+    delete this.newPropSpan;
+
+    if (!aValue || !aCommit) {
+      return;
+    }
+
+    // Create an empty-valued property and start editing it.
+    let prop = this.rule.createProperty(aValue, "", "");
+    let editor = new TextPropertyEditor(this, prop);
+    this.propertyList.appendChild(editor.element);
+    editor.valueSpan.focus();
+  },
+};
+
+/**
+ * Create a TextPropertyEditor.
+ *
+ * @param {RuleEditor} aRuleEditor
+ *        The rule editor that owns this TextPropertyEditor.
+ * @param {TextProperty} aProperty
+ *        The text property to edit.
+ * @constructor
+ */
+function TextPropertyEditor(aRuleEditor, aProperty)
+{
+  this.doc = aRuleEditor.doc;
+  this.prop = aProperty;
+  this.prop.editor = this;
+
+  this._onEnableClicked = this._onEnableClicked.bind(this);
+  this._onExpandClicked = this._onExpandClicked.bind(this);
+  this._onStartEditing = this._onStartEditing.bind(this);
+  this._onNameDone = this._onNameDone.bind(this);
+  this._onValueDone = this._onValueDone.bind(this);
+
+  this._create();
+  this.update();
+}
+
+TextPropertyEditor.prototype = {
+  get editing() {
+    return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor);
+  },
+
+  /**
+   * Create the property editor's DOM.
+   */
+  _create: function TextPropertyEditor_create()
+  {
+    this.element = this.doc.createElementNS(HTML_NS, "li");
+    this.element.classList.add("ruleview-property");
+
+    // The enable checkbox will disable or enable the rule.
+    this.enable = createChild(this.element, "input", {
+      class: "ruleview-enableproperty",
+      type: "checkbox",
+      tabindex: "-1"
+    });
+    this.enable.addEventListener("click", this._onEnableClicked, true);
+
+    // Click to expand the computed properties of the text property.
+    this.expander = createChild(this.element, "span", {
+      class: "ruleview-expander"
+    });
+    this.expander.addEventListener("click", this._onExpandClicked, true);
+
+    // Property name, editable when focused.  Property name
+    // is committed when the editor is unfocused.
+    this.nameSpan = createChild(this.element, "span", {
+      class: "ruleview-propertyname",
+      tabindex: "0",
+    });
+    editableField({
+      start: this._onStartEditing,
+      element: this.nameSpan,
+      done: this._onNameDone,
+      advanceChars: ':'
+    });
+
+    appendText(this.element, ": ");
+
+    // Property value, editable when focused.  Changes to the
+    // property value are applied as they are typed, and reverted
+    // if the user presses escape.
+    this.valueSpan = createChild(this.element, "span", {
+      class: "ruleview-propertyvalue",
+      tabindex: "0",
+    });
+
+    editableField({
+      start: this._onStartEditing,
+      element: this.valueSpan,
+      done: this._onValueDone,
+      advanceChars: ';'
+    });
+
+    // Save the initial value as the last committed value,
+    // for restoring after pressing escape.
+    this.committed = { name: this.prop.name,
+                       value: this.prop.value,
+                       priority: this.prop.priority };
+
+    appendText(this.element, ";");
+
+    // Holds the viewers for the computed properties.
+    // will be populated in |_updateComputed|.
+    this.computed = createChild(this.element, "ul", {
+      class: "ruleview-computedlist",
+    });
+  },
+
+  /**
+   * Populate the span based on changes to the TextProperty.
+   */
+  update: function TextPropertyEditor_update()
+  {
+    if (this.prop.enabled) {
+      this.enable.style.removeProperty("visibility");
+      this.enable.setAttribute("checked", "");
+    } else {
+      this.enable.style.visibility = "visible";
+      this.enable.removeAttribute("checked");
+    }
+
+    if (this.prop.overridden && !this.editing) {
+      this.element.classList.add("ruleview-overridden");
+    } else {
+      this.element.classList.remove("ruleview-overridden");
+    }
+
+    this.nameSpan.textContent = this.prop.name;
+
+    // Combine the property's value and priority into one string for
+    // the value.
+    let val = this.prop.value;
+    if (this.prop.priority) {
+      val += " !" + this.prop.priority;
+    }
+    this.valueSpan.textContent = val;
+
+    // Populate the computed styles.
+    this._updateComputed();
+  },
+
+  _onStartEditing: function TextPropertyEditor_onStartEditing()
+  {
+    this.element.classList.remove("ruleview-overridden");
+  },
+
+  /**
+   * Populate the list of computed styles.
+   */
+  _updateComputed: function TextPropertyEditor_updateComputed()
+  {
+    // Clear out existing viewers.
+    while (this.computed.hasChildNodes()) {
+      this.computed.removeChild(this.computed.lastChild);
+    }
+
+    let showExpander = false;
+    for each (let computed in this.prop.computed) {
+      // Don't bother to duplicate information already
+      // shown in the text property.
+      if (computed.name === this.prop.name) {
+        continue;
+      }
+
+      showExpander = true;
+
+      let li = createChild(this.computed, "li", {
+        class: "ruleview-computed"
+      });
+
+      if (computed.overridden) {
+        li.classList.add("ruleview-overridden");
+      }
+
+      createChild(li, "span", {
+        class: "ruleview-propertyname",
+        textContent: computed.name
+      });
+      appendText(li, ": ");
+      createChild(li, "span", {
+        class: "ruleview-propertyvalue",
+        textContent: computed.value
+      });
+      appendText(li, ";");
+    }
+
+    // Show or hide the expander as needed.
+    if (showExpander) {
+      this.expander.style.visibility = "visible";
+    } else {
+      this.expander.style.visibility = "hidden";
+    }
+  },
+
+  /**
+   * Handles clicks on the disabled property.
+   */
+  _onEnableClicked: function TextPropertyEditor_onEnableClicked()
+  {
+    this.prop.setEnabled(this.enable.checked);
+  },
+
+  /**
+   * Handles clicks on the computed property expander.
+   */
+  _onExpandClicked: function TextPropertyEditor_onExpandClicked()
+  {
+    this.expander.classList.toggle("styleinspector-open");
+    this.computed.classList.toggle("styleinspector-open");
+  },
+
+  /**
+   * Called when the property name's inplace editor is closed.
+   * Ignores the change if the user pressed escape, otherwise
+   * commits it.
+   *
+   * @param {string} aValue
+   *        The value contained in the editor.
+   * @param {boolean} aCommit
+   *        True if the change should be applied.
+   */
+  _onNameDone: function TextPropertyEditor_onNameDone(aValue, aCommit)
+  {
+    if (!aCommit) {
+      return;
+    }
+    if (!aValue) {
+      this.prop.remove();
+      this.element.parentNode.removeChild(this.element);
+      return;
+    }
+    this.prop.setName(aValue);
+  },
+
+  /**
+   * Pull priority (!important) out of the value provided by a
+   * value editor.
+   *
+   * @param {string} aValue
+   *        The value from the text editor.
+   * @return an object with 'value' and 'priority' properties.
+   */
+  _parseValue: function TextPropertyEditor_parseValue(aValue)
+  {
+    let [value, priority] = aValue.split("!", 2);
+    return {
+      value: value.trim(),
+      priority: (priority ? priority.trim() : "")
+    };
+  },
+
+  /**
+   * Called when a value editor closes.  If the user pressed escape,
+   * revert to the value this property had before editing.
+   *
+   * @param {string} aValue
+   *        The value contained in the editor.
+   * @param {boolean} aCommit
+   *        True if the change should be applied.
+   */
+   _onValueDone: function PropertyEditor_onValueDone(aValue, aCommit)
+  {
+    if (aCommit) {
+      let val = this._parseValue(aValue);
+      this.prop.setValue(val.value, val.priority);
+      this.committed.value = this.prop.value;
+      this.committed.priority = this.prop.priority;
+    } else {
+      this.prop.setValue(this.committed.value, this.committed.priority);
+    }
+  },
+};
+
+/**
+ * Mark a span editable.  |editableField| will listen for the span to
+ * be focused and create an InlineEditor to handle text input.
+ * Changes will be committed when the InlineEditor's input is blurred
+ * or dropped when the user presses escape.
+ *
+ * @param {object} aOptions
+ *    Options for the editable field, including:
+ *    {Element} element:
+ *      (required) The span to be edited on focus.
+ *    {function} start:
+ *       Will be called when the inplace editor is initialized.
+ *    {function} change:
+ *       Will be called when the text input changes.  Will be called
+ *       with the current value of the text input.
+ *    {function} done:
+ *       Called when input is committed or blurred.  Called with
+ *       current value and a boolean telling the caller whether to
+ *       commit the change.
+ *    {string} advanceChars:
+ *       If any characters in advanceChars are typed, focus will advance
+ *       to the next element.
+ */
+function editableField(aOptions)
+{
+  aOptions.element.addEventListener("focus", function() {
+    new InplaceEditor(aOptions);
+  }, false);
+}
+var _editableField = editableField;
+
+function InplaceEditor(aOptions)
+{
+  this.elt = aOptions.element;
+  this.elt.inplaceEditor = this;
+
+  this.change = aOptions.change;
+  this.done = aOptions.done;
+  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
+  this.doc = this.elt.ownerDocument;
+
+  this._onBlur = this._onBlur.bind(this);
+  this._onKeyPress = this._onKeyPress.bind(this);
+  this._onInput = this._onInput.bind(this);
+
+  this._createInput();
+  this._autosize();
+
+  // Pull out character codes for advanceChars, listing the
+  // characters that should trigger a blur.
+  this._advanceCharCodes = {};
+  let advanceChars = aOptions.advanceChars || '';
+  for (let i = 0; i < advanceChars.length; i++) {
+    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
+  }
+
+  // Hide the provided element and add our editor.
+  this.originalDisplay = this.elt.style.display;
+  this.elt.style.display = "none";
+  this.elt.parentNode.insertBefore(this.input, this.elt);
+
+  this.input.select();
+  this.input.focus();
+
+  this.input.addEventListener("blur", this._onBlur, false);
+  this.input.addEventListener("keypress", this._onKeyPress, false);
+  this.input.addEventListener("input", this._onInput, false);
+
+  if (aOptions.start) {
+    aOptions.start();
+  }
+}
+
+InplaceEditor.prototype = {
+  _createInput: function InplaceEditor_createEditor()
+  {
+    this.input = this.doc.createElementNS(HTML_NS, "input");
+    this.input.inplaceEditor = this;
+    this.input.classList.add("styleinspector-propertyeditor");
+    this.input.value = this.initial;
+
+    copyTextStyles(this.elt, this.input);
+  },
+
+  /**
+   * Get rid of the editor.
+   */
+  _clear: function InplaceEditor_clear()
+  {
+    this.input.removeEventListener("blur", this._onBlur, false);
+    this.input.removeEventListener("keypress", this._onKeyPress, false);
+    this.input.removeEventListener("oninput", this._onInput, false);
+    this._stopAutosize();
+
+    this.elt.parentNode.removeChild(this.input);
+    this.elt.style.display = this.originalDisplay;
+    this.input = null;
+
+    delete this.elt.inplaceEditor;
+    delete this.elt;
+  },
+
+  /**
+   * Keeps the editor close to the size of its input string.  This is pretty
+   * crappy, suggestions for improvement welcome.
+   */
+  _autosize: function InplaceEditor_autosize()
+  {
+    // Create a hidden, absolutely-positioned span to measure the text
+    // in the input.  Boo.
+
+    // We can't just measure the original element because a) we don't
+    // change the underlying element's text ourselves (we leave that
+    // up to the client), and b) without tweaking the style of the
+    // original element, it might wrap differently or something.
+    this._measurement = this.doc.createElementNS(HTML_NS, "span");
+    this.elt.parentNode.appendChild(this._measurement);
+    let style = this._measurement.style;
+    style.visibility = "hidden";
+    style.position = "absolute";
+    style.top = "0";
+    style.left = "0";
+    copyTextStyles(this.input, this._measurement);
+    this._updateSize();
+  },
+
+  /**
+   * Clean up the mess created by _autosize().
+   */
+  _stopAutosize: function InplaceEditor_stopAutosize()
+  {
+    if (!this._measurement) {
+      return;
+    }
+    this._measurement.parentNode.removeChild(this._measurement);
+    delete this._measurement;
+  },
+
+  /**
+   * Size the editor to fit its current contents.
+   */
+  _updateSize: function InplaceEditor_updateSize()
+  {
+    // Replace spaces with non-breaking spaces.  Otherwise setting
+    // the span's textContent will collapse spaces and the measurement
+    // will be wrong.
+    this._measurement.textContent = this.input.value.replace(' ', '\u00a0', 'g');
+
+    // We add a bit of padding to the end.  Should be enough to fit
+    // any letter that could be typed, otherwise we'll scroll before
+    // we get a chance to resize.  Yuck.
+    let width = this._measurement.offsetWidth + 10;
+
+    this.input.style.width = width + "px";
+  },
+
+  /**
+   * Handle loss of focus by calling the client's done handler and
+   * clearing out.
+   */
+  _onBlur: function InplaceEditor_onBlur(aEvent)
+  {
+    if (this.done) {
+      this.done(this.cancelled ? this.initial : this.input.value.trim(),
+                !this.cancelled);
+    }
+    this._clear();
+  },
+
+  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
+  {
+    let prevent = false;
+    if (aEvent.charCode in this._advanceCharCodes
+       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
+      // Focus the next element, triggering a blur which
+      // will eventually shut us down (making return roughly equal
+      // tab).
+      prevent = true;
+      moveFocus(this.input.ownerDocument.defaultView, FOCUS_FORWARD);
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
+      // Cancel and blur ourselves.  |_onBlur| will call the user's
+      // done handler for us.
+      prevent = true;
+      this.cancelled = true;
+      this.input.blur();
+    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
+      // No need for leading spaces here.  This is particularly
+      // noticable when adding a property: it's very natural to type
+      // <name>: (which advances to the next property) then spacebar.
+      prevent = !this.input.value;
+    }
+
+    if (prevent) {
+      aEvent.preventDefault();
+    }
+  },
+
+  /**
+   * Handle changes the input text.
+   */
+  _onInput: function InplaceEditor_onInput(aEvent)
+  {
+    // Update size if we're autosizing.
+    if (this._measurement) {
+      this._updateSize();
+    }
+
+    // Call the user's change handler if available.
+    if (this.change) {
+      this.change(this.input.value.trim());
+    }
+  }
+};
+
+/**
+ * Helper functions
+ */
+
+/**
+ * Create a child element with a set of attributes.
+ *
+ * @param {Element} aParent
+ *        The parent node.
+ * @param {string} aTag
+ *        The tag name.
+ * @param {object} aAttributes
+ *        A set of attributes to set on the node.
+ */
+function createChild(aParent, aTag, aAttributes)
+{
+  let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
+  for (let attr in aAttributes) {
+    if (aAttributes.hasOwnProperty(attr)) {
+      if (attr === "textContent") {
+        elt.textContent = aAttributes[attr];
+      } else {
+        elt.setAttribute(attr, aAttributes[attr]);
+      }
+    }
+  }
+  aParent.appendChild(elt);
+  return elt;
+}
+
+/**
+ * Append a text node to an element.
+ */
+function appendText(aParent, aText)
+{
+  aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
+}
+
+/**
+ * Copy text-related styles from one element to another.
+ */
+function copyTextStyles(aFrom, aTo)
+{
+  let win = aFrom.ownerDocument.defaultView;
+  let style = win.getComputedStyle(aFrom);
+  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
+  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
+  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
+  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
+}
+
+/**
+ * Trigger a focus change similar to pressing tab/shift-tab.
+ */
+function moveFocus(aWin, aDirection)
+{
+  let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+  fm.moveFocus(aWin, null, aDirection, 0);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/cssruleview.xhtml
@@ -0,0 +1,53 @@
+<!DOCTYPE html [
+  <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+  %htmlDTD;
+  <!ENTITY % inspectorDTD SYSTEM "chrome://browser/locale/styleinspector.dtd">
+  %inspectorDTD;
+]>
+<!-- ***** 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 the Mozilla Inspector Module.
+   -
+   - 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):
+   -   Dave Camp (dcamp@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 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 ***** -->
+<html xmlns="http://www.w3.org/1999/xhtml"
+  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<head>
+  <meta http-equiv="Content-Type"
+    content="application/xhtml+xml; charset=UTF-8" />
+  <link rel="stylesheet" type="text/css"
+    href="chrome://browser/content/devtools/styleinspector.css" />
+  <link rel="stylesheet" type="text/css"
+    href="chrome://browser/skin/devtools/csshtmltree.css" />
+</head>
+<body role="application" id="ruleview-body"></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/styleinspector.css
@@ -0,0 +1,40 @@
+/* ***** 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 the Mozilla Inspector Module.
+ *
+ * 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):
+ *   Dave Camp <dcamp@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+.ruleview-computedlist:not(.styleinspector-open) {
+  display: none;
+}
--- a/browser/devtools/styleinspector/test/browser/Makefile.in
+++ b/browser/devtools/styleinspector/test/browser/Makefile.in
@@ -47,16 +47,20 @@ include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   browser_styleinspector.js \
   browser_styleinspector_webconsole.js \
   browser_bug683672.js \
   browser_styleinspector_bug_672746_default_styles.js \
   browser_styleinspector_bug_672744_search_filter.js \
   browser_bug_692400_element_style.js \
+  browser_ruleview_editor.js \
+  browser_ruleview_manipulation.js \
+  browser_ruleview_override.js \
+  browser_ruleview_ui.js \
   head.js \
   $(NULL)
 
 _BROWSER_TEST_PAGES = \
   browser_styleinspector_webconsole.htm \
   browser_bug683672.html \
   $(NULL)
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser/browser_ruleview_editor.js
@@ -0,0 +1,121 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+
+let doc = content.document;
+
+function expectDone(aValue, aCommit, aNext)
+{
+  return function(aDoneValue, aDoneCommit) {
+    dump("aDoneValue: " + aDoneValue + " commit: " + aDoneCommit + "\n");
+
+    is(aDoneValue, aValue, "Should get expected value");
+    is(aDoneCommit, aDoneCommit, "Should get expected commit value");
+    aNext();
+  }
+}
+
+function clearBody()
+{
+  doc.body.innerHTML = "";
+}
+
+function createSpan()
+{
+  let span = doc.createElement("span");
+  span.setAttribute("tabindex", "0");
+  span.textContent = "Edit Me!";
+  doc.body.appendChild(span);
+  return span;
+}
+
+function testReturnCommit()
+{
+  clearBody();
+  let span = createSpan();
+  _editableField({
+    element: span,
+    initial: "explicit initial",
+    start: function() {
+      is(span.inplaceEditor.input.value, "explicit initial", "Explicit initial value should be used.");
+      span.inplaceEditor.input.value = "Test Value";
+      EventUtils.sendKey("return", span.inplaceEditor.input);
+    },
+    done: expectDone("Test Value", true, testBlurCommit)
+  });
+  span.focus();
+}
+
+function testBlurCommit()
+{
+  clearBody();
+  let span = createSpan();
+  _editableField({
+    element: span,
+    start: function() {
+      is(span.inplaceEditor.input.value, "Edit Me!", "textContent of the span used.");
+      span.inplaceEditor.input.value = "Test Value";
+      span.inplaceEditor.input.blur();
+    },
+    done: expectDone("Test Value", true, testAdvanceCharCommit)
+  });
+  span.focus();
+}
+
+function testAdvanceCharCommit()
+{
+  clearBody();
+  let span = createSpan();
+  _editableField({
+    element: span,
+    advanceChars: ":",
+    start: function() {
+      let input = span.inplaceEditor.input;
+      for each (let ch in "Test:") {
+        EventUtils.sendChar(ch, input);
+      }
+    },
+    done: expectDone("Test", true, testEscapeCancel)
+  });
+  span.focus();
+}
+
+function testEscapeCancel()
+{
+  clearBody();
+  let span = createSpan();
+  _editableField({
+    element: span,
+    initial: "initial text",
+    start: function() {
+      span.inplaceEditor.input.value = "Test Value";
+      EventUtils.sendKey("escape", span.inplaceEditor.input);
+    },
+    done: expectDone("initial text", false, finishTest)
+  });
+  span.focus();
+}
+
+
+function finishTest()
+{
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    waitForFocus(testReturnCommit, content);
+  }, true);
+
+  content.location = "data:text/html,inline editor tests";
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser/browser_ruleview_manipulation.js
@@ -0,0 +1,70 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+
+let doc;
+
+function simpleOverride()
+{
+  doc.body.innerHTML = '<div id="testid">Styled Node</div>';
+  let element = doc.getElementById("testid");
+  let elementStyle = new _ElementStyle(element);
+
+  let elementRule = elementStyle.rules[0];
+  let firstProp = elementRule.createProperty("background-color", "green", "");
+  let secondProp = elementRule.createProperty("background-color", "blue", "");
+  is(elementRule.textProps[0], firstProp, "Rules should be in addition order.");
+  is(elementRule.textProps[1], secondProp, "Rules should be in addition order.");
+
+  is(element.style.getPropertyValue("background-color"), "blue", "Second property should have been used.");
+
+  secondProp.remove();
+  is(element.style.getPropertyValue("background-color"), "green", "After deleting second property, first should be used.");
+
+  secondProp = elementRule.createProperty("background-color", "blue", "");
+  is(element.style.getPropertyValue("background-color"), "blue", "New property should be used.");
+
+  is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
+  is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
+
+  secondProp.setEnabled(false);
+  is(element.style.getPropertyValue("background-color"), "green", "After disabling second property, first value should be used");
+
+  firstProp.setEnabled(false);
+  is(element.style.getPropertyValue("background-color"), "", "After disabling both properties, value should be empty.");
+
+  secondProp.setEnabled(true);
+  is(element.style.getPropertyValue("background-color"), "blue", "Value should be set correctly after re-enabling");
+
+  firstProp.setEnabled(true);
+  is(element.style.getPropertyValue("background-color"), "blue", "Re-enabling an earlier property shouldn't make it override a later property.");
+  is(elementRule.textProps[0], firstProp, "Rules shouldn't have switched places.");
+  is(elementRule.textProps[1], secondProp, "Rules shouldn't have switched places.");
+
+  firstProp.setValue("purple", "");
+  is(element.style.getPropertyValue("background-color"), "blue", "Modifying an earlier property shouldn't override a later property.");
+
+  finishTest();
+}
+
+function finishTest()
+{
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    waitForFocus(simpleOverride, content);
+  }, true);
+
+  content.location = "data:text/html,basic style inspector tests";
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser/browser_ruleview_override.js
@@ -0,0 +1,134 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+
+let doc;
+
+function simpleOverride()
+{
+  let style = '' +
+    '#testid {' +
+    '  background-color: blue;' +
+    '} ' +
+    '.testclass {' +
+    '  background-color: green;' +
+    '}';
+
+  let styleNode = addStyle(doc, style);
+  doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+  let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  is(idProp.name, "background-color", "First ID prop should be background-color");
+  ok(!idProp.overridden, "ID prop should not be overridden.");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  is(classProp.name, "background-color", "First class prop should be background-color");
+  ok(classProp.overridden, "Class property should be overridden.");
+
+  // Override background-color by changing the element style.
+  let elementRule = elementStyle.rules[0];
+  elementRule.createProperty("background-color", "purple", "");
+  let elementProp = elementRule.textProps[0];
+  is(classProp.name, "background-color", "First element prop should now be background-color");
+
+  ok(!elementProp.overridden, "Element style property should not be overridden");
+  ok(idProp.overridden, "ID property should be overridden");
+  ok(classProp.overridden, "Class property should be overridden");
+
+  styleNode.parentNode.removeChild(styleNode);
+
+  partialOverride();
+}
+
+function partialOverride()
+{
+  let style = '' +
+    // Margin shorthand property...
+    '.testclass {' +
+    '  margin: 2px;' +
+    '}' +
+    // ... will be partially overridden.
+    '#testid {' +
+    '  margin-left: 1px;' +
+    '}';
+
+  let styleNode = addStyle(doc, style);
+  doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+  let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden, "Class prop shouldn't be overridden, some props are still being used.");
+  for each (let computed in classProp.computed) {
+    if (computed.name.indexOf("margin-left") == 0) {
+      ok(computed.overridden, "margin-left props should be overridden.");
+    } else {
+      ok(!computed.overridden, "Non-margin-left props should not be overridden.");
+    }
+  }
+
+  styleNode.parentNode.removeChild(styleNode);
+
+  importantOverride();
+}
+
+function importantOverride()
+{
+  let style = '' +
+    // Margin shorthand property...
+    '.testclass {' +
+    '  background-color: green !important;' +
+    '}' +
+    // ... will be partially overridden.
+    '#testid {' +
+    '  background-color: blue;' +
+    '}';
+  let styleNode = addStyle(doc, style);
+  doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+
+  let elementStyle = new _ElementStyle(doc.getElementById("testid"));
+
+  let idRule = elementStyle.rules[1];
+  let idProp = idRule.textProps[0];
+  ok(idProp.overridden, "Not-important rule should be overridden.");
+
+  let classRule = elementStyle.rules[2];
+  let classProp = classRule.textProps[0];
+  ok(!classProp.overridden, "Important rule should not be overridden.");
+
+  styleNode.parentNode.removeChild(styleNode);
+
+  let elementRule = elementStyle.rules[0];
+  let elementProp = elementRule.createProperty("background-color", "purple", "important");
+  ok(classProp.overridden, "New important prop should override class property.");
+  ok(!elementProp.overridden, "New important prop should not be overriden.");
+
+  finishTest();
+}
+
+function finishTest()
+{
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    waitForFocus(simpleOverride, content);
+  }, true);
+
+  content.location = "data:text/html,basic style inspector tests";
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser/browser_ruleview_ui.js
@@ -0,0 +1,180 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource:///modules/devtools/CssRuleView.jsm");
+
+let doc;
+let ruleDialog;
+let ruleView;
+
+function waitForEditorFocus(aParent, aCallback)
+{
+  aParent.addEventListener("focus", function onFocus(evt) {
+    if (evt.target.inplaceEditor) {
+      aParent.removeEventListener("focus", onFocus, true);
+      let editor = evt.target.inplaceEditor;
+      executeSoon(function() {
+        aCallback(editor);
+      });
+    }
+  }, true);
+}
+
+function waitForEditorBlur(aEditor, aCallback)
+{
+  let input = aEditor.input;
+  input.addEventListener("blur", function onBlur() {
+    input.removeEventListener("blur", onBlur, false);
+    executeSoon(function() {
+      aCallback();
+    });
+  }, false);
+}
+
+function startTest()
+{
+  let style = '' +
+    '#testid {' +
+    '  background-color: blue;' +
+    '} ' +
+    '.testclass {' +
+    '  background-color: green;' +
+    '}';
+
+  let styleNode = addStyle(doc, style);
+  doc.body.innerHTML = '<div id="testid" class="testclass">Styled Node</div>';
+  let testElement = doc.getElementById("testid");
+
+  ruleDialog = openDialog("chrome://browser/content/devtools/cssruleview.xhtml",
+                          "cssruleviewtest",
+                          "width=200,height=350");
+  ruleDialog.addEventListener("load", function onLoad(evt) {
+    ruleDialog.removeEventListener("load", onLoad);
+    let doc = ruleDialog.document;
+    let body = doc.getElementById("ruleview-body");
+    ruleView = new CssRuleView(doc);
+    body.appendChild(ruleView.element);
+    ruleView.highlight(testElement);
+    waitForFocus(testCancelNew, ruleDialog);
+  }, true);
+}
+
+function testCancelNew()
+{
+  // Start at the beginning: start to add a rule to the element's style
+  // declaration, but leave it empty.
+
+  let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+  waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+    is(elementRuleEditor.newPropSpan.inplaceEditor, aEditor, "Next focused editor should be the new property editor.");
+    let input = aEditor.input;
+    waitForEditorBlur(aEditor, function () {
+      is(elementRuleEditor.rule.textProps.length,  0, "Should have canceled creating a new text property.");
+      ok(!elementRuleEditor.propertyList.hasChildNodes(), "Should not have any properties.");
+      testCreateNew();
+    });
+    aEditor.input.blur();
+  });
+
+  EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+                             { },
+                             ruleDialog);
+}
+
+function testCreateNew()
+{
+  // Create a new property.
+  let elementRuleEditor = ruleView.element.children[0]._ruleEditor;
+  waitForEditorFocus(elementRuleEditor.element, function onNewElement(aEditor) {
+    is(elementRuleEditor.newPropSpan.inplaceEditor, aEditor, "Next focused editor should be the new property editor.");
+    let input = aEditor.input;
+    input.value = "background-color";
+
+    waitForEditorFocus(elementRuleEditor.element, function onNewValue(aEditor) {
+      is(elementRuleEditor.rule.textProps.length,  1, "Should have created a new text property.");
+      is(elementRuleEditor.propertyList.children.length, 1, "Should have created a property editor.");
+      let textProp = elementRuleEditor.rule.textProps[0];
+      is(aEditor, textProp.editor.valueSpan.inplaceEditor, "Should be editing the value span now.");
+
+      aEditor.input.value = "purple";
+      waitForEditorBlur(aEditor, function() {
+        is(textProp.value, "purple", "Text prop should have been changed.");
+        testEditProperty();
+      });
+
+      aEditor.input.blur();
+    });
+    EventUtils.sendKey("return", input);
+  });
+
+  EventUtils.synthesizeMouse(elementRuleEditor.closeBrace, 1, 1,
+                             { },
+                             ruleDialog);
+}
+
+function testEditProperty()
+{
+  let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+  let propEditor = idRuleEditor.rule.textProps[0].editor;
+  waitForEditorFocus(propEditor.element, function onNewElement(aEditor) {
+    is(propEditor.nameSpan.inplaceEditor, aEditor, "Next focused editor should be the name editor.");
+    let input = aEditor.input;
+    waitForEditorFocus(propEditor.element, function onNewName(aEditor) {
+      input = aEditor.input;
+      is(propEditor.valueSpan.inplaceEditor, aEditor, "Focus should have moved to the value.");
+
+      waitForEditorBlur(aEditor, function() {
+        is(idRuleEditor.rule.style.getPropertyValue("border-color"), "red",
+           "border-color should have been set.");
+        testDisableProperty();
+      });
+
+      for each (let ch in "red;") {
+        EventUtils.sendChar(ch, input);
+      }
+    });
+    for each (let ch in "border-color:") {
+      EventUtils.sendChar(ch, input);
+    }
+  });
+
+  EventUtils.synthesizeMouse(propEditor.nameSpan, 1, 1,
+                             { },
+                             ruleDialog);
+}
+
+function testDisableProperty()
+{
+  let idRuleEditor = ruleView.element.children[1]._ruleEditor;
+  let propEditor = idRuleEditor.rule.textProps[0].editor;
+
+  propEditor.enable.click();
+  is(idRuleEditor.rule.style.getPropertyValue("border-color"), "", "Border-color should have been unset.");
+  propEditor.enable.click();
+  is(idRuleEditor.rule.style.getPropertyValue("border-color"), "red",
+     "Border-color should have been reset.");
+  finishTest();
+}
+
+function finishTest()
+{
+  ruleDialog.close();
+  ruleDialog = ruleView = null;
+  doc = null;
+  gBrowser.removeCurrentTab();
+  finish();
+}
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function(evt) {
+    gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, true);
+    doc = content.document;
+    waitForFocus(startTest, content);
+  }, true);
+
+  content.location = "data:text/html,basic style inspector tests";
+}
--- a/browser/devtools/styleinspector/test/browser/head.js
+++ b/browser/devtools/styleinspector/test/browser/head.js
@@ -145,16 +145,25 @@ function testLogEntry(aOutputNode, aMatc
  * @param string aString
  *        The string to find.
  */
 function findLogEntry(aString)
 {
   testLogEntry(outputNode, aString, "found " + aString);
 }
 
+function addStyle(aDocument, aString)
+{
+  let node = aDocument.createElement('style');
+  node.setAttribute("type", "text/css");
+  node.textContent = aString;
+  aDocument.getElementsByTagName("head")[0].appendChild(node);
+  return node;
+}
+
 function openConsole()
 {
   HUDService.activateHUDForContext(tab);
 }
 
 function closeConsole()
 {
   HUDService.deactivateHUDForContext(tab);
--- a/browser/devtools/webconsole/HUDService.jsm
+++ b/browser/devtools/webconsole/HUDService.jsm
@@ -80,16 +80,22 @@ XPCOMUtils.defineLazyGetter(this, "gcli"
 });
 
 XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () {
   var obj = {};
   Cu.import("resource:///modules/devtools/StyleInspector.jsm", obj);
   return obj.StyleInspector;
 });
 
+XPCOMUtils.defineLazyGetter(this, "CssRuleView", function() {
+  let tmp = {};
+  Cu.import("resource:///modules/devtools/CssRuleView.jsm", tmp);
+  return tmp.CssRuleView;
+});
+
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function () {
   var obj = {};
   Cu.import("resource://gre/modules/NetUtil.jsm", obj);
   return obj.NetUtil;
 });
 
 XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () {
   var obj = {};
@@ -4595,16 +4601,63 @@ function JSTermHelper(aJSTerm)
         styleInspector.panel.setAttribute("hudToolId", aJSTerm.hudId);
         styleInspector.open(aNode);
       });
     } else {
       aJSTerm.writeOutput(errstr + "\n", CATEGORY_OUTPUT, SEVERITY_ERROR);
     }
   };
 
+  aJSTerm.sandbox.inspectrules = function JSTH_inspectrules(aNode)
+  {
+    aJSTerm.helperEvaluated = true;
+    let doc = aJSTerm.parentNode.ownerDocument;
+    let win = doc.defaultView;
+    let panel = createElement(doc, "panel", {
+      label: "CSS Rules",
+      titlebar: "normal",
+      noautofocus: "true",
+      noautohide: "true",
+      close: "true",
+      width: 350,
+      height: (win.screen.height / 2)
+    });
+
+    let iframe = createAndAppendElement(panel, "iframe", {
+      src: "chrome://browser/content/devtools/cssruleview.xhtml",
+      flex: "1",
+    });
+
+    panel.addEventListener("load", function onLoad() {
+      panel.removeEventListener("load", onLoad, true);
+      let doc = iframe.contentDocument;
+      let view = new CssRuleView(doc);
+      let body = doc.getElementById("ruleview-body");
+      body.appendChild(view.element);
+      view.highlight(aNode);
+    }, true);
+
+    let parent = doc.getElementById("mainPopupSet");
+    parent.appendChild(panel);
+
+    panel.addEventListener("popuphidden", function onHide() {
+      panel.removeEventListener("popuphidden", onHide);
+      parent.removeChild(panel);
+    });
+
+    let footer = createElement(doc, "hbox", { align: "end" });
+    createAndAppendElement(footer, "spacer", { flex: 1});
+    createAndAppendElement(footer, "resizer", { dir: "bottomend" });
+    panel.appendChild(footer);
+
+    let anchor = win.gBrowser.selectedBrowser;
+    panel.openPopup(anchor, "end_before", 0, 0, false, false);
+
+  }
+
   /**
    * Prints aObject to the output.
    *
    * @param object aObject
    *        Object to print to the output.
    * @returns void
    */
   aJSTerm.sandbox.pprint = function JSTH_pprint(aObject)
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -208,19 +208,20 @@ 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 inspectPanelTitle.label        "HTML">
+<!ENTITY inspectButton.label            "Inspect">
+<!ENTITY inspectButton.accesskey        "I">
+<!ENTITY inspectCloseButton.tooltiptext "Close Inspector">
 
 <!ENTITY getMoreDevtoolsCmd.label        "Get More Tools">
 <!ENTITY getMoreDevtoolsCmd.accesskey    "M">
 
 <!ENTITY fileMenu.label         "File"> 
 <!ENTITY fileMenu.accesskey       "F">
 <!ENTITY newNavigatorCmd.label        "New Window">
 <!ENTITY newNavigatorCmd.key        "N">
--- a/browser/themes/gnomestripe/browser/browser.css
+++ b/browser/themes/gnomestripe/browser/browser.css
@@ -1946,26 +1946,26 @@ panel[dimmed="true"] {
 }
 
 /* Highlighter */
 
 .highlighter-veil {
   background-color: rgba(0, 0, 0, 0.5);
 }
 
-#highlighter-close-button {
-  list-style-image: url("chrome://browser/skin/KUI-close.png");
-  top: 12px;
-  right: 12px;
-  cursor: pointer;
+#highlighter-closebutton {
+  list-style-image: url("moz-icon://stock/gtk-close?size=menu");
+  margin-top: 0;
+  margin-bottom: 0;
 }
 
-#highlighter-close-button:-moz-locale-dir(rtl)  {
-  right: auto;
-  left: 12px;
+#highlighter-closebutton > .toolbarbutton-icon {
+  /* XXX Buttons have padding in widget/ that we don't want here but can't override with good CSS, so we must
+     use evil CSS to give the impression of smaller content */
+  margin: -4px;
 }
 
 #highlighter-veil-transparentbox {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
   outline: 1px dashed rgba(255,255,255,0.5);
   outline-offset: -1px;
 }
 
@@ -1973,22 +1973,27 @@ panel[dimmed="true"] {
   box-shadow: 0 0 0 1px black;
   outline-color: white;
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   -moz-appearance: none;
-  padding: 0 3px 4px;
+  padding: 4px 3px;
   border-top: 1px solid hsla(210, 8%, 5%, .65);
   box-shadow: 0 1px 0 0 hsla(210, 16%, 76%, .2) inset;
   background-image: -moz-linear-gradient(top, hsl(210,11%,36%), hsl(210,11%,18%));
 }
 
+#inspector-toolbar[treepanel-open] {
+  padding-top: 0;
+  -moz-padding-end: 0;
+}
+
 #inspector-inspect-toolbutton,
 #inspector-tools > toolbarbutton {
   -moz-appearance: none;
   min-width: 78px;
   min-height: 22px;
   color: hsl(210,30%,85%);
   text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
   border: 1px solid hsla(210,8%,5%,.45);
@@ -2024,20 +2029,16 @@ panel[dimmed="true"] {
 
 /* Highlighter - toolbar resizers */
 
 .inspector-resizer {
   -moz-appearance: none;
   cursor: n-resize;
 }
 
-.inspector-resizer[disabled] {
-  visibility: hidden;
-}
-
 #inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
 #inspector-end-resizer {
   width: 12px;
   height: 8px;
--- a/browser/themes/gnomestripe/browser/devtools/csshtmltree.css
+++ b/browser/themes/gnomestripe/browser/devtools/csshtmltree.css
@@ -182,9 +182,94 @@ a.link:visited {
 .userStyles {
   position: relative;
   top: 3px;
 }
 
 .userStyles,
 .userStylesLabel {
   cursor: pointer;
-}
\ No newline at end of file
+}
+
+/**
+ * CSS Rule View
+ */
+
+.ruleview {
+  background-color: #FFF;
+}
+
+.ruleview-rule-source {
+  background-color: -moz-dialog;
+  padding: 2px;
+}
+
+.ruleview-code {
+  padding: 2px;
+}
+
+.ruleview-propertylist {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.ruleview-enableproperty {
+  height: 10px;
+  width: 10px;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 0;
+}
+
+.ruleview-expander {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  background: url("chrome://browser/skin/devtools/arrows.png") 24px 0;
+  cursor: pointer;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 5px;
+}
+
+.ruleview-expander[dir="rtl"] {
+  background-position: 16px 0;
+}
+
+.ruleview-expander.styleinspector-open {
+  background-position: 8px 0;
+}
+
+.ruleview-newproperty {
+  /* (enable checkbox width: 12px) + (expander width: 15px) */
+  -moz-margin-start: 27px;
+}
+
+.ruleview-propertyname {
+  display: inline-block;
+  padding: 1px 0;
+  cursor: text;
+  color: #0060C0;
+  text-decoration: inherit;
+}
+
+.ruleview-propertyvalue {
+  cursor: text;
+  text-decoration: inherit;
+}
+
+.ruleview-computedlist {
+  list-style: none;
+  padding: 0;
+}
+
+.ruleview-computed {
+  -moz-margin-start: 4em;
+}
+
+.ruleview-overridden {
+  text-decoration: line-through;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+  padding: 0;
+  -moz-box-shadow: 2px 2px 2px #CCC;
+}
--- a/browser/themes/pinstripe/browser/browser.css
+++ b/browser/themes/pinstripe/browser/browser.css
@@ -2517,26 +2517,34 @@ panel[dimmed="true"] {
 
 
 /* Highlighter */
 
 .highlighter-veil {
   background-color: rgba(0, 0, 0, 0.5);
 }
 
-#highlighter-close-button {
-  list-style-image: url("chrome://browser/skin/KUI-close.png");
-  top: 12px;
-  right: 12px;
-  cursor: pointer;
-}
-
-#highlighter-close-button:-moz-locale-dir(rtl)  {
-  right: auto;
-  left: 12px;
+#highlighter-closebutton {
+  list-style-image: url("chrome://browser/skin/devtools/toolbarbutton-close.png");
+  -moz-image-region: rect(0, 16px, 16px, 0);
+  min-width: 16px;
+  width: 16px;
+  margin: 0 4px;
+}
+
+#highlighter-closebutton > .toolbarbutton-text {
+  display: none;
+}
+
+#highlighter-closebutton:hover {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+#highlighter-closebutton:active {
+  -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 #highlighter-veil-transparentbox {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
   outline: 1px dashed rgba(255,255,255,0.5);
   outline-offset: -1px;
 }
 
@@ -2544,20 +2552,24 @@ panel[dimmed="true"] {
   box-shadow: 0 0 0 1px black;
   outline-color: white;
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   -moz-appearance: none;
-  padding: 0 3px 4px;
   border-top: 1px solid hsla(210, 8%, 5%, .65);
   box-shadow: 0 1px 0 0 hsla(210, 16%, 76%, .2) inset;
   background-image: -moz-linear-gradient(top, hsl(210,11%,36%), hsl(210,11%,18%));
+  padding: 4px 16px 4px 0; /* use -moz-padding-end: 16px when/if bug 631729 gets fixed */
+}
+
+#inspector-toolbar[treepanel-open] {
+  padding: 0 0 4px;
 }
 
 #inspector-inspect-toolbutton,
 #inspector-tools > toolbarbutton {
   -moz-appearance: none;
   min-width: 78px;
   min-height: 22px;
   color: hsl(210,30%,85%);
@@ -2595,20 +2607,16 @@ panel[dimmed="true"] {
 
 /* Highlighter - toolbar resizers */
 
 .inspector-resizer {
   -moz-appearance: none;
   cursor: n-resize;
 }
 
-.inspector-resizer[disabled] {
-  visibility: hidden;
-}
-
 #inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
 #inspector-end-resizer {
   width: 12px;
   height: 8px;
--- a/browser/themes/pinstripe/browser/devtools/csshtmltree.css
+++ b/browser/themes/pinstripe/browser/devtools/csshtmltree.css
@@ -182,9 +182,94 @@ a.link:visited {
 .userStyles {
   position: relative;
   top: 3px;
 }
 
 .userStyles,
 .userStylesLabel {
   cursor: pointer;
-}
\ No newline at end of file
+}
+
+/**
+ * CSS Rule View
+ */
+
+.ruleview {
+  background-color: #FFF;
+}
+
+.ruleview-rule-source {
+  background-color: -moz-dialog;
+  padding: 2px;
+}
+
+.ruleview-code {
+  padding: 2px;
+}
+
+.ruleview-propertylist {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.ruleview-enableproperty {
+  height: 10px;
+  width: 10px;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 0;
+}
+
+.ruleview-expander {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  background: url("chrome://browser/skin/devtools/arrows.png") 24px 0;
+  cursor: pointer;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 5px;
+}
+
+.ruleview-expander[dir="rtl"] {
+  background-position: 16px 0;
+}
+
+.ruleview-expander.styleinspector-open {
+  background-position: 8px 0;
+}
+
+.ruleview-newproperty {
+  /* (enable checkbox width: 12px) + (expander width: 15px) */
+  -moz-margin-start: 27px;
+}
+
+.ruleview-propertyname {
+  display: inline-block;
+  padding: 1px 0;
+  cursor: text;
+  color: #0060C0;
+  text-decoration: inherit;
+}
+
+.ruleview-propertyvalue {
+  cursor: text;
+  text-decoration: inherit;
+}
+
+.ruleview-computedlist {
+  list-style: none;
+  padding: 0;
+}
+
+.ruleview-computed {
+  -moz-margin-start: 4em;
+}
+
+.ruleview-overridden {
+  text-decoration: line-through;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+  padding: 0;
+  -moz-box-shadow: 2px 2px 2px #CCC;
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..887daf052a6eda0b463bd75ca65dd998449cd330
GIT binary patch
literal 1031
zc$@(T1o-=jP)<h;3K|Lk000e1NJLTq001xm000mO1^@s6P_F#3000BdNkl<Zc-qyL
z`%6;+6vrzxC|2N<)IIeV(`|0ri^4K3dk{pC6+J16%F13^Nf9Jj8A_#<W<vDRirxho
zK3ZkYHudh_HIt>573nYalmCEyzi@?1xZPj;#UDO+IOlt|^L6KJcihm>(3oaaUPSC3
z7!Sh0B%lFPf}o*>TJ|XV$pjD%CaORUwWDevZ2rQ<nMQMbO>~UzJ#Yr$JvG#_N6|mQ
znw*lM(;Mrc=?eul)Uszp0}zpPFqN^424sL>1}5W$U!r~sRDcJd3Y5uQ2M<}JhFbP0
z`om*%1}|z|;IlR|s(V^=tRJyN7$eui8a33iCzydnyY?M^ot!fBqHIEAu{lci<mSId
zyflb`DX2XL7r_18L-`)7&B0Hi6qF<;S)B*-j<?YSHPo_4(LWKnZXjy4Q9TKXmbY4M
zWG@DnNJM1Rx9Lf?rU-3RFE!M%XG8-|Pv<5SJ6<)lx@dyKzo>6%Vb1CHZwq0d1{9Z;
zRn+$X8T7l_B++8Ew<cO_uI5jI$Xt0v^#f|C9l<~Vh-vAWH=Q2;vi(O2j>5!u4BeM=
zAisdQ1({hlsG&Bf0Td#H!0$)HCWePc?m7NOhA?msoKH!eb*HQMSL49nL9wMx^ftQ%
zFXOL${cmT@nRk~4s10JkYcR!mj-M*ZZgfd$dk-DU+nt+tjPa?WlGP@2yoZ{Q2Ds<N
zmnseQ&B8klPVfTFXG^cfv8H0+45+l(Q{H&Iou4qcmk9oV+mSl6jx}mR7!bf`ox$YW
zx#z&P2A4RG<QzP@S#LB+tWl$8K%-n6<vQ^uR16e?%hQvr!iUz5b__3&7Pq(EVzcv&
z%c%)rpaVPa!`l4ZynT0W{YPOo*}5Yq2S@yaHEKc{n47k6rE+bQ>$D&vD_g}t5svo>
zcb;n#`2M@;OV7^+d7UJ?W5!czLVW_$qIG_(O##>DWMT2Dyc4Ih8S^HhV`BZ(sNKL?
z<<2W_0&G+<P>e&p*Z+6WhvA)An?`H0qk%WUTxIp`T9q3R(-$qNYHIbRbLVjnlHOpJ
zcoWQJW-YIvM)d|zT+iVd6PjR-vC6m5d2pxn^0oV^GiE<We3K;GQ=c)%n##A37j(^;
zJHOm$ifc#QN%Tf@2Xm~c9I+}vf5`BL9PR?R1a5$<;GE1A=2)YK+W&vZ^ahh#{_jfi
z;65M_#&S-ghFbQFrh)JcTXw9$VAUK{rV3ON#>07PsAZ4xQDWJORqJFEUleo_WsVwZ
z*)!S?a~cp25>%i@Gs45%Bwz$)6^zs>ALhn1{{Yzl!O-c}?LPnj002ovPDHLkV1g8J
B@QeTe
--- a/browser/themes/pinstripe/browser/jar.mn
+++ b/browser/themes/pinstripe/browser/jar.mn
@@ -121,16 +121,17 @@ browser.jar:
   skin/classic/browser/tabview/search.png                   (tabview/search.png)
   skin/classic/browser/tabview/stack-expander.png           (tabview/stack-expander.png)
   skin/classic/browser/tabview/tabview.png                  (tabview/tabview.png)
   skin/classic/browser/tabview/tabview.css                  (tabview/tabview.css)
   skin/classic/browser/devtools/arrows.png                  (devtools/arrows.png)
   skin/classic/browser/devtools/search.png                  (devtools/search.png)
   skin/classic/browser/devtools/csshtmltree.css             (devtools/csshtmltree.css)
   skin/classic/browser/devtools/gcli.css                    (devtools/gcli.css)
+  skin/classic/browser/devtools/toolbarbutton-close.png     (devtools/toolbarbutton-close.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end-pressed.png              (devtools/breadcrumbs/ltr-end-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end-selected-pressed.png     (devtools/breadcrumbs/ltr-end-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end-selected.png             (devtools/breadcrumbs/ltr-end-selected.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-end.png                      (devtools/breadcrumbs/ltr-end.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-middle-pressed.png           (devtools/breadcrumbs/ltr-middle-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-middle-selected-pressed.png  (devtools/breadcrumbs/ltr-middle-selected-pressed.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-middle-selected.png          (devtools/breadcrumbs/ltr-middle-selected.png)
   skin/classic/browser/devtools/breadcrumbs/ltr-middle.png                   (devtools/breadcrumbs/ltr-middle.png)
--- a/browser/themes/winstripe/browser/browser.css
+++ b/browser/themes/winstripe/browser/browser.css
@@ -2615,26 +2615,29 @@ panel[dimmed="true"] {
 }
 
 /* Highlighter */
 
 .highlighter-veil {
   background-color: rgba(0, 0, 0, 0.5);
 }
 
-#highlighter-close-button {
-  list-style-image: url("chrome://browser/skin/KUI-close.png");
-  top: 12px;
-  right: 12px;
-  cursor: pointer;
-}
-
-#highlighter-close-button:-moz-locale-dir(rtl)  {
-  right: auto;
-  left: 12px;
+#highlighter-closebutton {
+  list-style-image: url("chrome://browser/skin/devtools/toolbarbutton-close.png");
+  -moz-image-region: rect(0, 16px, 16px, 0);
+  min-width: 16px;
+  width: 16px;
+}
+
+#highlighter-closebutton:hover {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+#highlighter-closebutton:active {
+  -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 #highlighter-veil-transparentbox {
   box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
   outline: 1px dashed rgba(255,255,255,0.5);
   outline-offset: -1px;
 }
 
@@ -2642,22 +2645,27 @@ panel[dimmed="true"] {
   box-shadow: 0 0 0 1px black;
   outline-color: white;
 }
 
 /* Highlighter toolbar */
 
 #inspector-toolbar {
   -moz-appearance: none;
-  padding: 0 3px 4px;
+  padding: 4px 3px;
   border-top: 1px solid hsla(211,68%,6%,.65) !important;
   box-shadow: 0 1px 0 hsla(209,29%,72%,.25) inset;
   background-image: -moz-linear-gradient(top, hsl(209,18%,34%), hsl(210,24%,16%));
 }
 
+#inspector-toolbar[treepanel-open] {
+  padding-top: 0;
+  -moz-padding-end: 0;
+}
+
 #inspector-inspect-toolbutton,
 #inspector-tools > toolbarbutton {
   -moz-appearance: none;
   min-width: 78px;
   min-height: 22px;
   color: hsl(210,30%,85%);
   text-shadow: 0 -1px 0 hsla(210,8%,5%,.45);
   border: 1px solid hsla(211,68%,6%,.5);
@@ -2693,20 +2701,16 @@ panel[dimmed="true"] {
 
 /* Highlighter - toolbar resizers */
 
 .inspector-resizer {
   -moz-appearance: none;
   cursor: n-resize;
 }
 
-.inspector-resizer[disabled] {
-  visibility: hidden;
-}
-
 #inspector-top-resizer {
   background: none;
   height: 4px;
 }
 
 #inspector-end-resizer {
   width: 12px;
   height: 8px;
--- a/browser/themes/winstripe/browser/devtools/csshtmltree.css
+++ b/browser/themes/winstripe/browser/devtools/csshtmltree.css
@@ -182,9 +182,94 @@ a.link:visited {
 .userStyles {
   position: relative;
   top: 3px;
 }
 
 .userStyles,
 .userStylesLabel {
   cursor: pointer;
-}
\ No newline at end of file
+}
+
+/**
+ * CSS Rule View
+ */
+
+.ruleview {
+  background-color: #FFF;
+}
+
+.ruleview-rule-source {
+  background-color: -moz-dialog;
+  padding: 2px;
+}
+
+.ruleview-code {
+  padding: 2px;
+}
+
+.ruleview-propertylist {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+}
+
+.ruleview-enableproperty {
+  height: 10px;
+  width: 10px;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 0;
+}
+
+.ruleview-expander {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  background: url("chrome://browser/skin/devtools/arrows.png") 24px 0;
+  cursor: pointer;
+  -moz-margin-start: 2px;
+  -moz-margin-end: 5px;
+}
+
+.ruleview-expander[dir="rtl"] {
+  background-position: 16px 0;
+}
+
+.ruleview-expander.styleinspector-open {
+  background-position: 8px 0;
+}
+
+.ruleview-newproperty {
+  /* (enable checkbox width: 12px) + (expander width: 15px) */
+  -moz-margin-start: 27px;
+}
+
+.ruleview-propertyname {
+  display: inline-block;
+  padding: 1px 0;
+  cursor: text;
+  color: #0060C0;
+  text-decoration: inherit;
+}
+
+.ruleview-propertyvalue {
+  cursor: text;
+  text-decoration: inherit;
+}
+
+.ruleview-computedlist {
+  list-style: none;
+  padding: 0;
+}
+
+.ruleview-computed {
+  -moz-margin-start: 4em;
+}
+
+.ruleview-overridden {
+  text-decoration: line-through;
+}
+
+.styleinspector-propertyeditor {
+  border: 1px solid #CCC;
+  padding: 0;
+  -moz-box-shadow: 2px 2px 2px #CCC;
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4a58465b9b7ea3b3c7c14b36214524fb9b0e38b7
GIT binary patch
literal 898
zc$@)(1AY97P)<h;3K|Lk000e1NJLTq001xm000mO1^@s6P_F#30009?Nkl<Zc-qaD
z-%C?*7{<E^ys6=GX#TF*%%jfDG@>G?$RO%(D9DVE2wO>|IdyGXXbKuAMWsXumi@rW
zizxG=3lSo^$*@vs{TpxZ=io>?+i~PBx_CVA-uLse=lObewsj)1Carg5ulx8l>ikEo
z`ThB`CSjy(=l*>gv)qjuW-uASvcvAuAyldwlHG@8CuIRJG;-)bbn4h)nLJiwD;QXI
zVgb^ed4Z!F9MR(rht#I0ORduxSIl5w*-2RdPGw^6Zh7SQ%fnR!1Itb<0CjyvhP3AA
zOXt?Da^3BYD`qgT?4&fn^PSZ)tmf|fd~&a%LWcCpYeY?t8LySK1h|l$Enzj-?eR$0
zwr%G1W^plE(Sw0a7-0I->}l1E>J%SPL`{!bq5xynRi??w&KnY4h|1e=hurac5j8z#
zCZnt+Kw$G`xu)h`UyI03Wyjk>a-*mSQPX2)GO!5)=%A`8+|&1d`tt{jBWilg5(OCD
zQ7O08Y`<a1%hr%Aep{3``i6*_9y1taEdd(TRNIy<5>kUtnu9Vww;(Tq?TDHlGn0X}
z3$P-@a`Vg*1sJLH%V2rA45)fbN@QFOFQ{=uO^+FjvX%hnva%#tSSS%a@m4kYcqs&;
zrpF8h);0hd93E|&n3{QqsOi}Sc;NR*e_5IIsd^2=H2r<{2co9O%w&|c1UQ?SDa{21
z((ZPfW=ES_WbVs%L`{#G$-vqMSbp0eYI=47hI|#$Q(7wBdeW~#VLkZ;c@}I()byB{
z46NlTq^2(G6XvQK9cyVbPkyAS1yR#uW-|450sg+{(ee<90^F_eN|#}nM(5S=Q#Bs(
zcn~!`W+tPoCBPXq)u`sif`80k($hw<5X@j;?fwIms=C-ckErP}OBCR?-Wr|Tw@XML
zcR{zCE3f1Bl(ht?bGamtn=6fZd2(K_7xj8Y?{`E^j~NVXioXX4P%0hz%#N%g7-cO1
zPB@)XuOH5aoE*%?6*CxEc2fCMB5>U>76;VWty05QFtF^z0;K17E*^2DFCJH8C-qyl
z*5!&TW-zeqq%44U*TFp)O7_PXEIY9P{yjB&F_i2+EIaJ}4?m}M|5uu7lPWt&!KShM
Y8%Lp~>GYClRsaA107*qoM6N<$g8i4Sr2qf`
--- a/browser/themes/winstripe/browser/jar.mn
+++ b/browser/themes/winstripe/browser/jar.mn
@@ -105,16 +105,17 @@ browser.jar:
         skin/classic/browser/tabview/stack-expander.png             (tabview/stack-expander.png)
         skin/classic/browser/tabview/tabview.png                    (tabview/tabview.png)
         skin/classic/browser/tabview/tabview-inverted.png           (tabview/tabview-inverted.png)
         skin/classic/browser/tabview/tabview.css                    (tabview/tabview.css)
         skin/classic/browser/devtools/arrows.png                    (devtools/arrows.png)
         skin/classic/browser/devtools/search.png                    (devtools/search.png)
         skin/classic/browser/devtools/csshtmltree.css               (devtools/csshtmltree.css)
         skin/classic/browser/devtools/gcli.css                      (devtools/gcli.css)
+        skin/classic/browser/devtools/toolbarbutton-close.png       (devtools/toolbarbutton-close.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-end-pressed.png              (devtools/breadcrumbs/ltr-end-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-end-selected-pressed.png     (devtools/breadcrumbs/ltr-end-selected-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-end-selected.png             (devtools/breadcrumbs/ltr-end-selected.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-end.png                      (devtools/breadcrumbs/ltr-end.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-middle-pressed.png           (devtools/breadcrumbs/ltr-middle-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-middle-selected-pressed.png  (devtools/breadcrumbs/ltr-middle-selected-pressed.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-middle-selected.png          (devtools/breadcrumbs/ltr-middle-selected.png)
         skin/classic/browser/devtools/breadcrumbs/ltr-middle.png                   (devtools/breadcrumbs/ltr-middle.png)
@@ -256,16 +257,17 @@ browser.jar:
         skin/classic/aero/browser/tabview/stack-expander.png         (tabview/stack-expander.png)
         skin/classic/aero/browser/tabview/tabview.png                (tabview/tabview.png)
         skin/classic/aero/browser/tabview/tabview-inverted.png       (tabview/tabview-inverted.png)
         skin/classic/aero/browser/tabview/tabview.css                (tabview/tabview.css)
         skin/classic/aero/browser/devtools/arrows.png                (devtools/arrows.png)
         skin/classic/aero/browser/devtools/search.png                (devtools/search.png)
         skin/classic/aero/browser/devtools/csshtmltree.css           (devtools/csshtmltree.css)
         skin/classic/aero/browser/devtools/gcli.css                  (devtools/gcli.css)
+        skin/classic/aero/browser/devtools/toolbarbutton-close.png   (devtools/toolbarbutton-close.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-end-pressed.png              (devtools/breadcrumbs/ltr-end-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-end-selected-pressed.png     (devtools/breadcrumbs/ltr-end-selected-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-end-selected.png             (devtools/breadcrumbs/ltr-end-selected.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-end.png                      (devtools/breadcrumbs/ltr-end.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-middle-pressed.png           (devtools/breadcrumbs/ltr-middle-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-middle-selected-pressed.png  (devtools/breadcrumbs/ltr-middle-selected-pressed.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-middle-selected.png          (devtools/breadcrumbs/ltr-middle-selected.png)
         skin/classic/aero/browser/devtools/breadcrumbs/ltr-middle.png                   (devtools/breadcrumbs/ltr-middle.png)
--- a/dom/system/NetworkGeolocationProvider.js
+++ b/dom/system/NetworkGeolocationProvider.js
@@ -212,17 +212,17 @@ WifiGeoPositionProvider.prototype = {
     function encode(ap) {
       // make sure that the ssid doesn't contain any | chars.
       ap.ssid = ap.ssid.replace("|", "\\|");
       // gls service parses the | as fields
       return "&wifi=mac:"+ap.mac+"|ssid:"+ap.ssid+"|ss:"+ap.signal;
     };
 
     if (accessPoints) {
-        accessPoints.sort(sort).map(encode).join("");
+        providerUrl = providerUrl + accessPoints.sort(sort).map(encode).join("");
         // max length is 2k.  make sure we are under that
         let x = providerUrl.length - 2000;
         if (x >= 0) {
             // we need to trim
             let doomed = providerUrl.lastIndexOf("&", 2000);
             LOG("Doomed:"+doomed);
             providerUrl = providerUrl.substring(0, doomed);
         }
--- a/layout/generic/nsObjectFrame.cpp
+++ b/layout/generic/nsObjectFrame.cpp
@@ -822,26 +822,32 @@ nsObjectFrame::CallSetWindow(bool aCheck
   nsIFrame* rootFrame = rootPC->PresShell()->FrameManager()->GetRootFrame();
   nsRect bounds = GetContentRectRelativeToSelf() + GetOffsetToCrossDoc(rootFrame);
   nsIntRect intBounds = bounds.ToNearestPixels(appUnitsPerDevPixel);
   window->x = intBounds.x;
   window->y = intBounds.y;
   window->width = intBounds.width;
   window->height = intBounds.height;
 
-  // this will call pi->SetWindow and take care of window subclassing
-  // if needed, see bug 132759.
+  // Calling SetWindow might destroy this frame. We need to use the instance
+  // owner to clean up so hold a ref.
+  nsRefPtr<nsPluginInstanceOwner> instanceOwnerRef(mInstanceOwner);
+
+  // This will call pi->SetWindow and take care of window subclassing
+  // if needed, see bug 132759. Calling SetWindow can destroy this frame
+  // so check for that before doing anything else with this frame's memory.
   if (mInstanceOwner->UseAsyncRendering()) {
     rv = pi->AsyncSetWindow(window);
   }
   else {
     rv = window->CallSetWindow(pi);
   }
 
-  mInstanceOwner->ReleasePluginPort(window->window);
+  instanceOwnerRef->ReleasePluginPort(window->window);
+
   return rv;
 }
 
 bool
 nsObjectFrame::IsFocusable(PRInt32 *aTabIndex, bool aWithMouse)
 {
   if (aTabIndex)
     *aTabIndex = -1;
--- a/toolkit/content/widgets/videocontrols.xml
+++ b/toolkit/content/widgets/videocontrols.xml
@@ -472,16 +472,17 @@
                             this.setupStatusFader();
                             break;
                         case "ended":
                             this.setPlayButtonState(true);
                             // We throttle timechange events, so the thumb might not be
                             // exactly at the end when the video finishes.
                             this.showPosition(Math.round(this.video.currentTime * 1000),
                                               Math.round(this.video.duration * 1000));
+                            this.startFadeIn(this.controlBar);
                             this.setupStatusFader();
                             break;
                         case "volumechange":
                             var volume = this.video.muted ? 0 : Math.round(this.video.volume * 100);
                             this.setMuteButtonState(this.video.muted);
                             this.volumeControl.value = volume;
                             break;
                         case "loadedmetadata":
@@ -1018,17 +1019,18 @@
                                 else
                                     newval = oldval + maxtime / 10;
                                 this.video.currentTime = (newval <= maxtime ? newval : maxtime);
                                 break;
                             case "home": /* Seek to beginning */
                                 this.video.currentTime = 0;
                                 break;
                             case "end": /* Seek to end */
-                                this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
+                                if (this.video.currentTime != this.video.duration)
+                                    this.video.currentTime = (this.video.duration || this.maxCurrentTimeSeen / 1000);
                                 break;
                             default:
                                 return;
                         }
                     } catch(e) { /* ignore any exception from setting .currentTime */ }
 
                     event.preventDefault(); // Prevent page scrolling
                 },