Bug 966895 - [rule view] Adding new rules to the current selection in the CSS rule-view. r=harth
authorGabriel Luong <gabriel.luong@gmail.com>
Mon, 07 Jul 2014 09:18:00 +0200
changeset 193121 a7e7bafd6de9a127ca8d871cbfbf488dc7d30128
parent 193120 38625d13b84006be5f526c7d9fd8ae9dd09eca05
child 193122 8252ef80638981dd6bedb99cae714512981323e4
push id27109
push userryanvm@gmail.com
push dateWed, 09 Jul 2014 20:12:42 +0000
treeherdermozilla-central@fc35681b0a87 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs966895
milestone33.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
Bug 966895 - [rule view] Adding new rules to the current selection in the CSS rule-view. r=harth
browser/devtools/styleinspector/rule-view.js
browser/devtools/styleinspector/test/browser.ini
browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
toolkit/devtools/server/actors/root.js
toolkit/devtools/server/actors/styles.js
toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -437,17 +437,17 @@ function Rule(aElementStyle, aOptions) {
 Rule.prototype = {
   mediaText: "",
 
   get title() {
     if (this._title) {
       return this._title;
     }
     this._title = CssLogic.shortSource(this.sheet);
-    if (this.domRule.type !== ELEMENT_STYLE) {
+    if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
       this._title += ":" + this.ruleLine;
     }
 
     this._title = this._title + (this.mediaText ? " @media " + this.mediaText : "");
     return this._title;
   },
 
   get inheritedSource() {
@@ -1071,16 +1071,17 @@ function CssRuleView(aInspector, aDoc, a
   this.element = this.doc.createElementNS(HTML_NS, "div");
   this.element.className = "ruleview devtools-monospace";
   this.element.flex = 1;
 
   this._outputParser = new OutputParser();
 
   this._buildContextMenu = this._buildContextMenu.bind(this);
   this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
+  this._onAddRule = this._onAddRule.bind(this);
   this._onSelectAll = this._onSelectAll.bind(this);
   this._onCopy = this._onCopy.bind(this);
   this._onCopyColor = this._onCopyColor.bind(this);
   this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
 
   this.element.addEventListener("copy", this._onCopy);
 
   this._handlePrefChange = this._handlePrefChange.bind(this);
@@ -1120,32 +1121,37 @@ CssRuleView.prototype = {
    */
   _buildContextMenu: function() {
     let doc = this.doc.defaultView.parent.document;
 
     this._contextmenu = doc.createElementNS(XUL_NS, "menupopup");
     this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
     this._contextmenu.id = "rule-view-context-menu";
 
+    this.menuitemAddRule = createMenuItem(this._contextmenu, {
+      label: "ruleView.contextmenu.addRule",
+      accesskey: "ruleView.contextmenu.addRule.accessKey",
+      command: this._onAddRule
+    });
     this.menuitemSelectAll = createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.selectAll",
       accesskey: "ruleView.contextmenu.selectAll.accessKey",
       command: this._onSelectAll
     });
     this.menuitemCopy = createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.copy",
       accesskey: "ruleView.contextmenu.copy.accessKey",
       command: this._onCopy
     });
     this.menuitemCopyColor = createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.copyColor",
       accesskey: "ruleView.contextmenu.copyColor.accessKey",
       command: this._onCopyColor
     });
