Bug 585563 - The inspector style panel should link to the CSS editor; r=dcamp f=cedricv
authorMichael Ratcliffe <mratcliffe@mozilla.com>
Fri, 10 Feb 2012 13:39:47 +0000
changeset 87132 e5ecebbd9631c5a23a05c715ee162de82730dbce
parent 87131 39f8849e89c59a41d49aef2b1c81eada867f39d5
child 87133 a6e809d5a44618d179966405f7a9a69f1fb2890e
push id22081
push usertim.taubert@gmx.de
push dateSat, 18 Feb 2012 01:04:38 +0000
treeherdermozilla-central@87bb3cff1864 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdcamp
bugs585563
milestone13.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 585563 - The inspector style panel should link to the CSS editor; r=dcamp f=cedricv
browser/base/content/browser.js
browser/devtools/highlighter/inspector.jsm
browser/devtools/styleeditor/StyleEditor.jsm
browser/devtools/styleeditor/StyleEditorChrome.jsm
browser/devtools/styleeditor/styleeditor.xul
browser/devtools/styleeditor/test/Makefile.in
browser/devtools/styleeditor/test/browser_styleeditor_passedinsheet.js
browser/devtools/styleeditor/test/head.js
browser/devtools/styleinspector/CssHtmlTree.jsm
browser/devtools/styleinspector/CssRuleView.jsm
browser/devtools/styleinspector/csshtmltree.xul
browser/themes/gnomestripe/devtools/csshtmltree.css
browser/themes/pinstripe/devtools/csshtmltree.css
browser/themes/winstripe/devtools/csshtmltree.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -9048,38 +9048,54 @@ var Scratchpad = {
 XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() {
   let tmp = {};
   Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", tmp);
   return tmp.ScratchpadManager;
 });
 
 var StyleEditor = {
   prefEnabledName: "devtools.styleeditor.enabled",
-  openChrome: function SE_openChrome()
+  /**
+   * Opens the style editor. If the UI is already open, it will be focused.
+   *
+   * @param {CSSStyleSheet} [aSelectedStyleSheet] default Stylesheet.
+   * @param {Number} [aLine] Line to which the caret should be moved (one-indexed).
+   * @param {Number} [aCol] Column to which the caret should be moved (one-indexed).
+   */
+  openChrome: function SE_openChrome(aSelectedStyleSheet, aLine, aCol)
   {
     const CHROME_URL = "chrome://browser/content/styleeditor.xul";
     const CHROME_WINDOW_TYPE = "Tools:StyleEditor";
     const CHROME_WINDOW_FLAGS = "chrome,centerscreen,resizable,dialog=no";
 
     // focus currently open Style Editor window for this document, if any
     let contentWindow = gBrowser.selectedBrowser.contentWindow;
     let contentWindowID = contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
       getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
     let enumerator = Services.wm.getEnumerator(CHROME_WINDOW_TYPE);
     while (enumerator.hasMoreElements()) {
       var win = enumerator.getNext();
       if (win.styleEditorChrome.contentWindowID == contentWindowID) {
+        if (aSelectedStyleSheet) {
+          win.styleEditorChrome.selectStyleSheet(aSelectedStyleSheet, aLine, aCol);
+        }
         win.focus();
         return win;
       }
     }
 
+    let args = {
+      contentWindow: contentWindow,
+      selectedStyleSheet: aSelectedStyleSheet,
+      line: aLine,
+      col: aCol
+    };
+    args.wrappedJSObject = args;
     let chromeWindow = Services.ww.openWindow(null, CHROME_URL, "_blank",
-                                              CHROME_WINDOW_FLAGS,
-                                              contentWindow);
+                                              CHROME_WINDOW_FLAGS, args);
     chromeWindow.focus();
     return chromeWindow;
   }
 };
 
 function onWebDeveloperMenuShowing() {
   document.getElementById("Tools:WebConsole").setAttribute("checked", HUDConsoleUI.getOpenHUD() != null);
 }
--- a/browser/devtools/highlighter/inspector.jsm
+++ b/browser/devtools/highlighter/inspector.jsm
@@ -758,16 +758,19 @@ InspectorUI.prototype = {
         this.store.setValue(winID, "ruleView", ruleViewStore);
       }
 
       this.ruleView = new CssRuleView(doc, ruleViewStore);
 
       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);
 
       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);
