Bug 740615 - Move rule view copy code into CssRuleView.jsm. r=paul
authorDave Camp <dcamp@mozilla.com>
Wed, 04 Apr 2012 09:36:35 -0700
changeset 94302 f47780831b315a783d3d0f508e4a3206e9bc1f35
parent 94301 b00af9fa37bed44a582649f32d4a16dbeea1eb44
child 94303 811fcd591b181326fb37e5d7b318e2127b1ec7b2
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerspaul
bugs740615
milestone14.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 740615 - Move rule view copy code into CssRuleView.jsm. r=paul
browser/devtools/highlighter/inspector.jsm
browser/devtools/highlighter/test/browser_inspector_duplicate_ruleview.js
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -934,37 +934,16 @@ InspectorUI.prototype = {
 
       // Add event handlers bound to this.
       this.boundRuleViewChanged = this.ruleViewChanged.bind(this);
       this.ruleView.element.addEventListener("CssRuleViewChanged",
                                              this.boundRuleViewChanged);
       this.cssRuleViewBoundCSSLinkClicked = this.ruleViewCSSLinkClicked.bind(this);
       this.ruleView.element.addEventListener("CssRuleViewCSSLinkClicked",
                                              this.cssRuleViewBoundCSSLinkClicked);
-      this.cssRuleViewBoundMouseDown = this.ruleViewMouseDown.bind(this);
-      this.ruleView.element.addEventListener("mousedown",
-                                             this.cssRuleViewBoundMouseDown);
-      this.cssRuleViewBoundMouseUp = this.ruleViewMouseUp.bind(this);
-      this.ruleView.element.addEventListener("mouseup",
-                                             this.cssRuleViewBoundMouseUp);
-      this.cssRuleViewBoundMouseMove = this.ruleViewMouseMove.bind(this);
-      this.cssRuleViewBoundMenuUpdate = this.ruleViewMenuUpdate.bind(this);
-
-      this.cssRuleViewBoundCopy = this.ruleViewCopy.bind(this);
-      iframe.addEventListener("copy", this.cssRuleViewBoundCopy);
-
-      this.cssRuleViewBoundCopyRule = this.ruleViewCopyRule.bind(this);
-      this.cssRuleViewBoundCopyDeclaration =
-        this.ruleViewCopyDeclaration.bind(this);
-      this.cssRuleViewBoundCopyProperty = this.ruleViewCopyProperty.bind(this);
-      this.cssRuleViewBoundCopyPropertyValue =
-        this.ruleViewCopyPropertyValue.bind(this);
-
-      // Add the rule view's context menu.
-      this.ruleViewAddContextMenu();
 
       doc.documentElement.appendChild(this.ruleView.element);
       this.ruleView.highlight(this.selection);
       Services.obs.notifyObservers(null,
         INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, null);
     }.bind(this);
 
     iframe.addEventListener("load", boundLoadListener, true);