-    this.menuitemSources= createMenuItem(this._contextmenu, {
+    this.menuitemSources = createMenuItem(this._contextmenu, {
       label: "ruleView.contextmenu.showOrigSources",
       accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
       command: this._onToggleOrigSources
     });
 
     let popupset = doc.documentElement.querySelector("popupset");
     if (!popupset) {
       popupset = doc.createElementNS(XUL_NS, "popupset");
@@ -1349,16 +1355,53 @@ CssRuleView.prototype = {
   /**
    *  Toggle the original sources pref.
    */
   _onToggleOrigSources: function() {
     let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
   },
 
+  /**
+   * Add a new rule to the current element.
+   */
+  _onAddRule: function() {
+    let elementStyle = this._elementStyle;
+    let element = elementStyle.element;
+    let rules = elementStyle.rules;
+    let client = this.inspector.toolbox._target.client;
+
+    if (!client.traits.addNewRule) {
+      return;
+    }
+
+    this.pageStyle.addNewRule(element).then(options => {
+      let newRule = new Rule(elementStyle, options);
+      rules.push(newRule);
+      let editor = new RuleEditor(this, newRule);
+
+      // Insert the new rule editor after the inline element rule
+      if (rules.length <= 1) {
+        this.element.appendChild(editor.element);
+      } else {
+        for (let rule of rules) {
+          if (rule.selectorText === "element") {
+            let referenceElement = rule.editor.element.nextSibling;
+            this.element.insertBefore(editor.element, referenceElement);
+            break;
+          }
+        }
+      }
+
+      // Focus and make the new rule's selector editable
+      editor.selectorText.click();
+      elementStyle._changed();
+    });
+  },
+
   setPageStyle: function(aPageStyle) {
     this.pageStyle = aPageStyle;
   },
 
   /**
    * Return {bool} true if the rule view currently has an input editor visible.
    */
   get isEditing() {
@@ -1847,16 +1890,19 @@ RuleEditor.prototype = {
       // https://bugzilla.mozilla.org/show_bug.cgi?id=935803#c37
       if (sourceHref === "about:PreferenceStyleSheet") {
         sourceLabel.parentNode.setAttribute("unselectable", "true");
         sourceLabel.setAttribute("value", uaLabel);
         sourceLabel.removeAttribute("tooltiptext");
       }
     } else {
       sourceLabel.setAttribute("value", this.rule.title);
+      if (this.rule.ruleLine == -1 && this.rule.domRule.parentStyleSheet) {
+        sourceLabel.parentNode.setAttribute("unselectable", "true");
+      }
     }
 
     let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
     if (showOrig && !this.rule.isSystem && this.rule.domRule.type != ELEMENT_STYLE) {
       this.rule.getOriginalSourceStrings().then((strings) => {
         sourceLabel.setAttribute("value", strings.short);
         sourceLabel.setAttribute("tooltiptext", strings.full);
       }, console.error);
--- a/browser/devtools/styleinspector/test/browser.ini
+++ b/browser/devtools/styleinspector/test/browser.ini
@@ -37,16 +37,19 @@ support-files =
 [browser_computedview_select-and-copy-styles.js]
 [browser_computedview_style-editor-link.js]
 [browser_ruleview_add-property-and-reselect.js]
 [browser_ruleview_add-property-cancel_01.js]
 [browser_ruleview_add-property-cancel_02.js]
 [browser_ruleview_add-property-cancel_03.js]
 [browser_ruleview_add-property_01.js]
 [browser_ruleview_add-property_02.js]
+[browser_ruleview_add-rule_01.js]
+[browser_ruleview_add-rule_02.js]
+[browser_ruleview_add-rule_03.js]
 [browser_ruleview_colorpicker-and-image-tooltip_01.js]
 [browser_ruleview_colorpicker-and-image-tooltip_02.js]
 [browser_ruleview_colorpicker-appears-on-swatch-click.js]
 [browser_ruleview_colorpicker-commit-on-ENTER.js]
 [browser_ruleview_colorpicker-edit-gradient.js]
 [browser_ruleview_colorpicker-hides-on-tooltip.js]
 [browser_ruleview_colorpicker-multiple-changes.js]
 [browser_ruleview_colorpicker-revert-on-ESC.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_01.js
@@ -0,0 +1,89 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the behaviour of adding a new rule to the rule view and the
+// various inplace-editor behaviours in the new rule editor
+
+let PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  .testclass {',
+  '    text-align: center;',
+  '  }',
+  '</style>',
+  '<div id="testid" class="testclass">Styled Node</div>',
+  '<span class="testclass2">This is a span</span>',
+  '<p>Empty<p>'
+].join("\n");
+
+const TEST_DATA = [
+  { node: "#testid", expected: "#testid" },
+  { node: ".testclass2", expected: ".testclass2" },
+  { node: "p", expected: "p" }
+];
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+  info("Creating the test document");
+  content.document.body.innerHTML = PAGE_CONTENT;
+
+  info("Opening the rule-view");
+  let {toolbox, inspector, view} = yield openRuleView();
+
+  info("Iterating over the test data");
+  for (let data of TEST_DATA) {
+    yield runTestData(inspector, view, data);
+  }
+});
+
+function* runTestData(inspector, view, data) {
+  let {node, expected} = data;
+  info("Selecting the test element");
+  yield selectNode(node, inspector);
+
+  info("Waiting for context menu to be shown");
+  let onPopup = once(view._contextmenu, "popupshown");
+  let win = view.doc.defaultView;
+
+  EventUtils.synthesizeMouseAtCenter(view.element,
+    {button: 2, type: "contextmenu"}, win);
+  yield onPopup;
+
+  ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+  info("Waiting for rule view to change");
+  let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+  info("Adding the new rule");
+  view.menuitemAddRule.click();
+  yield onRuleViewChanged;
+  view._contextmenu.hidePopup();
+
+  yield testNewRule(view, expected, 1);
+
+  info("Resetting page content");
+  content.document.body.innerHTML = PAGE_CONTENT;
+}
+
+function* testNewRule(view, expected, index) {
+  let idRuleEditor = getRuleViewRuleEditor(view, index);
+  let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+  is(editor.value, expected,
+      "Selector editor value is as expected: " + expected);
+
+  info("Entering the escape key");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+  is(idRuleEditor.selectorText.textContent, expected,
+      "Selector text value is as expected: " + expected);
+
+  info("Adding new properties to new rule: " + expected)
+  idRuleEditor.addProperty("font-weight", "bold", "");
+  let textProps = idRuleEditor.rule.textProps;
+  let lastRule = textProps[textProps.length - 1];
+  is(lastRule.name, "font-weight", "Last rule name is font-weight");
+  is(lastRule.value, "bold", "Last rule value is bold");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_02.js
@@ -0,0 +1,78 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the behaviour of adding a new rule to the rule view and editing
+// its selector
+
+let PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  #testid {',
+  '    text-align: center;',
+  '  }',
+  '</style>',
+  '<div id="testid">Styled Node</div>',
+  '<span>This is a span</span>'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+  info("Creating the test document");
+  content.document.body.innerHTML = PAGE_CONTENT;
+
+  info("Opening the rule-view");
+  let {toolbox, inspector, view} = yield openRuleView();
+
+  info("Selecting the test element");
+  yield selectNode("#testid", inspector);
+
+  info("Waiting for context menu to be shown");
+  let onPopup = once(view._contextmenu, "popupshown");
+  let win = view.doc.defaultView;
+
+  EventUtils.synthesizeMouseAtCenter(view.element,
+    {button: 2, type: "contextmenu"}, win);
+  yield onPopup;
+
+  ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+  info("Waiting for rule view to change");
+  let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+  info("Adding the new rule");
+  view.menuitemAddRule.click();
+  yield onRuleViewChanged;
+  view._contextmenu.hidePopup();
+
+  yield testEditSelector(view, "span");
+
+  info("Selecting the modified element");
+  yield selectNode("span", inspector);
+  yield checkModifiedElement(view, "span");
+});
+
+function* testEditSelector(view, name) {
+  info("Test editing existing selector field");
+  let idRuleEditor = getRuleViewRuleEditor(view, 1);
+  let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+
+  info("Entering a new selector name and committing");
+  editor.value = name;
+
+  info("Waiting for rule view to refresh");
+  let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+  info("Entering the commit key");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewRefresh;
+
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+}
+
+function* checkModifiedElement(view, name) {
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+  ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleinspector/test/browser_ruleview_add-rule_03.js
@@ -0,0 +1,114 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the behaviour of adding a new rule to the rule view, adding a new
+// property and editing the selector
+
+let PAGE_CONTENT = [
+  '<style type="text/css">',
+  '  #testid {',
+  '    text-align: center;',
+  '  }',
+  '</style>',
+  '<div id="testid">Styled Node</div>',
+  '<span>This is a span</span>'
+].join("\n");
+
+let test = asyncTest(function*() {
+  yield addTab("data:text/html;charset=utf-8,test rule view add rule");
+
+  info("Creating the test document");
+  content.document.body.innerHTML = PAGE_CONTENT;
+
+  info("Opening the rule-view");
+  let {toolbox, inspector, view} = yield openRuleView();
+
+  info("Selecting the test element");
+  yield selectNode("#testid", inspector);
+
+  info("Waiting for context menu to be shown");
+  let onPopup = once(view._contextmenu, "popupshown");
+  let win = view.doc.defaultView;
+
+  EventUtils.synthesizeMouseAtCenter(view.element,
+    {button: 2, type: "contextmenu"}, win);
+  yield onPopup;
+
+  ok(!view.menuitemAddRule.hidden, "Add rule is visible");
+
+  info("Waiting for rule view to change");
+  let onRuleViewChanged = once(view.element, "CssRuleViewChanged");
+
+  info("Adding the new rule");
+  view.menuitemAddRule.click();
+  yield onRuleViewChanged;
+  view._contextmenu.hidePopup();
+
+  info("Adding new properties to the new rule");
+  yield testNewRule(view, "#testid", 1);
+
+  info("Editing existing selector field");
+  yield testEditSelector(view, "span");
+
+  info("Selecting the modified element");
+  yield selectNode("span", inspector);
+
+  info("Check new rule and property exist in the modified element");
+  yield checkModifiedElement(view, "span", 1);
+});
+
+function* testNewRule(view, expected, index) {
+  let idRuleEditor = getRuleViewRuleEditor(view, index);
+  let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
+  is(editor.value, expected,
+      "Selector editor value is as expected: " + expected);
+
+  info("Entering the escape key");
+  EventUtils.synthesizeKey("VK_ESCAPE", {});
+
+  is(idRuleEditor.selectorText.textContent, expected,
+      "Selector text value is as expected: " + expected);
+
+  info("Adding new properties to new rule: " + expected)
+  idRuleEditor.addProperty("font-weight", "bold", "");
+  let textProps = idRuleEditor.rule.textProps;
+  let lastRule = textProps[textProps.length - 1];
+  is(lastRule.name, "font-weight", "Last rule name is font-weight");
+  is(lastRule.value, "bold", "Last rule value is bold");
+}
+
+function* testEditSelector(view, name) {
+  let idRuleEditor = getRuleViewRuleEditor(view, 1);
+
+  info("Focusing an existing selector name in the rule-view");
+  let editor = yield focusEditableField(idRuleEditor.selectorText);
+
+  is(inplaceEditor(idRuleEditor.selectorText), editor,
+    "The selector editor got focused");
+
+  info("Entering a new selector name: " + name);
+  editor.input.value = name;
+
+  info("Waiting for rule view to refresh");
+  let onRuleViewRefresh = once(view.element, "CssRuleViewRefreshed");
+
+  info("Entering the commit key");
+  EventUtils.synthesizeKey("VK_RETURN", {});
+  yield onRuleViewRefresh;
+
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+}
+
+function* checkModifiedElement(view, name, index) {
+  is(view._elementStyle.rules.length, 2, "Should have 2 rules.");
+  ok(getRuleViewRule(view, name), "Rule with " + name + " selector exists.");
+
+  let idRuleEditor = getRuleViewRuleEditor(view, index);
+  let textProps = idRuleEditor.rule.textProps;
+  let lastRule = textProps[textProps.length - 1];
+  is(lastRule.name, "font-weight", "Last rule name is font-weight");
+  is(lastRule.value, "bold", "Last rule value is bold");
+}
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -119,17 +119,20 @@ RootActor.prototype = {
     storageInspector: true,
     // Whether storage inspector is read only
     storageInspectorReadOnly: true,
     // Whether conditional breakpoints are supported
     conditionalBreakpoints: true,
     bulk: true,
     // Whether the style rule actor implements the modifySelector method
     // that modifies the rule's selector
-    selectorEditable: true
+    selectorEditable: true,
+    // Whether the page style actor implements the addNewRule method that
+    // adds new rules to the page
+    addNewRule: true
   },
 
   /**
    * Return a 'hello' packet as specified by the Remote Debugging Protocol.
    */
   sayHello: function() {
     return {
       from: this.actorID,
--- a/toolkit/devtools/server/actors/styles.js
+++ b/toolkit/devtools/server/actors/styles.js
@@ -24,16 +24,19 @@ const ELEMENT_STYLE = 100;
 exports.ELEMENT_STYLE = ELEMENT_STYLE;
 
 const PSEUDO_ELEMENTS = [":first-line", ":first-letter", ":before", ":after", ":-moz-selection"];
 exports.PSEUDO_ELEMENTS = PSEUDO_ELEMENTS;
 
 // Predeclare the domnode actor type for use in requests.
 types.addActorType("domnode");
 
+// Predeclare the domstylerule actor type
+types.addActorType("domstylerule");
+
 /**
  * DOM Nodes returned by the style actor will be owned by the DOM walker
  * for the connection.
   */
 types.addLifetime("walker", "walker");
 
 /**
  * When asking for the styles applied to a node, we return a list of
@@ -47,16 +50,22 @@ types.addDictType("appliedstyle", {
 
 types.addDictType("matchedselector", {
   rule: "domstylerule#actorid",
   selector: "string",
   value: "string",
   status: "number"
 });
 
+types.addDictType("appliedStylesReturn", {
+  entries: "array:appliedstyle",
+  rules: "array:domstylerule",
+  sheets: "array:stylesheet"
+});
+
 /**
  * The PageStyle actor lets the client look at the styles on a page, as
  * they are applied to a given node.
  */
 var PageStyleActor = protocol.ActorClass({
   typeName: "pagestyle",
 
   /**
@@ -74,16 +83,19 @@ var PageStyleActor = protocol.ActorClass
       throw Error("The inspector's WalkerActor must be created before " +
                    "creating a PageStyleActor.");
     }
     this.walker = inspector.walker;
     this.cssLogic = new CssLogic;
 
     // Stores the association of DOM objects -> actors
     this.refMap = new Map;
+
+    this.onFrameUnload = this.onFrameUnload.bind(this);
+    events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
   },
 
   get conn() this.inspector.conn,
 
   /**
    * Return or create a StyleRuleActor for the given item.
    * @param item Either a CSSStyleRule or a DOM element.
    */
@@ -274,80 +286,37 @@ var PageStyleActor = protocol.ActorClass
       result += ".style"
     }
     return result;
   },
 
   /**
    * Get the set of styles that apply to a given node.
    * @param NodeActor node
-   * @param string property
    * @param object options
    *   `filter`: A string filter that affects the "matched" handling.
    *     'user': Include properties from user style sheets.
    *     'ua': Include properties from user and user-agent sheets.
    *     Default value is 'ua'
    *   `inherited`: Include styles inherited from parent nodes.
    *   `matchedSeletors`: Include an array of specific selectors that
    *     caused this rule to match its node.
    */
   getApplied: method(function(node, options) {
     let entries = [];
-
     this.addElementRules(node.rawNode, undefined, options, entries);
-
-    if (options.inherited) {
-      let parent = this.walker.parentNode(node);
-      while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
-        this.addElementRules(parent.rawNode, parent, options, entries);
-        parent = this.walker.parentNode(parent);
-      }
-    }
-
-    if (options.matchedSelectors) {
-      for (let entry of entries) {
-        if (entry.rule.type === ELEMENT_STYLE) {
-          continue;
-        }
-
-        let domRule = entry.rule.rawRule;
-        let selectors = CssLogic.getSelectors(domRule);
-        let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
-        entry.matchedSelectors = [];
-        for (let i = 0; i < selectors.length; i++) {
-          if (DOMUtils.selectorMatchesElement(element, domRule, i)) {
-            entry.matchedSelectors.push(selectors[i]);
-          }
-        }
-
-      }
-    }
-
-    let rules = new Set;
-    let sheets = new Set;
-    entries.forEach(entry => rules.add(entry.rule));
-    this.expandSets(rules, sheets);
-
-    return {
-      entries: entries,
-      rules: [...rules],
-      sheets: [...sheets]
-    }
+    return this.getAppliedProps(node, entries, options);
   }, {
     request: {
       node: Arg(0, "domnode"),
       inherited: Option(1, "boolean"),
       matchedSelectors: Option(1, "boolean"),
       filter: Option(1, "string")
     },
-    response: RetVal(types.addDictType("appliedStylesReturn", {
-      entries: "array:appliedstyle",
-      rules: "array:domstylerule",
-      sheets: "array:stylesheet"
-    }))
+    response: RetVal("appliedStylesReturn")
   }),
 
   _hasInheritedProps: function(style) {
     return Array.prototype.some.call(style, prop => {
       return DOMUtils.isInheritedProperty(prop);
     });
   },
 
@@ -410,16 +379,76 @@ var PageStyleActor = protocol.ActorClass
           isSystem: isSystem
         });
       }
 
     }
   },
 
   /**
+   * Helper function for getApplied and addNewRule that fetches a set of
+   * style properties that apply to the given node and associated rules
+   * @param NodeActor node
+   * @param object options
+   *   `filter`: A string filter that affects the "matched" handling.
+   *     'user': Include properties from user style sheets.
+   *     'ua': Include properties from user and user-agent sheets.
+   *     Default value is 'ua'
+   *   `inherited`: Include styles inherited from parent nodes.
+   *   `matchedSeletors`: Include an array of specific selectors that
+   *     caused this rule to match its node.
+   * @param array entries
+   *   List of appliedstyle objects that lists the rules that apply to the
+   *   node. If adding a new rule to the stylesheet, only the new rule entry
+   *   is provided and only the style properties that apply to the new
+   *   rule is fetched.
+   * @returns Object containing the list of rule entries, rule actors and
+   *   stylesheet actors that applies to the given node and its associated
+   *   rules.
+   */
+  getAppliedProps: function(node, entries, options) {
+    if (options.inherited) {
+      let parent = this.walker.parentNode(node);
+      while (parent && parent.rawNode.nodeType != Ci.nsIDOMNode.DOCUMENT_NODE) {
+        this.addElementRules(parent.rawNode, parent, options, entries);
+        parent = this.walker.parentNode(parent);
+      }
+    }
+
+    if (options.matchedSelectors) {
+      for (let entry of entries) {
+        if (entry.rule.type === ELEMENT_STYLE) {
+          continue;
+        }
+
+        let domRule = entry.rule.rawRule;
+        let selectors = CssLogic.getSelectors(domRule);
+        let element = entry.inherited ? entry.inherited.rawNode : node.rawNode;
+        entry.matchedSelectors = [];
+        for (let i = 0; i < selectors.length; i++) {
+          if (DOMUtils.selectorMatchesElement(element, domRule, i)) {
+            entry.matchedSelectors.push(selectors[i]);
+          }
+        }
+      }
+    }
+
+    let rules = new Set;
+    let sheets = new Set;
+    entries.forEach(entry => rules.add(entry.rule));
+    this.expandSets(rules, sheets);
+
+    return {
+      entries: entries,
+      rules: [...rules],
+      sheets: [...sheets]
+    }
+  },
+
+  /**
    * Expand Sets of rules and sheets to include all parent rules and sheets.
    */
   expandSets: function(ruleSet, sheetSet) {
     // Sets include new items in their iteration
     for (let rule of ruleSet) {
       if (rule.rawRule.parentRule) {
         let parent = this._styleRef(rule.rawRule.parentRule);
         if (!ruleSet.has(parent)) {
@@ -511,16 +540,69 @@ var PageStyleActor = protocol.ActorClass
       if (selectors && selectors.length > 0 && selectors[0].value == "auto") {
         margins[prop] = "auto";
       }
     }
 
     return margins;
   },
 
+  /**
+   * On page navigation, tidy up remaining objects.
+   */
+  onFrameUnload: function() {
+    this._styleElement = null;
+  },
+
+  /**
+   * Helper function to addNewRule to construct a new style tag in the document.
+   * @returns DOMElement of the style tag
+   */
+  get styleElement() {
+    if (!this._styleElement) {
+      let document = this.inspector.window.document;
+      let style = document.createElement("style");
+      style.setAttribute("type", "text/css");
+      document.head.appendChild(style);
+      this._styleElement = style;
+    }
+
+    return this._styleElement;
+  },
+
+  /**
+   * Adds a new rule, and returns the new StyleRuleActor.
+   * @param   NodeActor node
+   * @returns StyleRuleActor of the new rule
+   */
+  addNewRule: method(function(node) {
+    let style = this.styleElement;
+    let sheet = style.sheet;
+    let rawNode = node.rawNode;
+
+    let selector;
+    if (rawNode.id) {
+      selector = "#" + rawNode.id;
+    } else if (rawNode.className) {
+      selector = "." + rawNode.className;
+    } else {
+      selector = rawNode.tagName.toLowerCase();
+    }
+
+    let index = sheet.insertRule(selector + " {}", sheet.cssRules.length);
+    let ruleActor = this._styleRef(sheet.cssRules[index]);
+    return this.getAppliedProps(node, [{ rule: ruleActor }],
+      { matchedSelectors: true });
+  }, {
+    request: {
+      node: Arg(0, "domnode")
+    },
+    response: RetVal("appliedStylesReturn")
+  }),
+
 });
 exports.PageStyleActor = PageStyleActor;
 
 /**
  * Front object for the PageStyleActor
  */
 var PageStyleFront = protocol.FrontClass(PageStyleActor, {
   initialize: function(conn, form, ctx, detail) {
@@ -545,22 +627,27 @@ var PageStyleFront = protocol.FrontClass
   }),
 
   getApplied: protocol.custom(function(node, options={}) {
     return this._getApplied(node, options).then(ret => {
       return ret.entries;
     });
   }, {
     impl: "_getApplied"
+  }),
+
+  addNewRule: protocol.custom(function(node) {
+    return this._addNewRule(node).then(ret => {
+      return ret.entries[0];
+    });
+  }, {
+    impl: "_addNewRule"
   })
 });
 