@@ -796,26 +799,52 @@ InspectorUI.prototype = {
 
   ruleViewChanged: function IUI_ruleViewChanged()
   {
     this.isDirty = true;
     this.nodeChanged(this.ruleViewObject);
   },
 
   /**
+   * When a css link is clicked this method is called in order to either:
+   *   1. Open the link in view source (for element style attributes)
+   *   2. Open the link in the style editor
+   *
+   * @param aEvent The event containing the style rule to act on
+   */
+  ruleViewCSSLinkClicked: function(aEvent)
+  {
+    if (!this.chromeWin) {
+      return;
+    }
+
+    let rule = aEvent.detail.rule;
+    let styleSheet = rule.sheet;
+
+    if (styleSheet) {
+      this.chromeWin.StyleEditor.openChrome(styleSheet, rule.ruleLine);
+    } else {
+      let href = rule.elementStyle.element.ownerDocument.location.href;
+      this.chromeWin.openUILinkIn("view-source:" + href, "window");
+    }
+  },
+
+  /**
    * Destroy the rule view.
    */
   destroyRuleView: function IUI_destroyRuleView()
   {
     let iframe = this.getToolIframe(this.ruleViewObject);
     iframe.parentNode.removeChild(iframe);
 
     if (this.ruleView) {
       this.ruleView.element.removeEventListener("CssRuleViewChanged",
                                                 this.boundRuleViewChanged);
+      this.ruleView.element.removeEventListener("CssRuleViewCSSLinkClicked",
+                                                this.cssRuleViewBoundCSSLinkClicked);
       delete boundRuleViewChanged;
       this.ruleView.clear();
       delete this.ruleView;
     }
   },
 
   /////////////////////////////////////////////////////////////////////////
   //// Utility Methods
--- a/browser/devtools/styleeditor/StyleEditor.jsm
+++ b/browser/devtools/styleeditor/StyleEditor.jsm
@@ -141,17 +141,17 @@ StyleEditor.prototype = {
 
   /**
    * Retrieve the stylesheet this editor is attached to.
    *
    * @return DOMStyleSheet
    */
   get styleSheet()
   {
-    assert(this._styleSheet, "StyleSheet must be loaded first.")
+    assert(this._styleSheet, "StyleSheet must be loaded first.");
     return this._styleSheet;
   },
 
   /**
    * Retrieve the index (order) of stylesheet in the document.
    *
    * @return number
    */
@@ -916,19 +916,21 @@ StyleEditor.prototype = {
   {
     // insert the origin editor instance as first argument
     if (!aArgs) {
       aArgs = [this];
     } else {
       aArgs.unshift(this);
     }
 
+    // copy the list of listeners to allow adding/removing listeners in handlers
+    let listeners = this._actionListeners.concat();
     // trigger all listeners that have this action handler
-    for (let i = 0; i < this._actionListeners.length; ++i) {
-      let listener = this._actionListeners[i];
+    for (let i = 0; i < listeners.length; ++i) {
+      let listener = listeners[i];
       let actionHandler = listener["on" + aName];
       if (actionHandler) {
         actionHandler.apply(listener, aArgs);
       }
     }
 
     // when a flag got changed, user-facing state need to be persisted
     if (aName == "FlagChange") {
--- a/browser/devtools/styleeditor/StyleEditorChrome.jsm
+++ b/browser/devtools/styleeditor/StyleEditorChrome.jsm
@@ -265,19 +265,21 @@ StyleEditorChrome.prototype = {
   {
     // insert the origin Chrome instance as first argument
     if (!aArgs) {
       aArgs = [this];
     } else {
       aArgs.unshift(this);
     }
 
-    // trigger all listeners that have this named handler
-    for (let i = 0; i < this._listeners.length; ++i) {
-      let listener = this._listeners[i];
+    // copy the list of listeners to allow adding/removing listeners in handlers
+    let listeners = this._listeners.concat();
+    // trigger all listeners that have this named handler.
+    for (let i = 0; i < listeners.length; i++) {
+      let listener = listeners[i];
       let handler = listener["on" + aName];
       if (handler) {
         handler.apply(listener, aArgs);
       }
     }
   },
 
   /**
@@ -324,20 +326,20 @@ StyleEditorChrome.prototype = {
    * Populate the chrome UI according to the content document.
    *
    * @see StyleEditor._setupShadowStyleSheet
    */
   _populateChrome: function SEC__populateChrome()
   {
     this._resetChrome();
 
+    let document = this.contentDocument;
     this._document.title = _("chromeWindowTitle",
-          this.contentDocument.title || this.contentDocument.location.href);
+      document.title || document.location.href);
 
-    let document = this.contentDocument;
     for (let i = 0; i < document.styleSheets.length; ++i) {
       let styleSheet = document.styleSheets[i];
 
       let editor = new StyleEditor(document, styleSheet);
       editor.addActionListener(this);
       this._editors.push(editor);
     }
 
@@ -348,16 +350,89 @@ StyleEditorChrome.prototype = {
     // NOT loaded/ready yet. This also helps responsivity during loading when
     // there are many heavy stylesheets.
     this._editors.forEach(function (aEditor) {
       this._window.setTimeout(aEditor.load.bind(aEditor), 0);
     }, this);
   },
 
   /**
+   * selects a stylesheet and optionally moves the cursor to a selected line
+   *
+   * @param {CSSStyleSheet} [aSheet]
+   *        Stylesheet that should be selected. If a stylesheet is not passed
+   *        and the editor is not initialized we focus the first stylesheet. If
+   *        a stylesheet is not passed and the editor is initialized we ignore
+   *        the call.
+   * @param {Number} [aLine]
+   *        Line to which the caret should be moved (one-indexed).
+   * @param {Number} [aCol]
+   *        Column to which the caret should be moved (one-indexed).
+   */
+  selectStyleSheet: function SEC_selectSheet(aSheet, aLine, aCol)
+  {
+    let select = function DEC_select(aEditor) {
+      let summary = aSheet ? this.getSummaryElementForEditor(aEditor)
+                           : this._view.getSummaryElementByOrdinal(0);
+      let setCaret = false;
+
+      if (aLine || aCol) {
+        aLine = aLine || 1;
+        aCol = aCol || 1;
+        setCaret = true;
+      }
+      if (!aEditor.sourceEditor) {
+        // If a line or column was specified we move the caret appropriately.
+        if (setCaret) {
+          aEditor.addActionListener({
+            onAttach: function SEC_selectSheet_onAttach()
+            {
+              aEditor.removeActionListener(this);
+              aEditor.sourceEditor.setCaretPosition(aLine - 1, aCol - 1);
+            }
+          });
+        }
+        this._view.activeSummary = summary;
+      } else {
+        this._view.activeSummary = summary;
+
+        // If a line or column was specified we move the caret appropriately.
+        if (setCaret) {
+          aEditor.sourceEditor.setCaretPosition(aLine - 1, aCol - 1);
+        }
+      }
+    }.bind(this);
+
+    if (!this.editors.length) {
+      // We are in the main initialization phase so we wait for the editor
+      // containing the target stylesheet to be added and select the target
+      // stylesheet, optionally moving the cursor to a selected line.
+      this.addChromeListener({
+        onEditorAdded: function SEC_selectSheet_onEditorAdded(aChrome, aEditor) {
+          if ((!aSheet && aEditor.styleSheetIndex == 0) ||
+              aEditor.styleSheet == aSheet) {
+            aChrome.removeChromeListener(this);
+            select(aEditor);
+          }
+        }
+      });
+    } else if (aSheet) {
+      // We are already initialized and a stylesheet has been specified. Here
+      // we iterate through the editors and select the one containing the target
+      // stylesheet, optionally moving the cursor to a selected line.
+      for each (let editor in this.editors) {
+        if (editor.styleSheet == aSheet) {
+          select(editor);
+          break;
+        }
+      }
+    }
+  },
+
+  /**
    * Disable all UI, effectively making editors read-only.
    * This is automatically called when no content window is attached.
    *
    * @see contentWindow
    */
   _disableChrome: function SEC__disableChrome()
   {
     let matches = this._root.querySelectorAll("button,toolbarbutton,textbox");
@@ -450,19 +525,18 @@ StyleEditorChrome.prototype = {
 
         aSummary.addEventListener("focus", function onSummaryFocus(aEvent) {
           if (aEvent.target == aSummary) {
             // autofocus the stylesheet name
             aSummary.querySelector(".stylesheet-name").focus();
           }
         }, false);
 
-        // autofocus the first or new stylesheet
-        if (editor.styleSheetIndex == 0 ||
-            editor.hasFlag(StyleEditorFlags.NEW)) {
+        // autofocus new stylesheets
+        if (editor.hasFlag(StyleEditorFlags.NEW)) {
           this._view.activeSummary = aSummary;
         }
 
         this._triggerChromeListeners("EditorAdded", [editor]);
       }.bind(this),
       onHide: function ASV_onItemShow(aSummary, aDetails, aData) {
         aData.editor.onHide();
       },
--- a/browser/devtools/styleeditor/styleeditor.xul
+++ b/browser/devtools/styleeditor/styleeditor.xul
@@ -127,13 +127,15 @@
                data-placeholder="&editorTextbox.placeholder;"/>
     </xul:box>
   </div> <!-- #splitview-templates -->
 </xul:box>   <!-- .splitview-root -->
 
 <xul:script type="application/javascript"><![CDATA[
 Components.utils.import("resource:///modules/devtools/StyleEditorChrome.jsm");
 let chromeRoot = document.getElementById("style-editor-chrome");
-let contentWindow = window.arguments[0];
+let args = window.arguments[0].wrappedJSObject;
+let contentWindow = args.contentWindow;
 let chrome = new StyleEditorChrome(chromeRoot, contentWindow);
+chrome.selectStyleSheet(args.selectedStyleSheet, args.line, args.col);
 window.styleEditorChrome = chrome;
 ]]></xul:script>
 </xul:window>
--- a/browser/devtools/styleeditor/test/Makefile.in
+++ b/browser/devtools/styleeditor/test/Makefile.in
@@ -46,16 +46,17 @@ include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
                  browser_styleeditor_enabled.js \
                  browser_styleeditor_filesave.js \
                  browser_styleeditor_import.js \
                  browser_styleeditor_init.js \
                  browser_styleeditor_loading.js \
                  browser_styleeditor_new.js \
+                 browser_styleeditor_passedinsheet.js \
                  browser_styleeditor_pretty.js \
                  browser_styleeditor_readonly.js \
                  browser_styleeditor_reopen.js \
                  browser_styleeditor_sv_keynav.js \
                  browser_styleeditor_sv_resize.js \
                  four.html \
                  head.js \
                  media.html \
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_passedinsheet.js
@@ -0,0 +1,61 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TESTCASE_URI = TEST_BASE + "simple.html";
+const LINE = 6;
+const COL = 2;
+
+let editor = null;
+let sheet = null;
+
+function test()
+{
+  waitForExplicitFinish();
+  gBrowser.selectedBrowser.addEventListener("load", function () {
+    gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
+    run();
+  }, true);
+  content.location = TESTCASE_URI;
+}
+
+function run()
+{
+  sheet = content.document.styleSheets[1];
+  launchStyleEditorChrome(function attachListeners(aChrome) {
+    aChrome.addChromeListener({
+      onEditorAdded: checkSourceEditor
+    });
+  }, sheet, LINE, COL);
+}
+
+function checkSourceEditor(aChrome, aEditor)
+{
+  if (!aEditor.sourceEditor) {
+    aEditor.addActionListener({
+      onAttach: function (aEditor) {
+        aEditor.removeActionListener(this);
+        validate(aEditor);
+      }
+    });
+  } else {
+    validate(aEditor);
+  }
+}
+
+function validate(aEditor)
+{
+  info("validating style editor");
+  let sourceEditor = aEditor.sourceEditor;
+  let caretPosition = sourceEditor.getCaretPosition();
+  is(caretPosition.line, LINE - 1, "caret row is correct"); // index based
+  is(caretPosition.col, COL - 1, "caret column is correct");
+  is(aEditor.styleSheet, sheet, "loaded stylesheet matches document stylesheet");
+  finishUp();
+}
+
+function finishUp()
+{
+  editor = sheet = null;
+  finish();
+}
--- a/browser/devtools/styleeditor/test/head.js
+++ b/browser/devtools/styleeditor/test/head.js
@@ -14,33 +14,33 @@ function cleanup()
     gChromeWindow.close();
     gChromeWindow = null;
   }
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 }
 
-function launchStyleEditorChrome(aCallback)
+function launchStyleEditorChrome(aCallback, aSheet, aLine, aCol)
 {
-  gChromeWindow = StyleEditor.openChrome();
+  gChromeWindow = StyleEditor.openChrome(aSheet, aLine, aCol);
   if (gChromeWindow.document.readyState != "complete") {
     gChromeWindow.addEventListener("load", function onChromeLoad() {
       gChromeWindow.removeEventListener("load", onChromeLoad, true);
       gChromeWindow.styleEditorChrome._alwaysDisableAnimations = true;
       aCallback(gChromeWindow.styleEditorChrome);
     }, true);
   } else {
     gChromeWindow.styleEditorChrome._alwaysDisableAnimations = true;
     aCallback(gChromeWindow.styleEditorChrome);
   }
 }
 
-function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback)
+function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback, aSheet, aLine, aCol)
 {
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
-    launchStyleEditorChrome(aCallback);
+    launchStyleEditorChrome(aCallback, aSheet, aLine, aCol);
   }, true);
 }
 
 registerCleanupFunction(cleanup);
--- a/browser/devtools/styleinspector/CssHtmlTree.jsm
+++ b/browser/devtools/styleinspector/CssHtmlTree.jsm
@@ -969,9 +969,29 @@ SelectorView.prototype = {
           this.tree.styleInspector.selectFromPath(source);
           aEvent.preventDefault();
         }.bind(this), false);
       result += ".style";
     }
 
     return result;
   },
+
+  /**
+   * When a css link is clicked this method is called in order to either:
+   *   1. Open the link in view source (for element style attributes).
+   *   2. Open the link in the style editor.
+   *
+   * @param aEvent The click event
+   */
+  openStyleEditor: function(aEvent)
+  {
+    if (this.selectorInfo.selector._cssRule._cssSheet) {
+      let styleSheet = this.selectorInfo.selector._cssRule._cssSheet.domSheet;
+      let line = this.selectorInfo.ruleLine;
+
+      this.tree.win.StyleEditor.openChrome(styleSheet, line);
+    } else {
+      let href = this.selectorInfo.sourceElement.ownerDocument.location.href;
+      this.tree.win.openUILinkIn("view-source:" + href, "window");
+    }
+  },
 };