@@ -1046,373 +1025,33 @@ InspectorUI.prototype = {
         href = rule.elementStyle.element.ownerDocument.location.href;
       }
       let viewSourceUtils = this.chromeWin.gViewSourceUtils;
       viewSourceUtils.viewSource(href, null, doc, line);
     }
   },
 
   /**
-   * This is the mousedown handler for the rule view. We use it to track whether
-   * text is currently getting selected.
-   * .
-   * @param aEvent The event object
-   */
-  ruleViewMouseDown: function IUI_ruleViewMouseDown(aEvent)
-  {
-    this.ruleView.element.addEventListener("mousemove",
-      this.cssRuleViewBoundMouseMove);
-  },
-
-  /**
-   * This is the mouseup handler for the rule view. We use it to track whether
-   * text is currently getting selected.
-   * .
-   * @param aEvent The event object
-   */
-  ruleViewMouseUp: function IUI_ruleViewMouseUp(aEvent)
-  {
-    this.ruleView.element.removeEventListener("mousemove",
-      this.cssRuleViewBoundMouseMove);
-    this.ruleView._selectionMode = false;
-  },
-
-  /**
-   * This is the mousemove handler for the rule view. We use it to track whether
-   * text is currently getting selected.
-   * .
-   * @param aEvent The event object
-   */
-  ruleViewMouseMove: function IUI_ruleViewMouseMove(aEvent)
-  {
-    this.ruleView._selectionMode = true;
-  },
-
-  /**
-   * Add a context menu to the rule view.
-   */
-  ruleViewAddContextMenu: function IUI_ruleViewAddContextMenu()
-  {
-    let iframe = this.getToolIframe(this.ruleViewObject);
-    let popupSet = this.chromeDoc.getElementById("mainPopupSet");
-    let menu = this.chromeDoc.createElement("menupopup");
-    menu.addEventListener("popupshowing", this.cssRuleViewBoundMenuUpdate);
-    menu.id = "rule-view-context-menu";
-
-    // Copy selection
-    let label = styleInspectorStrings
-      .GetStringFromName("rule.contextmenu.copyselection");
-    let accessKey = styleInspectorStrings
-      .GetStringFromName("rule.contextmenu.copyselection.accesskey");
-    let item = this.chromeDoc.createElement("menuitem");
-    item.id = "rule-view-copy";
-    item.setAttribute("label", label);
-    item.setAttribute("accesskey", accessKey);
-    item.addEventListener("command", this.cssRuleViewBoundCopy);
-    menu.appendChild(item);
-
-    // Copy rule
-    label = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copyrule");
-    accessKey = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copyrule.accesskey");
-    item = this.chromeDoc.createElement("menuitem");
-    item.id = "rule-view-copy-rule";
-    item.setAttribute("label", label);
-    item.setAttribute("accesskey", accessKey);
-    item.addEventListener("command", this.cssRuleViewBoundCopyRule);
-    menu.appendChild(item);
-
-    // Copy declaration
-    label = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copydeclaration");
-    accessKey = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copydeclaration.accesskey");
-    item = this.chromeDoc.createElement("menuitem");
-    item.id = "rule-view-copy-declaration";
-    item.setAttribute("label", label);
-    item.setAttribute("accesskey", accessKey);
-    item.addEventListener("command", this.cssRuleViewBoundCopyDeclaration);
-    menu.appendChild(item);
-
-    // Copy property name
-    label = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copyproperty");
-    accessKey = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copyproperty.accesskey");
-    item = this.chromeDoc.createElement("menuitem");
-    item.id = "rule-view-copy-property";
-    item.setAttribute("label", label);
-    item.setAttribute("accesskey", accessKey);
-    item.addEventListener("command", this.cssRuleViewBoundCopyProperty);
-    menu.appendChild(item);
-
-    // Copy property value
-    label = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copypropertyvalue");
-    accessKey = styleInspectorStrings.
-      GetStringFromName("rule.contextmenu.copypropertyvalue.accesskey");
-    item = this.chromeDoc.createElement("menuitem");
-    item.id = "rule-view-copy-property-value";
-    item.setAttribute("label", label);
-    item.setAttribute("accesskey", accessKey);
-    item.addEventListener("command", this.cssRuleViewBoundCopyPropertyValue);
-    menu.appendChild(item);
-
-    popupSet.appendChild(menu);
-
-    iframe.setAttribute("context", menu.id);
-  },
-
-  /**
-   * Update the rule view's context menu by disabling irrelevant menuitems and
-   * enabling relevant ones.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewMenuUpdate: function IUI_ruleViewMenuUpdate(aEvent)
-  {
-    let iframe = this.getToolIframe(this.ruleViewObject);
-    let win = iframe.contentWindow;
-
-    // Copy selection.
-    let disable = win.getSelection().isCollapsed;
-    let menuitem = this.chromeDoc.getElementById("rule-view-copy");
-    menuitem.disabled = disable;
-
-    // Copy property, copy property name & copy property value.
-    let node = this.chromeDoc.popupNode;
-    if (!node.classList.contains("ruleview-property") &&
-        !node.classList.contains("ruleview-computed")) {
-      while (node = node.parentElement) {
-        if (node.classList.contains("ruleview-property") ||
-          node.classList.contains("ruleview-computed")) {
-          break;
-        }
-      }
-    }
-    let disablePropertyItems = !node || (node &&
-      !node.classList.contains("ruleview-property") &&
-      !node.classList.contains("ruleview-computed"));
-
-    menuitem = this.chromeDoc.querySelector("#rule-view-copy-declaration");
-    menuitem.disabled = disablePropertyItems;
-    menuitem = this.chromeDoc.querySelector("#rule-view-copy-property");
-    menuitem.disabled = disablePropertyItems;
-    menuitem = this.chromeDoc.querySelector("#rule-view-copy-property-value");
-    menuitem.disabled = disablePropertyItems;
-  },
-
-  /**
-   * Copy selected text from the rule view.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewCopy: function IUI_ruleViewCopy(aEvent)
-  {
-    let iframe = this.getToolIframe(this.ruleViewObject);
-    let win = iframe.contentWindow;
-    let text = win.getSelection().toString();
-
-    // Remove any double newlines.
-    text = text.replace(/(\r?\n)\r?\n/g, "$1");
-
-    // Remove "inline"
-    let inline = styleInspectorStrings.GetStringFromName("rule.sourceInline");
-    let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
-    text = text.replace(rx, "");
-
-    // Remove file:line
-    text = text.replace(/[\w\.]+:\d+(\r?\n)/g, "$1");
-
-    // Remove inherited from: line
-    let inheritedFrom = styleInspectorStrings
-      .GetStringFromName("rule.inheritedSource");
-    inheritedFrom = inheritedFrom.replace(/\s%S\s\(%S\)/g, "");
-    rx = new RegExp("(\r?\n)" + inheritedFrom + ".*", "g");
-    text = text.replace(rx, "$1");
-
-    clipboardHelper.copyString(text);
-
-    if (aEvent) {
-      aEvent.preventDefault();
-    }
-  },
-
-  /**
-   * Copy a rule from the rule view.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewCopyRule: function IUI_ruleViewCopyRule(aEvent)
-  {
-    let node = this.chromeDoc.popupNode;
-    if (node.className != "ruleview-code") {
-      if (node.className == "ruleview-rule-source") {
-        node = node.nextElementSibling;
-      } else {
-        while (node = node.parentElement) {
-          if (node.className == "ruleview-code") {
-            break;
-          }
-        }
-      }
-    }
-
-    if (node.className == "ruleview-code") {
-      // We need to strip expanded properties from the node because we use
-      // node.textContent below, which also gets text from hidden nodes. The
-      // simplest way to do this is to clone the node and remove them from the
-      // clone.
-      node = node.cloneNode();
-      let computed = node.querySelector(".ruleview-computedlist");
-      if (computed) {
-        computed.parentNode.removeChild(computed);
-      }
-      let autosizer = node.querySelector(".autosizer");
-      if (autosizer) {
-        autosizer.parentNode.removeChild(autosizer);
-      }
-    }
-
-    let text = node.textContent;
-
-    // Format the rule
-    if (osString == "WINNT") {
-      text = text.replace(/{/g, "{\r\n    ");
-      text = text.replace(/;/g, ";\r\n    ");
-      text = text.replace(/\s*}/g, "\r\n}");
-    } else {
-      text = text.replace(/{/g, "{\n    ");
-      text = text.replace(/;/g, ";\n    ");
-      text = text.replace(/\s*}/g, "\n}");
-    }
-
-    clipboardHelper.copyString(text);
-  },
-
-  /**
-   * Copy a declaration from the rule view.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewCopyDeclaration: function IUI_ruleViewCopyDeclaration(aEvent)
-  {
-    let node = this.chromeDoc.popupNode;
-    if (!node.classList.contains("ruleview-property") &&
-        !node.classList.contains("ruleview-computed")) {
-      while (node = node.parentElement) {
-        if (node.classList.contains("ruleview-property") ||
-            node.classList.contains("ruleview-computed")) {
-          break;
-        }
-      }
-    }
-
-    // We need to strip expanded properties from the node because we use
-    // node.textContent below, which also gets text from hidden nodes. The
-    // simplest way to do this is to clone the node and remove them from the
-    // clone.
-    node = node.cloneNode();
-    let computed = node.querySelector(".ruleview-computedlist");
-    if (computed) {
-      computed.parentNode.removeChild(computed);
-    }
-    clipboardHelper.copyString(node.textContent);
-  },
-
-  /**
-   * Copy a property name from the rule view.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewCopyProperty: function IUI_ruleViewCopyProperty(aEvent)
-  {
-    let node = this.chromeDoc.popupNode;
-
-    if (!node.classList.contains("ruleview-propertyname")) {
-      node = node.querySelector(".ruleview-propertyname");
-    }
-
-    if (node) {
-      clipboardHelper.copyString(node.textContent);
-    }
-  },
-
-  /**
-   * Copy a property value from the rule view.
-   *
-   * @param aEvent The event object
-   */
-  ruleViewCopyPropertyValue: function IUI_ruleViewCopyPropertyValue(aEvent)
-  {
-    let node = this.chromeDoc.popupNode;
-
-    if (!node.classList.contains("ruleview-propertyvalue")) {
-      node = node.querySelector(".ruleview-propertyvalue");
-    }
-
-    if (node) {
-      clipboardHelper.copyString(node.textContent);
-    }
-  },
-
-  /**
    * Destroy the rule view.
    */
   destroyRuleView: function IUI_destroyRuleView()
   {
-    let iframe = this.getToolIframe(this.ruleViewObject);
-    iframe.removeEventListener("copy", this.cssRuleViewBoundCopy);
-    iframe.parentNode.removeChild(iframe);
-
     if (this.ruleView) {
-      let menu = this.chromeDoc.querySelector("#rule-view-context-menu");
-      if (menu) {
-        // Copy
-        let menuitem = this.chromeDoc.querySelector("#rule-view-copy");
-        menuitem.removeEventListener("command", this.cssRuleViewBoundCopy);
-
-        // Copy rule
-        menuitem = this.chromeDoc.querySelector("#rule-view-copy-rule");
-        menuitem.removeEventListener("command", this.cssRuleViewBoundCopyRule);
-
-        // Copy property
-        menuitem = this.chromeDoc.querySelector("#rule-view-copy-declaration");
-        menuitem.removeEventListener("command",
-                                     this.cssRuleViewBoundCopyDeclaration);
-
-        // Copy property name
-        menuitem = this.chromeDoc.querySelector("#rule-view-copy-property");
-        menuitem.removeEventListener("command",
-                                     this.cssRuleViewBoundCopyProperty);
-
-        // Copy property value
-        menuitem = this.chromeDoc.querySelector("#rule-view-copy-property-value");
-        menuitem.removeEventListener("command",
-                                     this.cssRuleViewBoundCopyPropertyValue);
-
-        menu.removeEventListener("popupshowing", this.cssRuleViewBoundMenuUpdate);
-        menu.parentNode.removeChild(menu);
-      }
-
       this.ruleView.element.removeEventListener("CssRuleViewChanged",
                                                 this.boundRuleViewChanged);
       this.ruleView.element.removeEventListener("CssRuleViewCSSLinkClicked",
                                                 this.cssRuleViewBoundCSSLinkClicked);
-      this.ruleView.element.removeEventListener("mousedown",
-                                                this.cssRuleViewBoundMouseDown);
-      this.ruleView.element.removeEventListener("mouseup",
-                                                this.cssRuleViewBoundMouseUp);
-      this.ruleView.element.removeEventListener("mousemove",
-                                                this.cssRuleViewBoundMouseMove);
-      delete boundRuleViewChanged;
-      this.ruleView.clear();
+      delete this.boundRuleViewChanged;
+      delete this.cssRuleViewBoundCSSLinkClicked;
+      this.ruleView.destroy();
       delete this.ruleView;
     }
+
+    let iframe = this.getToolIframe(this.ruleViewObject);
+    iframe.parentNode.removeChild(iframe);
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Utility Methods
 
   /**
    * inspect the given node, highlighting it on the page and selecting the
    * correct row in the tree panel
@@ -2626,17 +2265,8 @@ XPCOMUtils.defineLazyGetter(this, "Style
 XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
   return Cc["@mozilla.org/widget/clipboardhelper;1"].
     getService(Ci.nsIClipboardHelper);
 });
-
-XPCOMUtils.defineLazyGetter(this, "styleInspectorStrings", function() {
-  return Services.strings.createBundle(
-    "chrome://browser/locale/devtools/styleinspector.properties");
-});
-
-XPCOMUtils.defineLazyGetter(this, "osString", function() {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
-});
--- a/browser/devtools/highlighter/test/browser_inspector_duplicate_ruleview.js
+++ b/browser/devtools/highlighter/test/browser_inspector_duplicate_ruleview.js
@@ -85,17 +85,19 @@ function inspectorFocusTab1()
 
   // Make sure the inspector is open.
   ok(InspectorUI.inspecting, "Inspector is highlighting");
   ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open");
   is(InspectorUI.store.length, 1, "Inspector.store.length = 1");
   is(InspectorUI.selection, div, "selection matches the div element");
   ok(InspectorUI.isSidebarOpen, "sidebar is open");
   ok(InspectorUI.isRuleViewOpen(), "rule view is open");
-  is(InspectorUI.ruleView.doc.documentElement.children.length, 1, "RuleView elements.length == 1");
+
+  // The rule view element plus its popupSet
+  is(InspectorUI.ruleView.doc.documentElement.children.length, 2, "RuleView elements.length == 2");
 
   requestLongerTimeout(4);
   executeSoon(function() {
     InspectorUI.closeInspectorUI();
     gBrowser.removeCurrentTab(); // tab 1
     gBrowser.removeCurrentTab(); // tab 2
     finish();
   });
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -59,16 +59,17 @@ const FOCUS_BACKWARD = Ci.nsIFocusManage
 // 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");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 var EXPORTED_SYMBOLS = ["CssRuleView",
                         "_ElementStyle",
                         "_editableField",
                         "_getInplaceEditorForSpan"];
 
 /**
  * Our model looks like this:
@@ -694,22 +695,65 @@ TextProperty.prototype = {
 function CssRuleView(aDoc, aStore)
 {
   this.doc = aDoc;
   this.store = aStore;
   this.element = this.doc.createElementNS(XUL_NS, "vbox");
   this.element.setAttribute("tabindex", "0");
   this.element.classList.add("ruleview");
   this.element.flex = 1;
+  this._selectionMode = false;
+
+  this._boundMouseDown = this._onMouseDown.bind(this);
+  this.element.addEventListener("mousedown",
+                                this._boundMouseDown);
+  this._boundMouseUp = this._onMouseUp.bind(this);
+  this.element.addEventListener("mouseup",
+                                this._boundMouseUp);
+  this._boundMouseMove = this._onMouseMove.bind(this);
+
+  this._boundCopy = this._onCopy.bind(this);
+  this.element.addEventListener("copy", this._boundCopy);
+
+  this._createContextMenu();
 }
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
   _viewedElement: null,
 
+  destroy: function CssRuleView_destroy()
+  {
+    this.clear();
+
+    this.element.removeEventListener("copy", this._boundCopy);
+    this._copyItem.removeEventListener("command", this._boundCopy);
+    delete this._boundCopy;
+
+    this._ruleItem.removeEventListener("command", this._boundCopyRule);
+    delete this._boundCopyRule;
+
+    this._declarationItem.removeEventListener("command", this._boundCopyDeclaration);
+    delete this._boundCopyDeclaration;
+
+    this._propertyItem.removeEventListener("command", this._boundCopyProperty);
+    delete this._boundCopyProperty;
+
+    this._propertyValueItem.removeEventListener("command", this._boundCopyPropertyValue);
+    delete this._boundCopyPropertyValue;
+
+    this._contextMenu.removeEventListener("popupshowing", this._boundMenuUpdate);
+    delete this._boundMenuUpdate;
+    delete this._contextMenu;
+
+    if (this.element.parentNode) {
+      this.element.parentNode.removeChild(this.element);
+    }
+  },
+
   /**
    * Update the highlighted element.
    *
    * @param {nsIDOMElement} aElement
    *        The node whose style rules we'll inspect.
    */
   highlight: function CssRuleView_highlight(aElement)
   {
@@ -757,17 +801,17 @@ CssRuleView.prototype = {
   /**
    * Update the rules for the currently highlighted element.
    */
   nodeChanged: function CssRuleView_nodeChanged()
   {
     this._clearRules();
     this._elementStyle.populate();
     this._createEditors();
-  },  
+  },
 
   /**
    * Clear the rules.
    */
   _clearRules: function CssRuleView_clearRules()
   {
     while (this.element.hasChildNodes()) {
       this.element.removeChild(this.element.lastChild);
@@ -808,16 +852,272 @@ CssRuleView.prototype = {
   {
     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, rule);
       this.element.appendChild(editor.element);
     }
   },
+
+  /**
+   * Add a context menu to the rule view.
+   */
+  _createContextMenu: function CssRuleView_createContextMenu()
+  {
+    let popupSet = this.doc.createElement("popupset");
+    this.doc.documentElement.appendChild(popupSet);
+
+    let menu = this.doc.createElement("menupopup");
+    menu.id = "rule-view-context-menu";
+
+    this._boundMenuUpdate = this._onMenuUpdate.bind(this);
+    menu.addEventListener("popupshowing", this._boundMenuUpdate);
+
+    // Copy selection
+    this._copyItem = createMenuItem(menu, {
+      label: "rule.contextmenu.copyselection",
+      accesskey: "rule.contextmenu.copyselection.accesskey",
+      command: this._boundCopy
+    });
+
+    // Copy rule
+    this._boundCopyRule = this._onCopyRule.bind(this);
+    this._ruleItem = createMenuItem(menu, {
+      label: "rule.contextmenu.copyrule",
+      accesskey: "rule.contextmenu.copyrule.accesskey",
+      command: this._boundCopyRule
+    });
+
+    // Copy declaration
+    this._boundCopyDeclaration = this._onCopyDeclaration.bind(this);
+    this._declarationItem = createMenuItem(menu, {
+      label: "rule.contextmenu.copydeclaration",
+      accesskey: "rule.contextmenu.copydeclaration.accesskey",
+      command: this._boundCopyDeclaration
+    });
+
+    this._boundCopyProperty = this._onCopyProperty.bind(this);
+    this._propertyItem = createMenuItem(menu, {
+      label: "rule.contextmenu.copyproperty",
+      accesskey: "rule.contextmenu.copyproperty.accesskey",
+      command: this._boundCopyProperty
+    });
+
+    this._boundCopyPropertyValue = this._onCopyPropertyValue.bind(this);
+    this._propertyValueItem = createMenuItem(menu,{
+      label: "rule.contextmenu.copypropertyvalue",
+      accesskey: "rule.contextmenu.copypropertyvalue.accesskey",
+      command: this._boundCopyPropertyValue
+    });
+
+    popupSet.appendChild(menu);
+    this.element.setAttribute("context", menu.id);
+
+    this._contextMenu = menu;
+  },
+
+  /**
+   * Update the rule view's context menu by disabling irrelevant menuitems and
+   * enabling relevant ones.
+   *
+   * @param aEvent The event object
+   */
+  _onMenuUpdate: function CssRuleView_onMenuUpdate(aEvent)
+  {
+    // Copy selection.
+    let disable = this.doc.defaultView.getSelection().isCollapsed;
+    this._copyItem.disabled = disable;
+
+    // Copy property, copy property name & copy property value.
+    let node = this.doc.popupNode;
+    if (!node.classList.contains("ruleview-property") &&
+        !node.classList.contains("ruleview-computed")) {
+      while (node = node.parentElement) {
+        if (node.classList.contains("ruleview-property") ||
+          node.classList.contains("ruleview-computed")) {
+          break;
+        }
+      }
+    }
+    let disablePropertyItems = !node || (node &&
+      !node.classList.contains("ruleview-property") &&
+      !node.classList.contains("ruleview-computed"));
+
+    this._declarationItem.disabled = disablePropertyItems;
+    this._propertyItem.disabled = disablePropertyItems;
+    this._propertyValueItem.disabled = disablePropertyItems;
+
+    dump("Done updating menu!\n");
+  },
+
+  _onMouseDown: function CssRuleView_onMouseDown()
+  {
+    this.element.addEventListener("mousemove", this._boundMouseMove);
+  },
+
+  _onMouseUp: function CssRuleView_onMouseUp()
+  {
+    this.element.removeEventListener("mousemove", this._boundMouseMove);
+    this._selectionMode = false;
+  },
+
+  _onMouseMove: function CssRuleView_onMouseMove()
+  {
+    this._selectionMode = true;
+  },
+
+  /**
+   * Copy selected text from the rule view.
+   *
+   * @param aEvent The event object
+   */
+  _onCopy: function CssRuleView_onCopy(aEvent)
+  {
+    let win = this.doc.defaultView;
+    let text = win.getSelection().toString();
+
+    // Remove any double newlines.
+    text = text.replace(/(\r?\n)\r?\n/g, "$1");
+
+    // Remove "inline"
+    let inline = _strings.GetStringFromName("rule.sourceInline");
+    let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
+    text = text.replace(rx, "");
+
+    // Remove file:line
+    text = text.replace(/[\w\.]+:\d+(\r?\n)/g, "$1");
+
+    // Remove inherited from: line
+    let inheritedFrom = _strings.
+      GetStringFromName("rule.inheritedSource");
+    inheritedFrom = inheritedFrom.replace(/\s%S\s\(%S\)/g, "");
+    rx = new RegExp("(\r?\n)" + inheritedFrom + ".*", "g");
+    text = text.replace(rx, "$1");
+
+    clipboardHelper.copyString(text);
+
+    if (aEvent) {
+      aEvent.preventDefault();
+    }
+  },
+
+  /**
+   * Copy a rule from the rule view.
+   *
+   * @param aEvent The event object
+   */
+  _onCopyRule: function CssRuleView_onCopyRule(aEvent)
+  {
+    let node = this.doc.popupNode;
+    if (node.className != "ruleview-code") {
+      if (node.className == "ruleview-rule-source") {
+        node = node.nextElementSibling;
+      } else {
+        while (node = node.parentElement) {
+          if (node.className == "ruleview-code") {
+            break;
+          }
+        }
+      }
+    }
+
+    if (node.className == "ruleview-code") {
+      // We need to strip expanded properties from the node because we use
+      // node.textContent below, which also gets text from hidden nodes. The
+      // simplest way to do this is to clone the node and remove them from the
+      // clone.
+      node = node.cloneNode();
+      let computed = node.querySelector(".ruleview-computedlist");
+      if (computed) {
+        computed.parentNode.removeChild(computed);
+      }
+    }
+
+    let text = node.textContent;
+
+    // Format the rule
+    if (osString == "WINNT") {
+      text = text.replace(/{/g, "{\r\n    ");
+      text = text.replace(/;/g, ";\r\n    ");
+      text = text.replace(/\s*}/g, "\r\n}");
+    } else {
+      text = text.replace(/{/g, "{\n    ");
+      text = text.replace(/;/g, ";\n    ");
+      text = text.replace(/\s*}/g, "\n}");
+    }
+
+    clipboardHelper.copyString(text);
+  },
+
+  /**
+   * Copy a declaration from the rule view.
+   *
+   * @param aEvent The event object
+   */
+  _onCopyDeclaration: function CssRuleView_onCopyDeclaration(aEvent)
+  {
+    let node = this.doc.popupNode;
+    if (!node.classList.contains("ruleview-property") &&
+        !node.classList.contains("ruleview-computed")) {
+      while (node = node.parentElement) {
+        if (node.classList.contains("ruleview-property") ||
+            node.classList.contains("ruleview-computed")) {
+          break;
+        }
+      }
+    }
+
+    // We need to strip expanded properties from the node because we use
+    // node.textContent below, which also gets text from hidden nodes. The
+    // simplest way to do this is to clone the node and remove them from the
+    // clone.
+    node = node.cloneNode();
+    let computed = node.querySelector(".ruleview-computedlist");
+    if (computed) {
+      computed.parentNode.removeChild(computed);
+    }
+    clipboardHelper.copyString(node.textContent);
+  },
+
+  /**
+   * Copy a property name from the rule view.
+   *
+   * @param aEvent The event object
+   */
+  _onCopyProperty: function CssRuleView_onCopyProperty(aEvent)
+  {
+    let node = this.doc.popupNode;
+
+    if (!node.classList.contains("ruleview-propertyname")) {
+      node = node.querySelector(".ruleview-propertyname");
+    }
+
+    if (node) {
+      clipboardHelper.copyString(node.textContent);
+    }
+  },
+
+ /**
+   * Copy a property value from the rule view.
+   *
+   * @param aEvent The event object
+   */
+  _onCopyPropertyValue: function CssRuleView_onCopyPropertyValue(aEvent)
+  {
+    let node = this.doc.popupNode;
+
+    if (!node.classList.contains("ruleview-propertyvalue")) {
+      node = node.querySelector(".ruleview-propertyvalue");
+    }
+
+    if (node) {
+      clipboardHelper.copyString(node.textContent);
+    }
+  }
 };
 
 /**
  * Create a RuleEditor.
  *
  * @param CssRuleView aRuleView
  *        The CssRuleView containg the document holding this rule editor and the
  *        _selectionMode flag.
@@ -1564,16 +1864,28 @@ function createChild(aParent, aTag, aAtt
         elt.setAttribute(attr, aAttributes[attr]);
       }
     }
   }
   aParent.appendChild(elt);
   return elt;
 }
 
+function createMenuItem(aMenu, aAttributes)
+{
+  let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
+  item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
+  item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
+  item.addEventListener("command", aAttributes.command);
+
+  aMenu.appendChild(item);
+
+  return item;
+}
+
 /**
  * Append a text node to an element.
  */
 function appendText(aParent, aText)
 {
   aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
 }
 
@@ -1593,8 +1905,23 @@ function copyTextStyles(aFrom, aTo)
 /**
  * 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);
 }
+
+XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
+  return Cc["@mozilla.org/widget/clipboardhelper;1"].
+    getService(Ci.nsIClipboardHelper);
+});
+
+XPCOMUtils.defineLazyGetter(this, "_strings", function() {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/devtools/styleinspector.properties");
+});
+
+XPCOMUtils.defineLazyGetter(this, "osString", function() {
+  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
+});
+
--- a/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_bug_703643_context_menu_copy.js
@@ -65,17 +65,17 @@ function inspectorUIOpen()
 }
 
 function testClip()
 {
   Services.obs.removeObserver(testClip,
     InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false);
 
   executeSoon(function() {
-    info("Checking that InspectorUI.ruleViewCopyRule() returns " +
+    info("Checking that _onCopyRule() returns " +
          "the correct clipboard value");
     let expectedPattern = "element {[\\r\\n]+" +
       "    margin: 10em;[\\r\\n]+" +
       "    font-size: 14pt;[\\r\\n]+" +
       "    font-family: helvetica,sans-serif;[\\r\\n]+" +
       "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
       "}[\\r\\n]*";
     info("Expected pattern: " + expectedPattern);
@@ -101,86 +101,86 @@ function checkCopyRule() {
   is(propName, "font-family", "checking property name");
   is(propValue, "helvetica,sans-serif", "checking property value");
 
   // We need the context menu to open in the correct place in order for
   // popupNode to be propertly set.
   EventUtils.synthesizeMouse(prop, 1, 1, { type: "contextmenu", button: 2 },
     ruleView.contentWindow);
 
-  InspectorUI.ruleViewCopyRule();
+  InspectorUI.ruleView._boundCopyRule();
 }
 
 function checkCopyProperty()
 {
-  info("Checking that InspectorUI.cssRuleViewBoundCopyDeclaration() returns " +
+  info("Checking that _onCopyDeclaration() returns " +
        "the correct clipboard value");
   let expectedPattern = "font-family: helvetica,sans-serif;";
   info("Expected pattern: " + expectedPattern);
 
   SimpleTest.waitForClipboard(function IUI_boundCopyPropCheck() {
       return checkClipboardData(expectedPattern);
     },
-    InspectorUI.cssRuleViewBoundCopyDeclaration,
+    InspectorUI.ruleView._boundCopyDeclaration,
     checkCopyPropertyName, checkCopyPropertyName);
 }
 
 function checkCopyPropertyName()
 {
-  info("Checking that InspectorUI.cssRuleViewBoundCopyProperty() returns " +
+  info("Checking that _onCopyProperty() returns " +
        "the correct clipboard value");
   let expectedPattern = "font-family";
   info("Expected pattern: " + expectedPattern);
 
   SimpleTest.waitForClipboard(function IUI_boundCopyPropNameCheck() {
       return checkClipboardData(expectedPattern);
     },
-    InspectorUI.cssRuleViewBoundCopyProperty,
+    InspectorUI.ruleView._boundCopyProperty,
     checkCopyPropertyValue, checkCopyPropertyValue);
 }
 
 function checkCopyPropertyValue()
 {
-  info("Checking that InspectorUI.cssRuleViewBoundCopyPropertyValue() " +
+  info("Checking that _onCopyPropertyValue() " +
        " returns the correct clipboard value");
   let expectedPattern = "helvetica,sans-serif";
   info("Expected pattern: " + expectedPattern);
 
   SimpleTest.waitForClipboard(function IUI_boundCopyPropValueCheck() {
       return checkClipboardData(expectedPattern);
     },
-    InspectorUI.cssRuleViewBoundCopyPropertyValue,
+    InspectorUI.ruleView._boundCopyPropertyValue,
     checkCopySelection, checkCopySelection);
 }
 
 function checkCopySelection()
 {
   let ruleView = document.querySelector("#devtools-sidebar-iframe-ruleview");
   let contentDoc = ruleView.contentDocument;
   let props = contentDoc.querySelectorAll(".ruleview-property");
 
   let range = document.createRange();
   range.setStart(props[0], 0);
   range.setEnd(props[4], 8);
   ruleView.contentWindow.getSelection().addRange(range);
 
-  info("Checking that InspectorUI.cssRuleViewBoundCopy()  returns the correct" +
+  info("Checking that _onCopy()  returns the correct" +
        "clipboard value");
   let expectedPattern = "    margin: 10em;[\\r\\n]+" +
                         "    font-size: 14pt;[\\r\\n]+" +
                         "    font-family: helvetica,sans-serif;[\\r\\n]+" +
                         "    color: rgb\\(170, 170, 170\\);[\\r\\n]+" +
                         "}[\\r\\n]+" +
                         "html {[\\r\\n]+" +
                         "    color: rgb\\(0, 0, 0\\);[\\r\\n]*";
   info("Expected pattern: " + expectedPattern);
 
   SimpleTest.waitForClipboard(function IUI_boundCopyCheck() {
       return checkClipboardData(expectedPattern);
-    },InspectorUI.cssRuleViewBoundCopy, finishup, finishup);
+    },InspectorUI.ruleView._boundCopy, finishup, finishup);
 }
 
 function checkClipboardData(aExpectedPattern)
 {
   let actual = SpecialPowers.getClipboardData("text/unicode");
   let expectedRegExp = new RegExp(aExpectedPattern, "g");
   return expectedRegExp.test(actual);
 }