-// Predeclare the domstylerule actor type
-types.addActorType("domstylerule");
-
 /**
  * An actor that represents a CSS style object on the protocol.
  *
  * We slightly flatten the CSSOM for this actor, it represents
  * both the CSSRule and CSSStyle objects in one actor.  For nodes
  * (which have a CSSStyle but no CSSRule) we create a StyleRuleActor
  * with a special rule type (100).
  */
@@ -736,30 +823,32 @@ var StyleRuleActor = protocol.ActorClass
     } catch (e) {
       return false;
     }
 
     // Check if the selector is valid and not the same as the original
     // selector
     if (selectorElement && rule.selectorText !== value) {
       let cssRules = parentStyleSheet.cssRules;
+      let cssText = rule.cssText;
+      let selectorText = rule.selectorText;
 
       // Delete the currently selected rule
       let i = 0;
       for (let cssRule of cssRules) {
         if (rule === cssRule) {
           parentStyleSheet.deleteRule(i);
           break;
         }
 
         i++;
       }
 
       // Inserts the new style rule into the current style sheet
-      let ruleText = rule.cssText.slice(rule.selectorText.length).trim();
+      let ruleText = cssText.slice(selectorText.length).trim();
       parentStyleSheet.insertRule(value + " " + ruleText, i);
 
       return true;
     } else {
       return false;
     }
   }, {
     request: { selector: Arg(0, "string") },
--- a/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
+++ b/toolkit/locales/en-US/chrome/global/devtools/styleinspector.properties
@@ -99,16 +99,24 @@ ruleView.contextmenu.showOrigSources.acc
 # LOCALIZATION NOTE (ruleView.contextmenu.showCSSSources): Text displayed in the rule view
 # context menu.
 ruleView.contextmenu.showCSSSources=Show CSS sources
 
 # LOCALIZATION NOTE (ruleView.contextmenu.showCSSSources.accessKey): Access key for
 # the rule view context menu "Show CSS sources" entry.
 ruleView.contextmenu.showCSSSources.accessKey=S
 
+# LOCALIZATION NOTE (ruleView.contextmenu.addRule): Text displayed in the
+# rule view context menu for adding a new rule to the element.
+ruleView.contextmenu.addRule=Add rule
+
+# LOCALIZATION NOTE (ruleView.contextmenu.addRule.accessKey): Access key for
+# the rule view context menu "Add rule" entry.
+ruleView.contextmenu.addRule.accessKey=R
+
 # LOCALIZATION NOTE (computedView.contextmenu.selectAll): Text displayed in the
 # computed view context menu.
 computedView.contextmenu.selectAll=Select all
 
 # LOCALIZATION NOTE (computedView.contextmenu.selectAll.accessKey): Access key for
 # the computed view context menu "Select all" entry.
 computedView.contextmenu.selectAll.accessKey=A