Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8345188: Support tree-structural pseudo-classes #1652

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
.csspropertytable th, .csspropertytable td {
padding: 2px;
}
.csspropertytable thead th, .csspropertytable thead td {
.csspropertytable thead th, .csspropertytable thead td, .csspropertytable tbody th.subheader {
font-size: 10px;
text-align: center;
background-color: #CCC;
Expand Down Expand Up @@ -539,8 +539,7 @@ <h3><a id="introscenegraph">CSS and the JavaFX Scene Graph</a></h3>
<p>
JavaFX CSS also supports pseudo&#8209;classes, but does not implement the full range of pseudo&#8209;classes as
specified in <a href="http://www.w3.org/TR/css3-selectors/#pseudo-classes">Pseudo&#8209;classes</a>. The pseudo&#8209;classes
supported by each Node type are given in the tables within this reference. Note that JavaFX does not currently
support structural pseudo&#8209;classes.
supported by each Node type are given in the tables within this reference.
</p>
<p>
Each node honors a set of properties that depends on the node's JavaFX
Expand Down Expand Up @@ -869,7 +868,6 @@ <h3><a id="introlimitations">Limitations</a></h3>
syntax not specified in this document.</li>
<li>With the exception of @font&#8209;face and @import, @-keyword statements are ignored.</li>
<li>The &lt;media-query-list&gt; of the @import statement is not parsed.</li>
<li>The structural pseudo&#8209;classes are not supported. </li>
<li>The ":active" and ":focus" dynamic pseudo&#8209;classes are not supported.
However, <a href="#node">Nodes</a> do support the ":pressed" and
":focused" pseudo&#8209;classes, which are similar. </li>
Expand Down Expand Up @@ -1529,7 +1527,7 @@ <h4>Looked-up Colors <span class="grammar" style="font-size: smaller;">&lt;looke
with the "style" property on a node.</p>
<p>In the following example, all background color of all buttons uses the
looked up color "abc".</p>
<p class="example">.root { abc: #f00 }<br>
<p class="example">:root { abc: #f00 }<br>
.button { -fx-background-color: abc }</p>
<h4>RGB Colors <span class="grammar" style="font-size: smaller;">&lt;rgb-color&gt;</span></h4>
<p>The RGB color model is used in numerical color specifications. It has a
Expand Down Expand Up @@ -1825,11 +1823,11 @@ <h2><a id="stage">Stage</a></h2>
</tbody>
</table>
<h4><a id="popupwindow">Group</a></h4>
<p class="styleclass">Style class: .root.popup</p>
<p class="styleclass">Style class: :root.popup</p>
<p>PopupWindow does not have any properties that can be styled by CSS, but a PopupWindow does have its own Scene.
Scene's root gets the .root style-class by default. If the Scene is the root scene of a PopupWindow, then the
Scene's root gets the :root pseudo-class by default. If the Scene is the root scene of a PopupWindow, then the
.popup style-class is also added. This allows the root scene of a PopupWindow to have distinct styles via
the CSS rule <code>.root.popup { /* declarations */ }</code>
the CSS rule <code>:root.popup { /* declarations */ }</code>
<h2><a id="nodes">Nodes</a></h2>
<table class="package" width="100%">
<tbody>
Expand Down Expand Up @@ -2002,41 +2000,63 @@ <h4><a id="node">Node</a></h4>
<h4>Pseudo-classes</h4>
<table class="csspropertytable">
<caption>Available CSS Pseudo-classes</caption>
<thead>
<tr>
<th class="propertyname" scope="col">CSS Pseudo-class</th>
<th scope="col">Comments</th>
</tr>
</thead>
<tbody>
<tr>
<th class="propertyname" scope="row">disabled</th>
<td>applies when the <strong>disabled</strong> variable is true</td>
<th class="propertyname subheader" scope="col">User Action Pseudo-classes</th>
<th class="subheader" scope="col">Comments</th>
</tr>
<tr>
<th class="propertyname" scope="row">focused</th>
<th class="propertyname" scope="row">focused</th>
<td>applies when the <strong>focused</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">focus-visible</th>
<th class="propertyname" scope="row">focus-visible</th>
<td>applies when the <strong>focusVisible</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">focus-within</th>
<th class="propertyname" scope="row">focus-within</th>
<td>applies when the <strong>focusWithin</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">hover</th>
<th class="propertyname" scope="row">hover</th>
<td>applies when the <strong>hover</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">pressed</th>
<th class="propertyname" scope="row">pressed</th>
<td>applies when the <strong>pressed</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">show-mnemonic</th>
<td>apples when the mnemonic affordance (typically an underscore)
should be shown.</td>
<th class="propertyname subheader" scope="col">Input Pseudo-classes</th>
<th class="subheader" scope="col">Comments</th>
</tr>
<tr>
<th class="propertyname" scope="row">disabled</th>
<td>applies when the <strong>disabled</strong> variable is true</td>
</tr>
<tr>
<th class="propertyname" scope="row">show-mnemonic</th>
<td>applies when the mnemonic affordance (typically an underscore) should be shown</td>
</tr>
<tr>
<th class="propertyname subheader" scope="col">Tree-Structural Pseudo-classes</th>
<th class="subheader" scope="col">Comments</th>
</tr>
<tr>
<th class="propertyname" scope="row">first-child</th>
<td>applies when the node is the first child in its <code>Parent</code> container</td>
</tr>
<tr>
<th class="propertyname" scope="row">last-child</th>
<td>applies when the node is the last child in its <code>Parent</code> container</td>
</tr>
<tr>
<th class="propertyname" scope="row">only-child</th>
<td>applies when the node is the only child in its <code>Parent</code> container</td>
</tr>
<tr>
<th class="propertyname" scope="row">nth-child()</th>
<td>applies when the node is the n-th child in its <code>Parent</code> container
(the only acceptable arguments are "even" and "odd")</td>
</tr>
</tbody>
</table>
Expand All @@ -2062,6 +2082,25 @@ <h4><a id="parent">Parent</a></h4>
</tr>
</tbody>
</table>
<h4>Pseudo-classes</h4>
<table class="csspropertytable">
<caption>Available CSS Pseudo-classes</caption>
<thead>
<tr>
<th class="propertyname" scope="col">CSS Pseudo-class</th>
<th scope="col">Comments</th>
</tr>
</thead>
<tbody>
<tr>
<th class="propertyname" scope="row">root</th>
<td>applies when the <code>Parent</code> is the root node of a <code>Scene</code> or <code>SubScene</code></td>
</tr>
<tr>
<td colspan="2" class="parents" scope="row">Also has all pseudo&#8209;classes of <a href="#node">Node</a></td>
</tr>
</tbody>
</table>
<br>
<h4><a id="scene">Scene</a></h4>
<p class="styleclass">Style class: not applicable<br>
Expand Down Expand Up @@ -4294,7 +4333,7 @@ <h4><a id="passwordfield">PasswordField</a></h4>
<p>The PasswordField control has all the properties of <a href="#textfield">TextField</a></p>
<h4><a id="popupcontrol">PopupControl</a></h4>
<p>PopupControl is also a <a href="#popupwindow">PopupWindow</a> and as such, its root node has the
style-class .root.popup</p>
style-class :root.popup</p>
<h4><a id="progressbar">ProgressBar</a></h4>
<p class="styleclass">Style class: progress-bar</p>
<table class="csspropertytable">
Expand Down
65 changes: 61 additions & 4 deletions modules/javafx.graphics/src/main/java/javafx/scene/Parent.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.sun.javafx.util.Utils;
import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.collections.VetoableListDecorator;
import javafx.css.PseudoClass;
import javafx.css.Selector;
import com.sun.javafx.css.StyleManager;
import com.sun.javafx.geom.BaseBounds;
Expand Down Expand Up @@ -316,14 +317,33 @@ private List<Node> getOrderedChildren() {
private boolean geomChanged;
private boolean childSetModified;
private final ObservableList<Node> children = new VetoableListDecorator<Node>(new TrackableObservableList<Node>() {
private static final PseudoClass FIRST_CHILD_PSEUDO_CLASS = PseudoClass.getPseudoClass("first-child");
private static final PseudoClass LAST_CHILD_PSEUDO_CLASS = PseudoClass.getPseudoClass("last-child");
private static final PseudoClass ONLY_CHILD_PSEUDO_CLASS = PseudoClass.getPseudoClass("only-child");
private static final PseudoClass NTH_EVEN_CHILD_PSEUDO_CLASS = PseudoClass.getPseudoClass("nth-child(even)");
private static final PseudoClass NTH_ODD_CHILD_PSEUDO_CLASS = PseudoClass.getPseudoClass("nth-child(odd)");

private static final List<PseudoClass> ONLY_CHILD_CLASSES = List.of(ONLY_CHILD_PSEUDO_CLASS,
FIRST_CHILD_PSEUDO_CLASS,
LAST_CHILD_PSEUDO_CLASS);

private static final List<PseudoClass> FIRST_CHILD_CLASSES = List.of(FIRST_CHILD_PSEUDO_CLASS);

private static void toggleStructuralPseudoClasses(Node node, List<PseudoClass> active) {
node.pseudoClassStateChanged(FIRST_CHILD_PSEUDO_CLASS, active.contains(FIRST_CHILD_PSEUDO_CLASS));
node.pseudoClassStateChanged(LAST_CHILD_PSEUDO_CLASS, active.contains(LAST_CHILD_PSEUDO_CLASS));
node.pseudoClassStateChanged(ONLY_CHILD_PSEUDO_CLASS, active.contains(ONLY_CHILD_PSEUDO_CLASS));
node.pseudoClassStateChanged(NTH_EVEN_CHILD_PSEUDO_CLASS, active.contains(NTH_EVEN_CHILD_PSEUDO_CLASS));
node.pseudoClassStateChanged(NTH_ODD_CHILD_PSEUDO_CLASS, active.contains(NTH_ODD_CHILD_PSEUDO_CLASS));
}

@Override
protected void onChanged(Change<Node> c) {
// proceed with updating the scene graph
unmodifiableManagedChildren = null;
boolean relayout = false;
boolean viewOrderChildrenDirty = false;
int firstDirtyChildIndex = -1;

if (childSetModified) {
while (c.next()) {
Expand Down Expand Up @@ -351,6 +371,14 @@ protected void onChanged(Change<Node> c) {
if (n.isManaged()) {
relayout = true;
}

toggleStructuralPseudoClasses(n, List.of());
}

// Sub-changes are sorted by their 'from' index, so it is sufficient to record
// the index of the first change to separate unchanged from changed elements.
if (firstDirtyChildIndex < 0) {
firstDirtyChildIndex = from;
}

// Mark viewOrderChildrenDirty if there is modification to children list
Expand Down Expand Up @@ -402,6 +430,12 @@ protected void onChanged(Change<Node> c) {
// If childSet was not modified, we still need to check whether the permutation
// did change the layout
layout_loop:while (c.next()) {
// Sub-changes are sorted by their 'from' index, so it is sufficient to record
// the index of the first change to separate unchanged from changed elements.
if (firstDirtyChildIndex < 0) {
firstDirtyChildIndex = c.getFrom();
}

List<Node> removed = c.getRemoved();
for (int i = 0, removedSize = removed.size(); i < removedSize; ++i) {
if (removed.get(i).isManaged()) {
Expand All @@ -419,6 +453,31 @@ protected void onChanged(Change<Node> c) {
}
}

// Toggle the "only-child" / "first-child" pseudo-classes on the first child.
if (size() == 1) {
toggleStructuralPseudoClasses(getFirst(), ONLY_CHILD_CLASSES);
} else if (size() > 1 && firstDirtyChildIndex == 0) {
toggleStructuralPseudoClasses(getFirst(), FIRST_CHILD_CLASSES);
}

// Clear the "last-child" pseudo-class if it was set on the last non-modified child.
if (firstDirtyChildIndex > 0) {
get(firstDirtyChildIndex - 1).pseudoClassStateChanged(LAST_CHILD_PSEUDO_CLASS, false);
}

// Add the "last-child" pseudo-class to the last child.
if (size() > 0) {
getLast().pseudoClassStateChanged(LAST_CHILD_PSEUDO_CLASS, true);
}

// Toggle the "nth-child(even)" and "nth-child(odd)" pseudo-classes on all modified children.
if (firstDirtyChildIndex >= 0) {
for (int i = firstDirtyChildIndex, max = size(); i < max; ++i) {
Node n = get(i);
n.pseudoClassStateChanged(NTH_EVEN_CHILD_PSEUDO_CLASS, i % 2 != 0);
n.pseudoClassStateChanged(NTH_ODD_CHILD_PSEUDO_CLASS, i % 2 == 0);
}
}

//
// Note that the styles of a child do not affect the parent or
Expand Down Expand Up @@ -449,10 +508,8 @@ protected void onChanged(Change<Node> c) {

// Note the starting index at which we need to update the
// PGGroup on the next update, and mark the children dirty
c.reset();
c.next();
if (startIdx > c.getFrom()) {
startIdx = c.getFrom();
if (startIdx > firstDirtyChildIndex) {
startIdx = firstDirtyChildIndex;
}

NodeHelper.markDirty(Parent.this, DirtyBits.PARENT_CHILDREN);
Expand Down
8 changes: 6 additions & 2 deletions modules/javafx.graphics/src/main/java/javafx/scene/Scene.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.StyleableObjectProperty;
import javafx.css.Stylesheet;
import javafx.event.*;
Expand Down Expand Up @@ -1183,7 +1184,9 @@ public String getName() {
* layout of the scene graph. If a resizable node (layout {@code Region} or
* {@code Control}) is set as the root, then the root's size will track the
* scene's size, causing the contents to be relayed out as necessary.
*
* <p>
* The {@code :root} pseudo-class matches the root node.
* <p>
* Scene doesn't accept null root.
*
*/
Expand All @@ -1201,7 +1204,6 @@ public final Parent getRoot() {
public final ObjectProperty<Parent> rootProperty() {
if (root == null) {
root = new ObjectPropertyBase<>() {

private void forceUnbind() {
System.err.println("Unbinding illegal root.");
unbind();
Expand Down Expand Up @@ -1235,9 +1237,11 @@ protected void invalidated() {
if (oldRoot != null) {
oldRoot.setScenes(null, null);
oldRoot.getStyleClass().remove("root");
oldRoot.pseudoClassStateChanged(PseudoClass.getPseudoClass("root"), false);
}
oldRoot = _value;
_value.getStyleClass().add(0, "root");
_value.pseudoClassStateChanged(PseudoClass.getPseudoClass("root"), true);
_value.setScenes(Scene.this, null);
markDirty(DirtyBits.ROOT_DIRTY);
_value.resize(getWidth(), getHeight()); // maybe no-op if root is not resizable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import javafx.application.Platform;
import javafx.beans.NamedArg;
import javafx.beans.property.*;
import javafx.css.PseudoClass;
import javafx.css.Stylesheet;
import javafx.geometry.NodeOrientation;
import javafx.geometry.Point3D;
Expand Down Expand Up @@ -256,7 +257,9 @@ private boolean isDepthBufferInternal() {
* Defines the root {@code Node} of the {@code SubScene} scene graph.
* If a {@code Group} is used as the root, the
* contents of the scene graph will be clipped by the {@code SubScene}'s width and height.
*
* <p>
* The {@code :root} pseudo-class matches the root node.
* <p>
* {@code SubScene} doesn't accept null root.
*
*/
Expand Down Expand Up @@ -317,9 +320,11 @@ protected void invalidated() {
StyleManager.getInstance().forget(SubScene.this);
oldRoot.setScenes(null, null);
oldRoot.getStyleClass().remove("root");
oldRoot.pseudoClassStateChanged(PseudoClass.getPseudoClass("root"), false);
}
oldRoot = _value;
_value.getStyleClass().add(0, "root");
_value.pseudoClassStateChanged(PseudoClass.getPseudoClass("root"), true);
_value.setScenes(getScene(), SubScene.this);
markDirty(SubSceneDirtyBits.ROOT_SG_DIRTY);
_value.resize(getWidth(), getHeight()); // maybe no-op if root is not resizable
Expand Down
Loading