Web Inspector: Emulate pseudo styles (hover etc.) of non-selected elements
authorapavlov@chromium.org <apavlov@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Jul 2012 16:10:45 +0000 (16:10 +0000)
committerapavlov@chromium.org <apavlov@chromium.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 4 Jul 2012 16:10:45 +0000 (16:10 +0000)
https://bugs.webkit.org/show_bug.cgi?id=86630

Reviewed by Pavel Feldman.

Source/WebCore:

- A map of pseudo-states for all bound DOM nodes is maintained in the backend and queried whenever StyleResolver
calculates the effective element style.
- In the frontend, markers are introduced to distinguish elements that have forced pseudo styles set for them.
Additionally, dimmed markers are added for collapsed nodes, whose descendants have forced pseudo styles.
More ElementDecorator subtypes will be added for other types of markers.

Test: inspector/styles/force-pseudo-state.html

* English.lproj/localizedStrings.js:
* inspector/InspectorCSSAgent.cpp:
(WebCore::InspectorCSSAgent::InspectorCSSAgent):
(WebCore::InspectorCSSAgent::clearFrontend):
(WebCore::InspectorCSSAgent::reset):
(WebCore::InspectorCSSAgent::forcePseudoState):
(WebCore::InspectorCSSAgent::recalcStyleForPseudoStateIfNeeded):
(WebCore::InspectorCSSAgent::elementForId):
(WebCore::InspectorCSSAgent::didRemoveDocument):
(WebCore::InspectorCSSAgent::didRemoveDOMNode):
(WebCore::InspectorCSSAgent::resetPseudoStates):
* inspector/InspectorCSSAgent.h:
(InspectorCSSAgent):
* inspector/InspectorDOMAgent.cpp:
(WebCore::InspectorDOMAgent::unbind):
(WebCore::InspectorDOMAgent::didRemoveDOMNode):
* inspector/front-end/ElementsPanel.js:
(WebInspector.ElementsPanel.get this):
(WebInspector.ElementsPanel):
(WebInspector.ElementsPanel.prototype._setPseudoClassForNodeId):
* inspector/front-end/ElementsTreeOutline.js:
(WebInspector.ElementsTreeOutline):
(WebInspector.ElementsTreeOutline.prototype._createNodeDecorators):
(WebInspector.ElementsTreeOutline.prototype.updateOpenCloseTags):
(WebInspector.ElementsTreeOutline.ElementDecorator):
(WebInspector.ElementsTreeOutline.ElementDecorator.prototype.decorate):
(WebInspector.ElementsTreeOutline.ElementDecorator.prototype.decorateAncestor):
(WebInspector.ElementsTreeOutline.PseudoStateDecorator):
(WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype.decorate):
(WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype.decorateAncestor):
(WebInspector.ElementsTreeElement.prototype._populateTagContextMenu):
(WebInspector.ElementsTreeElement.prototype._populateForcedPseudoStateItems):
(WebInspector.ElementsTreeElement.prototype.updateTitle):
(WebInspector.ElementsTreeElement.prototype._createDecoratorElement):
(WebInspector.ElementsTreeElement.prototype._updateDecorations):
* inspector/front-end/StylesSidebarPane.js:
(WebInspector.StylesSidebarPane):
(WebInspector.StylesSidebarPane.prototype.get forcedPseudoClasses):
(WebInspector.StylesSidebarPane.prototype._updateForcedPseudoStateInputs):
(WebInspector.StylesSidebarPane.prototype.update):
(WebInspector.StylesSidebarPane.prototype._refreshUpdate):
(WebInspector.StylesSidebarPane.prototype._rebuildUpdate):
(WebInspector.StylesSidebarPane.prototype._toggleElementStatePane):
(WebInspector.StylesSidebarPane.prototype._createElementStatePane.clickListener):
* inspector/front-end/elementsPanel.css:
(#elements-content .elements-gutter-decoration):
(#elements-content .elements-gutter-decoration.elements-has-decorated-children):

LayoutTests:

* inspector/styles/force-pseudo-state-expected.txt: Added.
* inspector/styles/force-pseudo-state.html: Added.

git-svn-id: http://svn.webkit.org/repository/webkit/trunk@121860 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/styles/force-pseudo-state-expected.txt [new file with mode: 0644]
LayoutTests/inspector/styles/force-pseudo-state.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/English.lproj/localizedStrings.js
Source/WebCore/inspector/InspectorCSSAgent.cpp
Source/WebCore/inspector/InspectorCSSAgent.h
Source/WebCore/inspector/InspectorDOMAgent.cpp
Source/WebCore/inspector/front-end/ElementsPanel.js
Source/WebCore/inspector/front-end/ElementsTreeOutline.js
Source/WebCore/inspector/front-end/StylesSidebarPane.js
Source/WebCore/inspector/front-end/elementsPanel.css

index c693891..d85261d 100644 (file)
@@ -1,3 +1,13 @@
+2012-07-04  Alexander Pavlov  <apavlov@chromium.org>
+
+        Web Inspector: Emulate pseudo styles (hover etc.) of non-selected elements
+        https://bugs.webkit.org/show_bug.cgi?id=86630
+
+        Reviewed by Pavel Feldman.
+
+        * inspector/styles/force-pseudo-state-expected.txt: Added.
+        * inspector/styles/force-pseudo-state.html: Added.
+
 2012-07-04  Vsevolod Vlasov  <vsevik@chromium.org>
 
         REGRESSION(r121792): inspector/extensions/extensions-resources.html fails
diff --git a/LayoutTests/inspector/styles/force-pseudo-state-expected.txt b/LayoutTests/inspector/styles/force-pseudo-state-expected.txt
new file mode 100644 (file)
index 0000000..0c0c5c8
--- /dev/null
@@ -0,0 +1,110 @@
+Tests that forced element state is reflected in the DOM tree and Styles pane.
+
+Test text
+
+DIV with :hover and :active
+[expanded] 
+element.style  { ()
+
+======== Matched CSS Rules ========
+[expanded] 
+div:active, a:active  { (force-pseudo-state.html:69)
+font-weight: bold;
+
+[expanded] 
+div:hover, a:hover  { (force-pseudo-state.html:61)
+color: red;
+
+[expanded] 
+div  { (user agent stylesheet)
+display: block;
+
+======== Inherited from body#mainBody.main1.main2.mainpage ========
+[expanded] 
+Style Attribute  { ()
+/-- overloaded --/ font-weight: normal;
+
+
+- <html> [descendantUserAttributeCounters:[pseudoState=1]]
+    + <head>…</head>
+    - <body id="mainBody" class="main1 main2 mainpage" onload="runTest()" style="font-weight: normal; width: 85%; background-image: url(bar.png)"> [descendantUserAttributeCounters:[pseudoState=1]]
+          <p>Tests that forced element state is reflected in the DOM tree and Styles pane.</p>
+          <div id="div">Test text</div> [userProperties:[pseudoState=hover,active]]
+      </body>
+  </html>
+
+DIV with :active and :focus
+[expanded] 
+element.style  { ()
+
+======== Matched CSS Rules ========
+[expanded] 
+div:active, a:active  { (force-pseudo-state.html:69)
+font-weight: bold;
+
+[expanded] 
+div:focus, a:focus  { (force-pseudo-state.html:65)
+border: 1px solid green;
+    border-top-width: 1px;
+    border-right-width: 1px;
+    border-bottom-width: 1px;
+    border-left-width: 1px;
+    border-top-style: solid;
+    border-right-style: solid;
+    border-bottom-style: solid;
+    border-left-style: solid;
+    border-top-color: green;
+    border-right-color: green;
+    border-bottom-color: green;
+    border-left-color: green;
+
+[expanded] 
+:focus  { (user agent stylesheet)
+outline: -webkit-focus-ring-color auto 5px;
+    outline-style: auto;
+    outline-width: 5px;
+    outline-color: -webkit-focus-ring-color;
+
+[expanded] 
+div  { (user agent stylesheet)
+display: block;
+
+======== Inherited from body#mainBody.main1.main2.mainpage ========
+[expanded] 
+Style Attribute  { ()
+/-- overloaded --/ font-weight: normal;
+
+
+- <html> [descendantUserAttributeCounters:[pseudoState=1]]
+    + <head>…</head>
+    - <body id="mainBody" class="main1 main2 mainpage" onload="runTest()" style="font-weight: normal; width: 85%; background-image: url(bar.png)"> [descendantUserAttributeCounters:[pseudoState=1]]
+          <p>Tests that forced element state is reflected in the DOM tree and Styles pane.</p>
+          <div id="div">Test text</div> [userProperties:[pseudoState=active,focus]]
+        + <div>…</div>
+      </body>
+  </html>
+
+DIV with no forced state
+[expanded] 
+element.style  { ()
+
+======== Matched CSS Rules ========
+[expanded] 
+div  { (user agent stylesheet)
+display: block;
+
+======== Inherited from body#mainBody.main1.main2.mainpage ========
+[expanded] 
+Style Attribute  { ()
+font-weight: normal;
+
+
+- <html>
+    + <head>…</head>
+    - <body id="mainBody" class="main1 main2 mainpage" onload="runTest()" style="font-weight: normal; width: 85%; background-image: url(bar.png)">
+          <p>Tests that forced element state is reflected in the DOM tree and Styles pane.</p>
+          <div id="div">Test text</div>
+        + <div>…</div>
+      </body>
+  </html>
+
diff --git a/LayoutTests/inspector/styles/force-pseudo-state.html b/LayoutTests/inspector/styles/force-pseudo-state.html
new file mode 100644 (file)
index 0000000..a6146dd
--- /dev/null
@@ -0,0 +1,82 @@
+<html>
+<head>
+<script src="../../http/tests/inspector/inspector-test.js"></script>
+<script src="../../http/tests/inspector/elements-test.js"></script>
+<script>
+
+function test()
+{
+    WebInspector.inspectorView.setCurrentPanel(WebInspector.panels.elements);
+
+    InspectorTest.nodeWithId("div", foundDiv);
+
+    var divNodeId;
+
+    function dumpData()
+    {
+        InspectorTest.dumpSelectedElementStyles(true);
+        InspectorTest.dumpElementsTree();
+    }
+
+    function foundDiv(divNode)
+    {
+        divNodeId = divNode.id;
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "hover", true);
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "active", true);
+        InspectorTest.selectNodeAndWaitForStyles("div", divSelected1);
+    }
+
+    function divSelected1()
+    {
+        InspectorTest.addResult("");
+        InspectorTest.addResult("DIV with :hover and :active");
+        dumpData();
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "hover", false);
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "focus", true);
+        InspectorTest.waitForStyles("div", divSelected2, true);
+    }
+
+    function divSelected2()
+    {
+        InspectorTest.addResult("");
+        InspectorTest.addResult("DIV with :active and :focus");
+        dumpData();
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "focus", false);
+        WebInspector.panels.elements._setPseudoClassForNodeId(divNodeId, "active", false);
+        InspectorTest.waitForStyles("div", divSelected3, true);
+    }
+
+    function divSelected3(node)
+    {
+        InspectorTest.addResult("");
+        InspectorTest.addResult("DIV with no forced state");
+        dumpData();
+        InspectorTest.completeTest();
+        return;
+    }
+}
+</script>
+
+<style>
+div:hover, a:hover {
+    color: red;
+}
+
+div:focus, a:focus {
+    border: 1px solid green;
+}
+
+div:active, a:active {
+    font-weight: bold;
+}
+
+</style>
+</head>
+
+<body id="mainBody" class="main1 main2 mainpage" onload="runTest()" style="font-weight: normal; width: 85%; background-image: url(bar.png)">
+<p>
+Tests that forced element state is reflected in the DOM tree and Styles pane.
+</p>
+<div id="div">Test text</div>
+</body>
+</html>
index c7c6dce..32d47b0 100644 (file)
@@ -1,3 +1,66 @@
+2012-07-03  Alexander Pavlov  <apavlov@chromium.org>
+
+        Web Inspector: Emulate pseudo styles (hover etc.) of non-selected elements
+        https://bugs.webkit.org/show_bug.cgi?id=86630
+
+        Reviewed by Pavel Feldman.
+
+        - A map of pseudo-states for all bound DOM nodes is maintained in the backend and queried whenever StyleResolver
+        calculates the effective element style.
+        - In the frontend, markers are introduced to distinguish elements that have forced pseudo styles set for them.
+        Additionally, dimmed markers are added for collapsed nodes, whose descendants have forced pseudo styles.
+        More ElementDecorator subtypes will be added for other types of markers.
+
+        Test: inspector/styles/force-pseudo-state.html
+
+        * English.lproj/localizedStrings.js:
+        * inspector/InspectorCSSAgent.cpp:
+        (WebCore::InspectorCSSAgent::InspectorCSSAgent):
+        (WebCore::InspectorCSSAgent::clearFrontend):
+        (WebCore::InspectorCSSAgent::reset):
+        (WebCore::InspectorCSSAgent::forcePseudoState):
+        (WebCore::InspectorCSSAgent::recalcStyleForPseudoStateIfNeeded):
+        (WebCore::InspectorCSSAgent::elementForId):
+        (WebCore::InspectorCSSAgent::didRemoveDocument):
+        (WebCore::InspectorCSSAgent::didRemoveDOMNode):
+        (WebCore::InspectorCSSAgent::resetPseudoStates):
+        * inspector/InspectorCSSAgent.h:
+        (InspectorCSSAgent):
+        * inspector/InspectorDOMAgent.cpp:
+        (WebCore::InspectorDOMAgent::unbind):
+        (WebCore::InspectorDOMAgent::didRemoveDOMNode):
+        * inspector/front-end/ElementsPanel.js:
+        (WebInspector.ElementsPanel.get this):
+        (WebInspector.ElementsPanel):
+        (WebInspector.ElementsPanel.prototype._setPseudoClassForNodeId):
+        * inspector/front-end/ElementsTreeOutline.js:
+        (WebInspector.ElementsTreeOutline):
+        (WebInspector.ElementsTreeOutline.prototype._createNodeDecorators):
+        (WebInspector.ElementsTreeOutline.prototype.updateOpenCloseTags):
+        (WebInspector.ElementsTreeOutline.ElementDecorator):
+        (WebInspector.ElementsTreeOutline.ElementDecorator.prototype.decorate):
+        (WebInspector.ElementsTreeOutline.ElementDecorator.prototype.decorateAncestor):
+        (WebInspector.ElementsTreeOutline.PseudoStateDecorator):
+        (WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype.decorate):
+        (WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype.decorateAncestor):
+        (WebInspector.ElementsTreeElement.prototype._populateTagContextMenu):
+        (WebInspector.ElementsTreeElement.prototype._populateForcedPseudoStateItems):
+        (WebInspector.ElementsTreeElement.prototype.updateTitle):
+        (WebInspector.ElementsTreeElement.prototype._createDecoratorElement):
+        (WebInspector.ElementsTreeElement.prototype._updateDecorations):
+        * inspector/front-end/StylesSidebarPane.js:
+        (WebInspector.StylesSidebarPane):
+        (WebInspector.StylesSidebarPane.prototype.get forcedPseudoClasses):
+        (WebInspector.StylesSidebarPane.prototype._updateForcedPseudoStateInputs):
+        (WebInspector.StylesSidebarPane.prototype.update):
+        (WebInspector.StylesSidebarPane.prototype._refreshUpdate):
+        (WebInspector.StylesSidebarPane.prototype._rebuildUpdate):
+        (WebInspector.StylesSidebarPane.prototype._toggleElementStatePane):
+        (WebInspector.StylesSidebarPane.prototype._createElementStatePane.clickListener):
+        * inspector/front-end/elementsPanel.css:
+        (#elements-content .elements-gutter-decoration):
+        (#elements-content .elements-gutter-decoration.elements-has-decorated-children):
+
 2012-07-04  Pavel Feldman  <pfeldman@chromium.org>
 
         Web Inspector: fix search on the network panel.
index 27aea7b..23c0331 100644 (file)
@@ -17,6 +17,8 @@ localizedStrings["%.2fs"] = "%.2fs";
 localizedStrings["%.3fms"] = "%.3fms";
 localizedStrings["%d console messages are not shown."] = "%d console messages are not shown.";
 localizedStrings["%d cookies (%s)"] = "%d cookies (%s)";
+localizedStrings["%d descendant with forced state"] = "%d descendant with forced state";
+localizedStrings["%d descendants with forced state"] = "%d descendants with forced state";
 localizedStrings["%d error"] = "%d error";
 localizedStrings["%d error, %d warning"] = "%d error, %d warning";
 localizedStrings["%d error, %d warnings"] = "%d error, %d warnings";
@@ -181,6 +183,7 @@ localizedStrings["Edit Text"] = "Edit Text";
 localizedStrings["Edit text"] = "Edit text";
 localizedStrings["Edit as HTML"] = "Edit as HTML";
 localizedStrings["Edit"] = "Edit";
+localizedStrings["Element state: %s"] = "Element state: %s";
 localizedStrings["Elements Panel"] = "Elements Panel";
 localizedStrings["Elements"] = "Elements";
 localizedStrings["Emulate touch events"] = "Emulate touch events";
@@ -202,6 +205,8 @@ localizedStrings["Expected Content Length"] = "Expected Content Length";
 localizedStrings["Expires"] = "Expires";
 localizedStrings["File size"] = "File size";
 localizedStrings["Fit in window"] = "Fit in window";
+localizedStrings["Force Element State"] = "Force Element State";
+localizedStrings["Force element state"] = "Force element state";
 localizedStrings["Go to the panel to the left/right"] = "Go to the panel to the left/right";
 localizedStrings["Go back/forward in panel history"] = "Go back/forward in panel history";
 localizedStrings["Finish Loading"] = "Finish Loading";
index 917027f..a54d8c9 100644 (file)
@@ -461,7 +461,6 @@ InspectorCSSAgent::InspectorCSSAgent(InstrumentingAgents* instrumentingAgents, I
     : InspectorBaseAgent<InspectorCSSAgent>("CSS", instrumentingAgents, state)
     , m_frontend(0)
     , m_domAgent(domAgent)
-    , m_lastPseudoState(0)
     , m_lastStyleSheetId(1)
 {
     m_domAgent->setDOMListener(this);
@@ -485,7 +484,7 @@ void InspectorCSSAgent::clearFrontend()
 {
     ASSERT(m_frontend);
     m_frontend = 0;
-    clearPseudoState(true);
+    resetPseudoStates();
     String errorString;
     stopSelectorProfilerImpl(&errorString, false);
 }
@@ -514,6 +513,7 @@ void InspectorCSSAgent::reset()
     m_cssStyleSheetToInspectorStyleSheet.clear();
     m_nodeToInspectorStyleSheet.clear();
     m_documentToInspectorStyleSheet.clear();
+    resetPseudoStates();
 }
 
 void InspectorCSSAgent::enable(ErrorString*)
@@ -534,18 +534,27 @@ void InspectorCSSAgent::mediaQueryResultChanged()
 
 bool InspectorCSSAgent::forcePseudoState(Element* element, CSSSelector::PseudoType pseudoType)
 {
-    if (m_lastElementWithPseudoState != element)
+    if (m_nodeIdToForcedPseudoState.isEmpty())
         return false;
 
+    int nodeId = m_domAgent->boundNodeId(element);
+    if (!nodeId)
+        return false;
+
+    NodeIdToForcedPseudoState::iterator it = m_nodeIdToForcedPseudoState.find(nodeId);
+    if (it == m_nodeIdToForcedPseudoState.end())
+        return false;
+
+    unsigned forcedPseudoState = it->second;
     switch (pseudoType) {
     case CSSSelector::PseudoActive:
-        return m_lastPseudoState & PseudoActive;
+        return forcedPseudoState & PseudoActive;
     case CSSSelector::PseudoFocus:
-        return m_lastPseudoState & PseudoFocus;
+        return forcedPseudoState & PseudoFocus;
     case CSSSelector::PseudoHover:
-        return m_lastPseudoState & PseudoHover;
+        return forcedPseudoState & PseudoHover;
     case CSSSelector::PseudoVisited:
-        return m_lastPseudoState & PseudoVisited;
+        return forcedPseudoState & PseudoVisited;
     default:
         return false;
     }
@@ -553,12 +562,22 @@ bool InspectorCSSAgent::forcePseudoState(Element* element, CSSSelector::PseudoTy
 
 void InspectorCSSAgent::recalcStyleForPseudoStateIfNeeded(Element* element, InspectorArray* forcedPseudoClasses)
 {
-    unsigned forcePseudoState = computePseudoClassMask(forcedPseudoClasses);
-    bool needStyleRecalc = element != m_lastElementWithPseudoState || forcePseudoState != m_lastPseudoState;
-    m_lastPseudoState = forcePseudoState;
-    m_lastElementWithPseudoState = element;
-    if (needStyleRecalc)
-        element->ownerDocument()->styleResolverChanged(RecalcStyleImmediately);
+    int nodeId = m_domAgent->boundNodeId(element);
+    if (!nodeId)
+        return;
+
+    unsigned forcedPseudoState = computePseudoClassMask(forcedPseudoClasses);
+    NodeIdToForcedPseudoState::iterator it = m_nodeIdToForcedPseudoState.find(nodeId);
+    unsigned currentForcedPseudoState = it == m_nodeIdToForcedPseudoState.end() ? 0 : it->second;
+    bool needStyleRecalc = forcedPseudoState != currentForcedPseudoState;
+    if (!needStyleRecalc)
+        return;
+
+    if (forcedPseudoState)
+        m_nodeIdToForcedPseudoState.set(nodeId, forcedPseudoState);
+    else
+        m_nodeIdToForcedPseudoState.remove(nodeId);
+    element->ownerDocument()->styleResolverChanged(RecalcStyleImmediately);
 }
 
 void InspectorCSSAgent::getMatchedStylesForNode(ErrorString* errorString, int nodeId, const RefPtr<InspectorArray>* forcedPseudoClasses, const bool* includePseudo, const bool* includeInherited, RefPtr<TypeBuilder::Array<TypeBuilder::CSS::CSSRule> >& matchedCSSRules, RefPtr<TypeBuilder::Array<TypeBuilder::CSS::PseudoIdRules> >& pseudoIdRules, RefPtr<TypeBuilder::Array<TypeBuilder::CSS::InheritedStyleEntry> >& inheritedEntries)
@@ -844,7 +863,7 @@ Element* InspectorCSSAgent::elementForId(ErrorString* errorString, int nodeId)
         *errorString = "Not an element node";
         return 0;
     }
-    return static_cast<Element*>(node);
+    return toElement(node);
 }
 
 void InspectorCSSAgent::collectStyleSheets(CSSStyleSheet* styleSheet, TypeBuilder::Array<TypeBuilder::CSS::CSSStyleSheetHeader>* result)
@@ -985,7 +1004,6 @@ void InspectorCSSAgent::didRemoveDocument(Document* document)
 {
     if (document)
         m_documentToInspectorStyleSheet.remove(document);
-    clearPseudoState(false);
 }
 
 void InspectorCSSAgent::didRemoveDOMNode(Node* node)
@@ -993,8 +1011,9 @@ void InspectorCSSAgent::didRemoveDOMNode(Node* node)
     if (!node)
         return;
 
-    if (m_lastElementWithPseudoState.get() == node)
-        clearPseudoState(false);
+    int nodeId = m_domAgent->boundNodeId(node);
+    if (nodeId)
+        m_nodeIdToForcedPseudoState.remove(nodeId);
 
     NodeToInspectorStyleSheet::iterator it = m_nodeToInspectorStyleSheet.find(node);
     if (it == m_nodeToInspectorStyleSheet.end())
@@ -1022,16 +1041,18 @@ void InspectorCSSAgent::styleSheetChanged(InspectorStyleSheet* styleSheet)
         m_frontend->styleSheetChanged(styleSheet->id());
 }
 
-void InspectorCSSAgent::clearPseudoState(bool recalcStyles)
+void InspectorCSSAgent::resetPseudoStates()
 {
-    RefPtr<Element> element = m_lastElementWithPseudoState;
-    m_lastElementWithPseudoState = 0;
-    m_lastPseudoState = 0;
-    if (recalcStyles && element) {
-        Document* document = element->ownerDocument();
-        if (document)
-            document->styleResolverChanged(RecalcStyleImmediately);
+    HashSet<Document*> documentsToChange;
+    for (NodeIdToForcedPseudoState::iterator it = m_nodeIdToForcedPseudoState.begin(), end = m_nodeIdToForcedPseudoState.end(); it != end; ++it) {
+        Element* element = toElement(m_domAgent->nodeForId(it->first));
+        if (element && element->ownerDocument())
+            documentsToChange.add(element->ownerDocument());
     }
+
+    m_nodeIdToForcedPseudoState.clear();
+    for (HashSet<Document*>::iterator it = documentsToChange.begin(), end = documentsToChange.end(); it != end; ++it)
+        (*it)->styleResolverChanged(RecalcStyleImmediately);
 }
 
 } // namespace WebCore
index d6390f7..791a7bf 100644 (file)
@@ -134,6 +134,7 @@ private:
     typedef HashMap<CSSStyleSheet*, RefPtr<InspectorStyleSheet> > CSSStyleSheetToInspectorStyleSheet;
     typedef HashMap<Node*, RefPtr<InspectorStyleSheetForInlineStyle> > NodeToInspectorStyleSheet; // bogus "stylesheets" with elements' inline styles
     typedef HashMap<RefPtr<Document>, RefPtr<InspectorStyleSheet> > DocumentToViaInspectorStyleSheet; // "via inspector" stylesheets
+    typedef HashMap<int, unsigned> NodeIdToForcedPseudoState;
 
     void recalcStyleForPseudoStateIfNeeded(Element*, InspectorArray* forcedPseudoClasses);
     InspectorStyleSheetForInlineStyle* asInspectorStyleSheet(Element* element);
@@ -156,7 +157,7 @@ private:
     // InspectorCSSAgent::Listener implementation
     virtual void styleSheetChanged(InspectorStyleSheet*);
 
-    void clearPseudoState(bool recalcStyles);
+    void resetPseudoStates();
 
     InspectorFrontend::CSS* m_frontend;
     InspectorDOMAgent* m_domAgent;
@@ -165,9 +166,7 @@ private:
     CSSStyleSheetToInspectorStyleSheet m_cssStyleSheetToInspectorStyleSheet;
     NodeToInspectorStyleSheet m_nodeToInspectorStyleSheet;
     DocumentToViaInspectorStyleSheet m_documentToInspectorStyleSheet;
-
-    RefPtr<Element> m_lastElementWithPseudoState;
-    unsigned m_lastPseudoState;
+    NodeIdToForcedPseudoState m_nodeIdToForcedPseudoState;
 
     int m_lastStyleSheetId;
 
index 71db17a..120c646 100644 (file)
@@ -350,6 +350,9 @@ void InspectorDOMAgent::unbind(Node* node, NodeToIdMap* nodesMap)
     }
 
     nodesMap->remove(node);
+    if (m_domListener)
+        m_domListener->didRemoveDOMNode(node);
+
     bool childrenRequested = m_childrenRequested.contains(id);
     if (childrenRequested) {
         // Unbind subtree known to client recursively.
@@ -1443,9 +1446,6 @@ void InspectorDOMAgent::didRemoveDOMNode(Node* node)
 
     int parentId = m_documentNodeToIdMap.get(parent);
 
-    if (m_domListener)
-        m_domListener->didRemoveDOMNode(node);
-
     if (!m_childrenRequested.contains(parentId)) {
         // No children are mapped yet -> only notify on changes of hasChildren.
         if (innerChildNodeCount(parent) == 1)
index 3ad2534..3feaf35 100644 (file)
@@ -55,7 +55,7 @@ WebInspector.ElementsPanel = function()
 
     this.contentElement.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
 
-    this.treeOutline = new WebInspector.ElementsTreeOutline(true, true, false, this._populateContextMenu.bind(this));
+    this.treeOutline = new WebInspector.ElementsTreeOutline(true, true, false, this._populateContextMenu.bind(this), this._setPseudoClassForNodeId.bind(this));
     this.treeOutline.wireToDomAgent();
 
     this.treeOutline.addEventListener(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedNodeChanged, this);
@@ -67,7 +67,7 @@ WebInspector.ElementsPanel = function()
 
     this.sidebarPanes = {};
     this.sidebarPanes.computedStyle = new WebInspector.ComputedStyleSidebarPane();
-    this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(this.sidebarPanes.computedStyle);
+    this.sidebarPanes.styles = new WebInspector.StylesSidebarPane(this.sidebarPanes.computedStyle, this._setPseudoClassForNodeId.bind(this));
     this.sidebarPanes.metrics = new WebInspector.MetricsSidebarPane();
     this.sidebarPanes.properties = new WebInspector.PropertiesSidebarPane();
     this.sidebarPanes.domBreakpoints = WebInspector.domBreakpointsSidebarPane;
@@ -166,6 +166,37 @@ WebInspector.ElementsPanel.prototype = {
         this.updateBreadcrumbSizes();
     },
 
+    /**
+     * @param {DOMAgent.NodeId} nodeId
+     * @param {string} pseudoClass
+     * @param {boolean} enable
+     */
+    _setPseudoClassForNodeId: function(nodeId, pseudoClass, enable)
+    {
+        var node = WebInspector.domAgent.nodeForId(nodeId);
+        if (!node)
+            return;
+
+        var pseudoClasses = node.getUserProperty("pseudoState");
+        if (enable) {
+            pseudoClasses = pseudoClasses || [];
+            if (pseudoClasses.indexOf(pseudoClass) >= 0)
+                return;
+            pseudoClasses.push(pseudoClass);
+            node.setUserProperty("pseudoState", pseudoClasses);
+        } else {
+            if (!pseudoClasses || pseudoClasses.indexOf(pseudoClass) < 0)
+                return;
+            pseudoClasses.remove(pseudoClass);
+            if (!pseudoClasses.length)
+                node.removeUserProperty("pseudoState");
+        }
+
+        this.treeOutline.updateOpenCloseTags(node);
+        this._metricsPaneEdited();
+        this._stylesPaneEdited();
+    },
+
     _selectedNodeChanged: function()
     {
         var selectedNode = this.selectedDOMNode();
index f79495a..a92b011 100644 (file)
@@ -35,8 +35,9 @@
  * @param {boolean=} selectEnabled
  * @param {boolean=} showInElementsPanelEnabled
  * @param {function(WebInspector.ContextMenu, WebInspector.DOMNode)=} contextMenuCallback
+ * @param {function(DOMAgent.NodeId, string, boolean)=} setPseudoClassCallback
  */
-WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled, contextMenuCallback)
+WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, showInElementsPanelEnabled, contextMenuCallback, setPseudoClassCallback)
 {
     this.element = document.createElement("ol");
     this.element.addEventListener("mousedown", this._onmousedown.bind(this), false);
@@ -62,6 +63,8 @@ WebInspector.ElementsTreeOutline = function(omitRootDOMNode, selectEnabled, show
 
     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
     this._contextMenuCallback = contextMenuCallback;
+    this._setPseudoClassCallback = setPseudoClassCallback;
+    this._createNodeDecorators();
 }
 
 WebInspector.ElementsTreeOutline.Events = {
@@ -69,6 +72,12 @@ WebInspector.ElementsTreeOutline.Events = {
 }
 
 WebInspector.ElementsTreeOutline.prototype = {
+    _createNodeDecorators: function()
+    {
+        this._nodeDecorators = [];
+        this._nodeDecorators.push(new WebInspector.ElementsTreeOutline.PseudoStateDecorator());
+    },
+
     wireToDomAgent: function()
     {
         this._elementsTreeUpdater = new WebInspector.ElementsTreeUpdater(this);
@@ -182,11 +191,28 @@ WebInspector.ElementsTreeOutline.prototype = {
         element.updateSelection();
     },
 
+    /**
+     * @param {WebInspector.DOMNode} node
+     */
+    updateOpenCloseTags: function(node)
+    {
+        var treeElement = this.findTreeElement(node);
+        if (treeElement)
+            treeElement.updateTitle();
+        var children = treeElement.children;
+        var closingTagElement = children[children.length - 1];
+        if (closingTagElement && closingTagElement._elementCloseTag)
+            closingTagElement.updateTitle();
+    },
+
     _selectedNodeChanged: function()
     {
         this._eventSupport.dispatchEventToListeners(WebInspector.ElementsTreeOutline.Events.SelectedNodeChanged, this._selectedDOMNode);
     },
 
+    /**
+     * @param {WebInspector.DOMNode} node
+     */
     findTreeElement: function(node)
     {
         function isAncestorNode(ancestor, node)
@@ -208,6 +234,9 @@ WebInspector.ElementsTreeOutline.prototype = {
         return treeElement;
     },
 
+    /**
+     * @param {WebInspector.DOMNode} node
+     */
     createTreeElementFor: function(node)
     {
         var treeElement = this.findTreeElement(node);
@@ -554,6 +583,67 @@ WebInspector.ElementsTreeOutline.prototype = {
 WebInspector.ElementsTreeOutline.prototype.__proto__ = TreeOutline.prototype;
 
 /**
+ * @interface
+ */
+WebInspector.ElementsTreeOutline.ElementDecorator = function()
+{
+}
+
+WebInspector.ElementsTreeOutline.ElementDecorator.prototype = {
+    /**
+     * @param {WebInspector.DOMNode} node
+     */
+    decorate: function(node)
+    {
+    },
+
+    /**
+     * @param {WebInspector.DOMNode} node
+     */
+    decorateAncestor: function(node)
+    {
+    }
+}
+
+/**
+ * @constructor
+ * @implements {WebInspector.ElementsTreeOutline.ElementDecorator}
+ */
+WebInspector.ElementsTreeOutline.PseudoStateDecorator = function()
+{
+    WebInspector.ElementsTreeOutline.ElementDecorator.call(this);
+}
+
+WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName = "pseudoState";
+
+WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype = {
+    decorate: function(node)
+    {
+        if (node.nodeType() !== Node.ELEMENT_NODE)
+            return null;
+        var propertyValue = node.getUserProperty(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
+        if (!propertyValue)
+            return null;
+        return WebInspector.UIString("Element state: %s", ":" + propertyValue.join(", :"));
+    },
+
+    decorateAncestor: function(node)
+    {
+        if (node.nodeType() !== Node.ELEMENT_NODE)
+            return null;
+
+        var descendantCount = node.descendantUserPropertyCount(WebInspector.ElementsTreeOutline.PseudoStateDecorator.PropertyName);
+        if (!descendantCount)
+            return null;
+        if (descendantCount === 1)
+            return WebInspector.UIString("%d descendant with forced state", descendantCount);
+        return WebInspector.UIString("%d descendants with forced state", descendantCount);
+    }
+}
+
+WebInspector.ElementsTreeOutline.PseudoStateDecorator.prototype.__proto__ = WebInspector.ElementsTreeOutline.ElementDecorator.prototype;
+
+/**
  * @constructor
  * @extends {TreeElement}
  * @param {boolean=} elementCloseTag
@@ -1064,11 +1154,28 @@ WebInspector.ElementsTreeElement.prototype = {
         if (attribute && !newAttribute)
             contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit attribute" : "Edit Attribute"), this._startEditingAttribute.bind(this, attribute, event.target));
         contextMenu.appendSeparator();
+        if (this.treeOutline._setPseudoClassCallback) {
+            var pseudoSubMenu = contextMenu.appendSubMenuItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Force element state" : "Force Element State"));
+            this._populateForcedPseudoStateItems(pseudoSubMenu);
+            contextMenu.appendSeparator();
+        }
 
         this._populateNodeContextMenu(contextMenu);
         this.treeOutline._populateContextMenu(contextMenu, this.representedObject);
     },
 
+    _populateForcedPseudoStateItems: function(subMenu)
+    {
+        const pseudoClasses = ["active", "hover", "focus", "visited"];
+        var node = this.representedObject;
+        var forcedPseudoState = (node ? node.getUserProperty("pseudoState") : null) || [];
+        var elementsPanel = WebInspector.panels.elements;
+        for (var i = 0; i < pseudoClasses.length; ++i) {
+            var pseudoClassForced = forcedPseudoState.indexOf(pseudoClasses[i]) >= 0;
+            subMenu.appendCheckboxItem(":" + pseudoClasses[i], this.treeOutline._setPseudoClassCallback.bind(null, node.id, pseudoClasses[i], !pseudoClassForced), pseudoClassForced, false);
+        }
+    },
+
     _populateTextContextMenu: function(contextMenu, textNode)
     {
         contextMenu.appendItem(WebInspector.UIString(WebInspector.useLowerCaseMenuTitles() ? "Edit text" : "Edit Text"), this._startEditingTextNode.bind(this, textNode));
@@ -1469,6 +1576,7 @@ WebInspector.ElementsTreeElement.prototype = {
             highlightElement.className = "highlight";
             highlightElement.appendChild(this._nodeTitleInfo(WebInspector.linkifyURLAsNode).titleDOM);
             this.title = highlightElement;
+            this._updateDecorations();
             delete this._highlightResult;
         }
 
@@ -1479,6 +1587,46 @@ WebInspector.ElementsTreeElement.prototype = {
         this._highlightSearchResults();
     },
 
+    _createDecoratorElement: function()
+    {
+        var node = this.representedObject;
+        var decoratorMessages = [];
+        var parentDecoratorMessages = [];
+        for (var i = 0; i < this.treeOutline._nodeDecorators.length; ++i) {
+            var decorator = this.treeOutline._nodeDecorators[i];
+            var message = decorator.decorate(node);
+            if (message) {
+                decoratorMessages.push(message);
+                continue;
+            }
+
+            if (this.expanded || this._elementCloseTag)
+                continue;
+
+            message = decorator.decorateAncestor(node);
+            if (message)
+                parentDecoratorMessages.push(message)
+        }
+        if (!decoratorMessages.length && !parentDecoratorMessages.length)
+            return null;
+
+        var decoratorElement = document.createElement("div");
+        decoratorElement.addStyleClass("elements-gutter-decoration");
+        if (!decoratorMessages.length)
+            decoratorElement.addStyleClass("elements-has-decorated-children");
+        decoratorElement.title = decoratorMessages.concat(parentDecoratorMessages).join("\n");
+        return decoratorElement;
+    },
+
+    _updateDecorations: function()
+    {
+        if (this._decoratorElement && this._decoratorElement.parentElement)
+            this._decoratorElement.parentElement.removeChild(this._decoratorElement);
+        this._decoratorElement = this._createDecoratorElement();
+        if (this._decoratorElement && this.listItemElement)
+            this.listItemElement.insertBefore(this._decoratorElement, this.listItemElement.firstChild);
+    },
+
     /**
      * @param {WebInspector.DOMNode=} node
      * @param {function(string, string, string, boolean=, string=)=} linkify
index ef091cd..a05182e 100644 (file)
 /**
  * @constructor
  * @extends {WebInspector.SidebarPane}
+ * @param {WebInspector.ComputedStyleSidebarPane} computedStylePane
+ * @param {function(DOMAgent.NodeId, string, boolean)} setPseudoClassCallback
  */
-WebInspector.StylesSidebarPane = function(computedStylePane)
+WebInspector.StylesSidebarPane = function(computedStylePane, setPseudoClassCallback)
 {
     WebInspector.SidebarPane.call(this, WebInspector.UIString("Styles"));
 
@@ -82,6 +84,7 @@ WebInspector.StylesSidebarPane = function(computedStylePane)
 
     this._computedStylePane = computedStylePane;
     computedStylePane._stylesSidebarPane = this;
+    this._setPseudoClassCallback = setPseudoClassCallback;
     this.element.addEventListener("contextmenu", this._contextMenuEventFired.bind(this), true);
     WebInspector.settings.colorFormat.addChangeListener(this._colorFormatSettingChanged.bind(this));
 
@@ -211,7 +214,21 @@ WebInspector.StylesSidebarPane.prototype = {
 
     get forcedPseudoClasses()
     {
-        return this._forcedPseudoClasses;
+        return this.node ? (this.node.getUserProperty("pseudoState") || undefined) : undefined;
+    },
+
+    _updateForcedPseudoStateInputs: function()
+    {
+        if (!this.node)
+            return;
+
+        var nodePseudoState = this.forcedPseudoClasses;
+        if (!nodePseudoState)
+            nodePseudoState = [];
+
+        var inputs = this._elementStatePane.inputs;
+        for (var i = 0; i < inputs.length; ++i)
+            inputs[i].checked = nodePseudoState.indexOf(inputs[i].state) >= 0;
     },
 
     update: function(node, forceUpdate)
@@ -238,6 +255,8 @@ WebInspector.StylesSidebarPane.prototype = {
         else
             node = this.node;
 
+        this._updateForcedPseudoStateInputs();
+
         if (refresh)
             this._refreshUpdate();
         else
@@ -279,7 +298,7 @@ WebInspector.StylesSidebarPane.prototype = {
 
         if (this._computedStylePane.expanded || forceFetchComputedStyle) {
             this._refreshUpdateInProgress = true;
-            WebInspector.cssModel.getComputedStyleAsync(node.id, this._forcedPseudoClasses, computedStyleCallback.bind(this));
+            WebInspector.cssModel.getComputedStyleAsync(node.id, this.forcedPseudoClasses, computedStyleCallback.bind(this));
         } else {
             this._innerRefreshUpdate(node, null, editedSection);
             if (userCallback)
@@ -337,11 +356,14 @@ WebInspector.StylesSidebarPane.prototype = {
         }
 
         if (this._computedStylePane.expanded)
-            WebInspector.cssModel.getComputedStyleAsync(node.id, this._forcedPseudoClasses, computedCallback.bind(this));
+            WebInspector.cssModel.getComputedStyleAsync(node.id, this.forcedPseudoClasses, computedCallback.bind(this));
         WebInspector.cssModel.getInlineStylesAsync(node.id, inlineCallback.bind(this));
-        WebInspector.cssModel.getMatchedStylesAsync(node.id, this._forcedPseudoClasses, true, true, stylesCallback.bind(this));
+        WebInspector.cssModel.getMatchedStylesAsync(node.id, this.forcedPseudoClasses, true, true, stylesCallback.bind(this));
     },
 
+    /**
+     * @param {function()=} userCallback
+     */
     _validateNode: function(userCallback)
     {
         if (!this.node) {
@@ -813,13 +835,6 @@ WebInspector.StylesSidebarPane.prototype = {
         } else {
             this._elementStateButton.removeStyleClass("toggled");
             this._elementStatePane.removeStyleClass("expanded");
-            // Clear flags on hide.
-            if (this._forcedPseudoClasses) {
-                for (var i = 0; i < this._elementStatePane.inputs.length; ++i)
-                    this._elementStatePane.inputs[i].checked = false;
-                delete this._forcedPseudoClasses;
-                this._rebuildUpdate();
-            }
         }
     },
 
@@ -834,13 +849,10 @@ WebInspector.StylesSidebarPane.prototype = {
 
         function clickListener(event)
         {
-            var pseudoClasses = [];
-            for (var i = 0; i < inputs.length; ++i) {
-                if (inputs[i].checked)
-                    pseudoClasses.push(inputs[i].state);
-            }
-            this._forcedPseudoClasses = pseudoClasses.length ? pseudoClasses : undefined;
-            this._rebuildUpdate();
+            var node = this._validateNode();
+            if (!node)
+                return;
+            this._setPseudoClassCallback(node.id, event.target.state, event.target.checked);
         }
 
         function createCheckbox(state)
index 6b7455b..8c9bc3c 100644 (file)
     margin-left: 8px;
 }
 
+#elements-content .elements-gutter-decoration {
+    position: absolute;
+    left: 1px;
+    margin-top: 2px;
+    height: 8px;
+    width: 8px;
+    border-radius: 4px;
+    border: 1px solid orange;
+    background-color: orange;
+}
+
+#elements-content .elements-gutter-decoration.elements-has-decorated-children {
+    opacity: 0.5;
+}
+
 .elements-tree-editor {
     -webkit-user-select: text;
     -webkit-user-modify: read-write-plaintext-only;