--- a/browser/devtools/styleinspector/CssRuleView.jsm
+++ b/browser/devtools/styleinspector/CssRuleView.jsm
@@ -33,17 +33,17 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-"use strict"
+"use strict";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
@@ -319,17 +319,17 @@ ElementStyle.prototype = {
       dirty = computedProp._overriddenDirty || dirty;
       delete computedProp._overriddenDirty;
     }
 
     dirty = (!!aProp.overridden != overridden) || dirty;
     aProp.overridden = overridden;
     return dirty;
   }
-}
+};
 
 /**
  * A single style rule or declaration.
  *
  * @param {ElementStyle} aElementStyle
  *        The ElementStyle to which this rule belongs.
  * @param {object} aOptions
  *        The information used to construct this rule.  Properties include:
@@ -353,37 +353,55 @@ function Rule(aElementStyle, aOptions)
 }
 
 Rule.prototype = {
   get title()
   {
     if (this._title) {
       return this._title;
     }
-    let sheet = this.domRule ? this.domRule.parentStyleSheet : null;
-    this._title = CssLogic.shortSource(sheet);
+    this._title = CssLogic.shortSource(this.sheet);
     if (this.domRule) {
-      let line = this.elementStyle.domUtils.getRuleLine(this.domRule);
-      this._title += ":" + line;
+      this._title += ":" + this.ruleLine;
     }
 
     if (this.inherited) {
       let eltText = this.inherited.tagName.toLowerCase();
       if (this.inherited.id) {
         eltText += "#" + this.inherited.id;
       }
       let args = [eltText, this._title];
       this._title = CssLogic._strings.formatStringFromName("rule.inheritedSource",
                                                            args, args.length);
     }
 
     return this._title;
   },
 
   /**
+   * The rule's stylesheet.
+   */
+  get sheet()
+  {
+    return this.domRule ? this.domRule.parentStyleSheet : null;
+  },
+
+  /**
+   * The rule's line within a stylesheet
+   */
+  get ruleLine()
+  {
+    if (!this.sheet) {
+      // No stylesheet, no ruleLine
+      return null;
+    }
+    return this.elementStyle.domUtils.getRuleLine(this.domRule);
+  },
+
+  /**
    * Create a new TextProperty to include in the rule.
    *
    * @param {string} aName
    *        The text property name (such as "background" or "border-top").
    * @param {string} aValue
    *        The property's value (not including priority).
    * @param {string} aPriority
    *        The property's priority (either "important" or an empty string).
@@ -525,17 +543,17 @@ Rule.prototype = {
 
     for each (let prop in disabledProps) {
       let textProp = new TextProperty(this, prop.name,
                                       prop.value, prop.priority);
       textProp.enabled = false;
       this.textProps.push(textProp);
     }
   },
-}
+};
 
 /**
  * A single property in a rule's cssText.
  *
  * @param {Rule} aRule
  *        The rule this TextProperty came from.
  * @param {string} aName
  *        The text property name (such as "background" or "border-top").
@@ -613,17 +631,17 @@ TextProperty.prototype = {
     this.rule.setPropertyEnabled(this, aValue);
     this.updateEditor();
   },
 
   remove: function TextProperty_remove()
   {
     this.rule.removeProperty(this);
   }
-}
+};
 
 
 /**
  * View hierarchy mostly follows the model hierarchy.
  *
  * CssRuleView:
  *   Owns an ElementStyle and creates a list of RuleEditors for its
  *    Rules.
@@ -638,29 +656,28 @@ TextProperty.prototype = {
  *   Can mark a property disabled or enabled.
  */
 
 /**
  * CssRuleView is a view of the style rules and declarations that
  * apply to a given element.  After construction, the 'element'
  * property will be available with the user interface.
  *
- * @param Document aDocument
+ * @param Document aDoc
  *        The document that will contain the rule view.
  * @param object aStore
  *        The CSS rule view can use this object to store metadata
  *        that might outlast the rule view, particularly the current
  *        set of disabled properties.
  * @constructor
  */
 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;
 }
 
 CssRuleView.prototype = {
   // The element that we're inspecting.
@@ -763,16 +780,24 @@ RuleEditor.prototype = {
     // span to be placed absolutely against.
     this.element.style.position = "relative";
 
     // Add the source link.
     let source = createChild(this.element, "div", {
       class: "ruleview-rule-source",
       textContent: this.rule.title
     });
+    source.addEventListener("click", function() {
+      let rule = this.rule;
+      let evt = this.doc.createEvent("CustomEvent");
+      evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, {
+        rule: rule,
+      });
+      this.element.dispatchEvent(evt);
+    }.bind(this));
 
     let code = createChild(this.element, "div", {
       class: "ruleview-code"
     });
 
     let header = createChild(code, "div", {});
 
     let selectors = createChild(header, "span", {
@@ -1089,18 +1114,16 @@ TextPropertyEditor.prototype = {
    *
    * @param {string} aValue
    *        The value from the text editor.
    * @return an object with 'value' and 'priority' properties.
    */
   _parseValue: function TextPropertyEditor_parseValue(aValue)
   {
     let pieces = aValue.split("!", 2);
-    let value = pieces[0];
-    let priority = pieces.length > 1 ? pieces[1] : "";
     return {
       value: pieces[0].trim(),
       priority: (pieces.length > 1 ? pieces[1].trim() : "")
     };
   },
 
   /**
    * Called when a value editor closes.  If the user pressed escape,
--- a/browser/devtools/styleinspector/csshtmltree.xul
+++ b/browser/devtools/styleinspector/csshtmltree.xul
@@ -109,17 +109,17 @@ To visually debug the templates without 
   <div id="templateMatchedSelectors">
     <table>
       <loop foreach="selector in ${matchedSelectorViews}">
         <tr>
           <td dir="ltr" class="rule-text ${selector.statusClass}">
             ${selector.humanReadableText(__element)}
           </td>
           <td class="rule-link">
-            <a target="_blank" href="view-source:${selector.selectorInfo.href}" class="link"
+            <a target="_blank" onclick="${selector.openStyleEditor}" class="link"
                title="${selector.selectorInfo.href}">${selector.selectorInfo.source}</a>
           </td>
         </tr>
       </loop>
     </table>
   </div>
 </div>
 
--- a/browser/themes/gnomestripe/devtools/csshtmltree.css
+++ b/browser/themes/gnomestripe/devtools/csshtmltree.css
@@ -63,16 +63,19 @@
   color: #0091ff;
 }
 .link,
 .helplink,
 .link:visited,
 .helplink:visited {
   text-decoration: none;
 }
+.link:hover {
+  text-decoration: underline;
+}
 
 .helplink {
   display: block;
   height: 14px;
   width: 0;
   overflow: hidden;
   -moz-padding-start: 14px;
   background-image: url("chrome://browser/skin/devtools/goto-mdn.png");
@@ -130,16 +133,17 @@
 .property-view-hidden,
 .property-content-hidden {
   display: none;
 }
 
 .rule-link {
   text-align: end;
   -moz-padding-start: 10px;
+  cursor: pointer;
 }
 
 /* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */
 .rule-text {
   direction: ltr;
   padding: 0;
   -moz-padding-start: 20px;
 }
@@ -195,17 +199,23 @@
  */
 
 .ruleview {
   background-color: #FFF;
 }
 
 .ruleview-rule-source {
   background-color: -moz-dialog;
+  color: #0091ff;
   padding: 2px 5px;
+  cursor: pointer;
+}
+
+.ruleview-rule-source:hover {
+  text-decoration: underline;
 }
 
 .ruleview-code {
   padding: 2px 5px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;
--- a/browser/themes/pinstripe/devtools/csshtmltree.css
+++ b/browser/themes/pinstripe/devtools/csshtmltree.css
@@ -63,16 +63,19 @@
   color: #0091ff;
 }
 .link,
 .helplink,
 .link:visited,
 .helplink:visited {
   text-decoration: none;
 }
+.link:hover {
+  text-decoration: underline;
+}
 
 .helplink {
   display: block;
   height: 14px;
   width: 0;
   overflow: hidden;
   -moz-padding-start: 14px;
   background-image: url("chrome://browser/skin/devtools/goto-mdn.png");
@@ -132,16 +135,17 @@
 .property-view-hidden,
 .property-content-hidden {
   display: none;
 }
 
 .rule-link {
   text-align: end;
   -moz-padding-start: 10px;
+  cursor: pointer;
 }
 
 /* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */
 .rule-text {
   direction: ltr;
   padding: 0;
   -moz-padding-start: 20px;
 }
@@ -197,17 +201,23 @@
  */
 
 .ruleview {
   background-color: #FFF;
 }
 
 .ruleview-rule-source {
   background-color: -moz-dialog;
+  color: #0091ff;
   padding: 2px 5px;
+  cursor: pointer;
+}
+
+.ruleview-rule-source:hover {
+  text-decoration: underline;
 }
 
 .ruleview-code {
   padding: 2px 5px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;
--- a/browser/themes/winstripe/devtools/csshtmltree.css
+++ b/browser/themes/winstripe/devtools/csshtmltree.css
@@ -62,16 +62,19 @@
   color: #0091ff;
 }
 .link,
 .helplink,
 .link:visited,
 .helplink:visited {
   text-decoration: none;
 }
+.link:hover {
+  text-decoration: underline;
+}
 
 .helplink {
   display: block;
   height: 14px;
   width: 0;
   overflow: hidden;
   -moz-padding-start: 14px;
   background-image: url("chrome://browser/skin/devtools/goto-mdn.png");
@@ -130,16 +133,17 @@
 .property-view-hidden,
 .property-content-hidden {
   display: none;
 }
 
 .rule-link {
   text-align: end;
   -moz-padding-start: 10px;
+  cursor: pointer;
 }
 
 /* This rule is necessary because Templater.jsm breaks LTR TDs in RTL docs */
 .rule-text {
   direction: ltr;
   padding: 0;
   -moz-padding-start: 20px;
 }
@@ -195,17 +199,23 @@
  */
 
 .ruleview {
   background-color: #FFF;
 }
 
 .ruleview-rule-source {
   background-color: -moz-dialog;
+  color: #0091ff;
   padding: 2px 5px;
+  cursor: pointer;
+}
+
+.ruleview-rule-source:hover {
+  text-decoration: underline;
 }
 
 .ruleview-code {
   padding: 2px 5px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;