diff --git a/browser/devtools/highlighter/highlighter.jsm b/browser/devtools/highlighter/highlighter.jsm index 1b3f8a77866..d479c82a6f5 100644 --- a/browser/devtools/highlighter/highlighter.jsm +++ b/browser/devtools/highlighter/highlighter.jsm @@ -43,7 +43,11 @@ * ***** END LICENSE BLOCK ***** */ const Cu = Components.utils; +const Cc = Components.classes; +const Ci = Components.interfaces; + Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); var EXPORTED_SYMBOLS = ["Highlighter"]; @@ -59,6 +63,9 @@ const INSPECTOR_INVISIBLE_ELEMENTS = { "title": true, }; +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; + // add ":visited" and ":link" after bug 713106 is fixed + /** * A highlighter mechanism. * @@ -109,6 +116,8 @@ const INSPECTOR_INVISIBLE_ELEMENTS = { * "highlighting" - Highlighter is highlighting * "locked" - The selected node has been locked * "unlocked" - The selected ndoe has been unlocked + * "pseudoclasstoggled" - A pseudo-class lock has changed on the selected node + * * Structure: * @@ -238,6 +247,17 @@ Highlighter.prototype = { } }, + /** + * Notify that a pseudo-class lock was toggled on the highlighted element + * + * @param aPseudo - The pseudo-class to toggle, e.g. ":hover". + */ + pseudoClassLockToggled: function Highlighter_pseudoClassLockToggled(aPseudo) + { + this.emitEvent("pseudoclasstoggled", [aPseudo]); + this.updateInfobar(); + }, + /** * Update the highlighter size and position. */ @@ -446,29 +466,80 @@ Highlighter.prototype = { let classesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); classesBox.id = "highlighter-nodeinfobar-classes"; + + let pseudoClassesBox = this.chromeDoc.createElementNS("http://www.w3.org/1999/xhtml", "span"); + pseudoClassesBox.id = "highlighter-nodeinfobar-pseudo-classes"; + // Add some content to force a better boundingClientRect down below. - classesBox.textContent = " "; + pseudoClassesBox.textContent = " "; nodeInfobar.appendChild(tagNameLabel); nodeInfobar.appendChild(idLabel); nodeInfobar.appendChild(classesBox); + nodeInfobar.appendChild(pseudoClassesBox); container.appendChild(arrowBoxTop); container.appendChild(nodeInfobar); container.appendChild(arrowBoxBottom); aParent.appendChild(container); + nodeInfobar.onclick = (function _onInfobarRightClick(aEvent) { + if (aEvent.button == 2) { + this.openPseudoClassMenu(); + } + }).bind(this); + let barHeight = container.getBoundingClientRect().height; this.nodeInfo = { tagNameLabel: tagNameLabel, idLabel: idLabel, classesBox: classesBox, + pseudoClassesBox: pseudoClassesBox, container: container, barHeight: barHeight, }; }, + /** + * Open the infobar's pseudo-class context menu. + */ + openPseudoClassMenu: function Highlighter_openPseudoClassMenu() + { + let menu = this.chromeDoc.createElement("menupopup"); + menu.id = "infobar-context-menu"; + + let popupSet = this.chromeDoc.getElementById("mainPopupSet"); + popupSet.appendChild(menu); + + let fragment = this.buildPseudoClassMenu(); + menu.appendChild(fragment); + + menu.openPopup(this.nodeInfo.pseudoClassesBox, "end_before", 0, 0, true, false); + }, + + /** + * Create the menuitems for toggling the selection's pseudo-class state + * + * @returns DocumentFragment. The menuitems for toggling pseudo-classes. + */ + buildPseudoClassMenu: function IUI_buildPseudoClassesMenu() + { + let fragment = this.chromeDoc.createDocumentFragment(); + for (let i = 0; i < PSEUDO_CLASSES.length; i++) { + let pseudo = PSEUDO_CLASSES[i]; + let item = this.chromeDoc.createElement("menuitem"); + item.setAttribute("type", "checkbox"); + item.setAttribute("label", pseudo); + item.addEventListener("command", + this.pseudoClassLockToggled.bind(this, pseudo), false); + item.setAttribute("checked", DOMUtils.hasPseudoClassLock(this.node, + pseudo)); + fragment.appendChild(item); + } + return fragment; + }, + /** * Highlight a rectangular region. * @@ -543,6 +614,14 @@ Highlighter.prototype = { classes.textContent = this.node.classList.length ? "." + Array.join(this.node.classList, ".") : ""; + + // Pseudo-classes + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(this.node, pseudo); + }, this); + + let pseudoBox = this.nodeInfo.pseudoClassesBox; + pseudoBox.textContent = pseudos.join(""); }, /** @@ -617,8 +696,8 @@ Highlighter.prototype = { */ computeZoomFactor: function Highlighter_computeZoomFactor() { this.zoom = - this.win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) - .getInterface(Components.interfaces.nsIDOMWindowUtils) + this.win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) .screenPixelsPerCSSPixel; }, @@ -805,3 +884,6 @@ Highlighter.prototype = { /////////////////////////////////////////////////////////////////////////// +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils) +}); diff --git a/browser/devtools/highlighter/inspector.jsm b/browser/devtools/highlighter/inspector.jsm index 5ecea73be18..1cc4c4639d0 100644 --- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -83,6 +83,8 @@ const INSPECTOR_NOTIFICATIONS = { EDITOR_SAVED: "inspector-editor-saved", }; +const PSEUDO_CLASSES = [":hover", ":active", ":focus"]; + /////////////////////////////////////////////////////////////////////////// //// InspectorUI @@ -362,6 +364,7 @@ InspectorUI.prototype = { show: this.openRuleView, hide: this.closeRuleView, onSelect: this.selectInRuleView, + onChanged: this.changeInRuleView, panel: null, unregister: this.destroyRuleView, sidebar: true, @@ -481,6 +484,7 @@ InspectorUI.prototype = { if (!aKeepStore) { this.store.deleteStore(this.winID); this.win.removeEventListener("pagehide", this, true); + this.clearPseudoClassLocks(); } else { // Update the store before closing. if (this.selection) { @@ -566,7 +570,7 @@ InspectorUI.prototype = { this.inspecting = false; this.toolsDim(false); if (this.highlighter.getNode()) { - this.select(this.highlighter.getNode(), true, true, !aPreventScroll); + this.select(this.highlighter.getNode(), true, !aPreventScroll); } else { this.select(null, true, true); } @@ -574,15 +578,17 @@ InspectorUI.prototype = { }, /** - * Select an object in the tree view. + * Select an object in the inspector. * @param aNode * node to inspect * @param forceUpdate * force an update? * @param aScroll boolean * scroll the tree panel? + * @param aFrom [optional] string + * which part of the UI the selection occured from */ - select: function IUI_select(aNode, forceUpdate, aScroll) + select: function IUI_select(aNode, forceUpdate, aScroll, aFrom) { // if currently editing an attribute value, using the // highlighter dismisses the editor @@ -593,6 +599,10 @@ InspectorUI.prototype = { aNode = this.defaultSelection; if (forceUpdate || aNode != this.selection) { + if (aFrom != "breadcrumbs") { + this.clearPseudoClassLocks(); + } + this.selection = aNode; if (!this.inspecting) { this.highlighter.highlight(this.selection); @@ -605,6 +615,41 @@ InspectorUI.prototype = { this.toolsSelect(aScroll); }, + + /** + * Toggle the pseudo-class lock on the currently inspected element. If the + * pseudo-class is :hover or :active, that pseudo-class will also be toggled + * on every ancestor of the element, mirroring real :hover and :active + * behavior. + * + * @param aPseudo the pseudo-class lock to toggle, e.g. ":hover" + */ + togglePseudoClassLock: function IUI_togglePseudoClassLock(aPseudo) + { + if (DOMUtils.hasPseudoClassLock(this.selection, aPseudo)) { + this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { + DOMUtils.removePseudoClassLock(crumb.node, aPseudo); + }); + } else { + let hierarchical = aPseudo == ":hover" || aPseudo == ":active"; + let node = this.selection; + do { + DOMUtils.addPseudoClassLock(node, aPseudo); + node = node.parentNode; + } while (hierarchical && node.parentNode) + } + this.nodeChanged(); + }, + + /** + * Clear all pseudo-class locks applied to elements in the node hierarchy + */ + clearPseudoClassLocks: function IUI_clearPseudoClassLocks() + { + this.breadcrumbs.nodeHierarchy.forEach(function(crumb) { + DOMUtils.clearPseudoClassLocks(crumb.node); + }); + }, /** * Called when the highlighted node is changed by a tool. @@ -616,6 +661,7 @@ InspectorUI.prototype = { nodeChanged: function IUI_nodeChanged(aUpdater) { this.highlighter.invalidateSize(); + this.breadcrumbs.updateSelectors(); this.toolsOnChanged(aUpdater); }, @@ -641,6 +687,10 @@ InspectorUI.prototype = { self.select(self.highlighter.getNode(), false, false); }); + this.highlighter.addListener("pseudoclasstoggled", function(aPseudo) { + self.togglePseudoClassLock(aPseudo); + }); + if (this.store.getValue(this.winID, "inspecting")) { this.startInspecting(); this.highlighter.unlock(); @@ -911,6 +961,15 @@ InspectorUI.prototype = { if (this.ruleView) this.ruleView.highlight(aNode); }, + + /** + * Update the rules for the current node in the Css Rule View. + */ + changeInRuleView: function IUI_selectInRuleView() + { + if (this.ruleView) + this.ruleView.nodeChanged(); + }, ruleViewChanged: function IUI_ruleViewChanged() { @@ -1336,7 +1395,7 @@ InspectorUI.prototype = { toolsOnChanged: function IUI_toolsChanged(aUpdater) { this.toolsDo(function IUI_toolsOnChanged(aTool) { - if (aTool.isOpen && ("onChanged" in aTool) && aTool != aUpdater) { + if (("onChanged" in aTool) && aTool != aUpdater) { aTool.onChanged.call(aTool.context); } }); @@ -1730,6 +1789,13 @@ HTMLBreadcrumbs.prototype = { for (let i = 0; i < aNode.classList.length; i++) { text += "." + aNode.classList[i]; } + for (let i = 0; i < PSEUDO_CLASSES.length; i++) { + let pseudo = PSEUDO_CLASSES[i]; + if (DOMUtils.hasPseudoClassLock(aNode, pseudo)) { + text += pseudo; + } + } + return text; }, @@ -1755,6 +1821,9 @@ HTMLBreadcrumbs.prototype = { let classesLabel = this.IUI.chromeDoc.createElement("label"); classesLabel.className = "inspector-breadcrumbs-classes plain"; + + let pseudosLabel = this.IUI.chromeDoc.createElement("label"); + pseudosLabel.className = "inspector-breadcrumbs-pseudo-classes plain"; tagLabel.textContent = aNode.tagName.toLowerCase(); idLabel.textContent = aNode.id ? ("#" + aNode.id) : ""; @@ -1765,9 +1834,15 @@ HTMLBreadcrumbs.prototype = { } classesLabel.textContent = classesText; + let pseudos = PSEUDO_CLASSES.filter(function(pseudo) { + return DOMUtils.hasPseudoClassLock(aNode, pseudo); + }, this); + pseudosLabel.textContent = pseudos.join(""); + fragment.appendChild(tagLabel); fragment.appendChild(idLabel); fragment.appendChild(classesLabel); + fragment.appendChild(pseudosLabel); return fragment; }, @@ -1807,7 +1882,7 @@ HTMLBreadcrumbs.prototype = { item.onmouseup = (function(aNode) { return function() { - inspector.select(aNode, true, true); + inspector.select(aNode, true, true, "breadcrumbs"); } })(nodes[i]); @@ -1961,7 +2036,7 @@ HTMLBreadcrumbs.prototype = { button.onBreadcrumbsClick = function onBreadcrumbsClick() { inspector.stopInspecting(); - inspector.select(aNode, true, true); + inspector.select(aNode, true, true, "breadcrumbs"); }; button.onclick = (function _onBreadcrumbsRightClick(aEvent) { @@ -2076,6 +2151,20 @@ HTMLBreadcrumbs.prototype = { let element = this.nodeHierarchy[this.currentIndex].button; scrollbox.ensureElementIsVisible(element); }, + + updateSelectors: function BC_updateSelectors() + { + for (let i = this.nodeHierarchy.length - 1; i >= 0; i--) { + let crumb = this.nodeHierarchy[i]; + let button = crumb.button; + + while(button.hasChildNodes()) { + button.removeChild(button.firstChild); + } + button.appendChild(this.prettyPrintNodeAsXUL(crumb.node)); + button.setAttribute("tooltiptext", this.prettyPrintNodeAsText(crumb.node)); + } + }, /** * Update the breadcrumbs display when a new node is selected. @@ -2117,6 +2206,8 @@ HTMLBreadcrumbs.prototype = { // Make sure the selected node and its neighbours are visible. this.scroll(); + + this.updateSelectors(); }, } @@ -2136,3 +2227,6 @@ XPCOMUtils.defineLazyGetter(this, "StyleInspector", function () { return obj.StyleInspector; }); +XPCOMUtils.defineLazyGetter(this, "DOMUtils", function () { + return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); +}); diff --git a/browser/devtools/highlighter/test/Makefile.in b/browser/devtools/highlighter/test/Makefile.in index bc99b0b86df..a76b018f175 100644 --- a/browser/devtools/highlighter/test/Makefile.in +++ b/browser/devtools/highlighter/test/Makefile.in @@ -72,6 +72,7 @@ _BROWSER_FILES = \ browser_inspector_invalidate.js \ browser_inspector_sidebarstate.js \ browser_inspector_treePanel_menu.js \ + browser_inspector_pseudoclass_lock.js \ head.js \ $(NULL) diff --git a/browser/devtools/highlighter/test/browser_inspector_pseudoclass_lock.js b/browser/devtools/highlighter/test/browser_inspector_pseudoclass_lock.js new file mode 100644 index 00000000000..e5067896b3c --- /dev/null +++ b/browser/devtools/highlighter/test/browser_inspector_pseudoclass_lock.js @@ -0,0 +1,154 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let DOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils); + +let doc; +let div; + +let pseudo = ":hover"; + +function test() +{ + waitForExplicitFinish(); + ignoreAllUncaughtExceptions(); + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + doc = content.document; + waitForFocus(createDocument, content); + }, true); + + content.location = "data:text/html,pseudo-class lock tests"; +} + +function createDocument() +{ + div = doc.createElement("div"); + div.textContent = "test div"; + + let head = doc.getElementsByTagName('head')[0]; + let style = doc.createElement('style'); + let rules = doc.createTextNode('div { color: red; } div:hover { color: blue; }'); + + style.appendChild(rules); + head.appendChild(style); + doc.body.appendChild(div); + + setupTests(); +} + +function setupTests() +{ + Services.obs.addObserver(selectNode, + InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED, false); + InspectorUI.openInspectorUI(); +} + +function selectNode() +{ + Services.obs.removeObserver(selectNode, + InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); + + executeSoon(function() { + InspectorUI.highlighter.addListener("nodeselected", openRuleView); + InspectorUI.inspectNode(div); + }); +} + +function openRuleView() +{ + Services.obs.addObserver(performTests, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY, false); + + InspectorUI.showSidebar(); + InspectorUI.openRuleView(); +} + +function performTests() +{ + Services.obs.removeObserver(performTests, + InspectorUI.INSPECTOR_NOTIFICATIONS.RULEVIEWREADY); + + InspectorUI.highlighter.removeListener("nodeselected", performTests); + + // toggle the class + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + testAdded(); + + // toggle the lock off + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + testRemoved(); + testRemovedFromUI(); + + // toggle it back on + InspectorUI.highlighter.pseudoClassLockToggled(pseudo); + + // close the inspector + Services.obs.addObserver(testInspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); + InspectorUI.closeInspectorUI(); +} + +function testAdded() +{ + // lock is applied to it and ancestors + let node = div; + do { + is(DOMUtils.hasPseudoClassLock(node, pseudo), true, + "pseudo-class lock has been applied"); + node = node.parentNode; + } while (node.parentNode) + + // infobar selector contains pseudo-class + let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes"); + is(pseudoClassesBox.textContent, pseudo, "pseudo-class in infobar selector"); + + // ruleview contains pseudo-class rule + is(InspectorUI.ruleView.element.children.length, 3, + "rule view is showing 3 rules for pseudo-class locked div"); + + is(InspectorUI.ruleView.element.children[1]._ruleEditor.rule.selectorText, + "div:hover", "rule view is showing " + pseudo + " rule"); +} + +function testRemoved() +{ + // lock removed from node and ancestors + let node = div; + do { + is(DOMUtils.hasPseudoClassLock(node, pseudo), false, + "pseudo-class lock has been removed"); + node = node.parentNode; + } while (node.parentNode) +} + +function testRemovedFromUI() +{ + // infobar selector doesn't contain pseudo-class + let pseudoClassesBox = document.getElementById("highlighter-nodeinfobar-pseudo-classes"); + is(pseudoClassesBox.textContent, "", "pseudo-class removed from infobar selector"); + + // ruleview no longer contains pseudo-class rule + is(InspectorUI.ruleView.element.children.length, 2, + "rule view is showing 2 rules after removing lock"); +} + +function testInspectorClosed() +{ + Services.obs.removeObserver(testInspectorClosed, + InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); + + testRemoved(); + + finishUp(); +} + +function finishUp() +{ + doc = div = null; + gBrowser.removeCurrentTab(); + finish(); +} diff --git a/browser/devtools/styleinspector/CssRuleView.jsm b/browser/devtools/styleinspector/CssRuleView.jsm index f001b49f9d6..74832d6d93d 100644 --- a/browser/devtools/styleinspector/CssRuleView.jsm +++ b/browser/devtools/styleinspector/CssRuleView.jsm @@ -116,7 +116,7 @@ function ElementStyle(aElement, aStore) // how their .style attribute reflects them as computed values. this.dummyElement = doc.createElementNS(this.element.namespaceURI, this.element.tagName); - this._populate(); + this.populate(); } // We're exporting _ElementStyle for unit tests. var _ElementStyle = ElementStyle; @@ -147,7 +147,7 @@ ElementStyle.prototype = { * Refresh the list of rules to be displayed for the active element. * Upon completion, this.rules[] will hold a list of Rule objects. */ - _populate: function ElementStyle_populate() + populate: function ElementStyle_populate() { this.rules = []; @@ -713,15 +713,33 @@ CssRuleView.prototype = { this._createEditors(); }, + + /** + * 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); + } + }, /** * Clear the rule view. */ clear: function CssRuleView_clear() { - while (this.element.hasChildNodes()) { - this.element.removeChild(this.element.lastChild); - } + this._clearRules(); this._viewedElement = null; this._elementStyle = null; }, diff --git a/browser/themes/gnomestripe/browser.css b/browser/themes/gnomestripe/browser.css index 5cfa13522d5..403b7744360 100644 --- a/browser/themes/gnomestripe/browser.css +++ b/browser/themes/gnomestripe/browser.css @@ -2014,6 +2014,10 @@ html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { @@ -2118,11 +2122,19 @@ html|*#highlighter-nodeinfobar-id { color: hsl(205,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type { diff --git a/browser/themes/pinstripe/browser.css b/browser/themes/pinstripe/browser.css index eb88910e5be..1f75bb67609 100644 --- a/browser/themes/pinstripe/browser.css +++ b/browser/themes/pinstripe/browser.css @@ -2761,6 +2761,10 @@ html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { @@ -2859,11 +2863,19 @@ html|*#highlighter-nodeinfobar-id { color: hsl(205,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type { diff --git a/browser/themes/winstripe/browser.css b/browser/themes/winstripe/browser.css index 5063877aefc..ab738d05d16 100644 --- a/browser/themes/winstripe/browser.css +++ b/browser/themes/winstripe/browser.css @@ -2709,6 +2709,10 @@ html|*#highlighter-nodeinfobar-id { color: hsl(90, 79%, 52%); } +html|*#highlighter-nodeinfobar-pseudo-classes { + color: hsl(20, 100%, 70%); +} + /* Highlighter - Node Infobar - box & arrow */ #highlighter-nodeinfobar { @@ -2813,11 +2817,19 @@ html|*#highlighter-nodeinfobar-id { color: hsl(200,100%,70%); } +.inspector-breadcrumbs-button[checked] > .inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 70%); +} + .inspector-breadcrumbs-id, .inspector-breadcrumbs-classes { color: #8d99a6; } +.inspector-breadcrumbs-pseudo-classes { + color: hsl(20, 100%, 85%); +} + /* Highlighter toolbar - breadcrumbs - LTR */ .inspector-breadcrumbs-button:-moz-locale-dir(ltr):first-of-type {