Bug 966895 - [rule view] Adding new rules to the current selection in the CSS rule-view r=harth
☠☠ backed out by 89ef4a15ea03 ☠ ☠
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 02 Jul 2014 23:52:00 +0200
changeset 191989 afdb479e642701717fb948218f5705745dae11c3
parent 191988 caefe18d7ffef0df36a99e9ecd1f368bddced144
child 191990 ef6fcda84ce40a1c61a61166c1a76e63a52b97ff
push id7566
push usercbook@mozilla.com
push dateThu, 03 Jul 2014 07:51:36 +0000
treeherderfx-team@2307dffdebfe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs966895
milestone33.0a1
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
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);
+      elementStyle.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,18 @@ 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_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.");
+}
--- 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).
  */
--- 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