javafx.scene.Parent Maven / Gradle / Ivy
/*
* Copyright (c) 2010, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene;
import com.sun.javafx.scene.traversal.ParentTraversalEngine;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.sun.javafx.util.TempState;
import com.sun.javafx.util.Utils;
import com.sun.javafx.collections.TrackableObservableList;
import com.sun.javafx.collections.VetoableListDecorator;
import javafx.css.Selector;
import com.sun.javafx.css.StyleManager;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.PickRay;
import com.sun.javafx.geom.Point2D;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.javafx.geom.transform.NoninvertibleTransformException;
import com.sun.javafx.scene.CssFlags;
import com.sun.javafx.scene.DirtyBits;
import com.sun.javafx.scene.input.PickResultChooser;
import com.sun.javafx.sg.prism.NGGroup;
import com.sun.javafx.sg.prism.NGNode;
import com.sun.javafx.tk.Toolkit;
import com.sun.javafx.scene.LayoutFlags;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.ParentHelper;
import com.sun.javafx.stage.WindowHelper;
import java.util.Collections;
import javafx.stage.Window;
/**
* The base class for all nodes that have children in the scene graph.
*
* This class handles all hierarchical scene graph operations, including adding/removing
* child nodes, marking branches dirty for layout and rendering, picking,
* bounds calculations, and executing the layout pass on each pulse.
*
* There are two direct concrete Parent subclasses
*
* - {@link Group} effects and transforms to be applied to a collection of child nodes.
* - {@link javafx.scene.layout.Region} class for nodes that can be styled with CSS and layout children.
*
*
* @since JavaFX 2.0
*/
public abstract class Parent extends Node {
// package private for testing
static final int DIRTY_CHILDREN_THRESHOLD = 10;
// If set to true, generate a warning message whenever adding a node to a
// parent if it is currently a child of another parent.
private static final boolean warnOnAutoMove = PropertyHelper.getBooleanProperty("javafx.sg.warn");
/**
* Threshold when it's worth to populate list of removed children.
*/
private static final int REMOVED_CHILDREN_THRESHOLD = 20;
/**
* Do not populate list of removed children when its number exceeds threshold,
* but mark whole parent dirty.
*/
private boolean removedChildrenOptimizationDisabled = false;
static {
// This is used by classes in different packages to get access to
// private and package private methods.
ParentHelper.setParentAccessor(new ParentHelper.ParentAccessor() {
@Override
public NGNode doCreatePeer(Node node) {
return ((Parent) node).doCreatePeer();
}
@Override
public void doUpdatePeer(Node node) {
((Parent) node).doUpdatePeer();
}
@Override
public BaseBounds doComputeGeomBounds(Node node,
BaseBounds bounds, BaseTransform tx) {
return ((Parent) node).doComputeGeomBounds(bounds, tx);
}
@Override
public boolean doComputeContains(Node node, double localX, double localY) {
return ((Parent) node).doComputeContains(localX, localY);
}
@Override
public void doProcessCSS(Node node) {
((Parent) node).doProcessCSS();
}
@Override
public void doPickNodeLocal(Node node, PickRay localPickRay,
PickResultChooser result) {
((Parent) node).doPickNodeLocal(localPickRay, result);
}
@Override
public boolean pickChildrenNode(Parent parent, PickRay pickRay, PickResultChooser result) {
return parent.pickChildrenNode(pickRay, result);
}
@Override
public void setTraversalEngine(Parent parent, ParentTraversalEngine value) {
parent.setTraversalEngine(value);
}
@Override
public ParentTraversalEngine getTraversalEngine(Parent parent) {
return parent.getTraversalEngine();
}
@Override
public List doGetAllParentStylesheets(Parent parent) {
return parent.doGetAllParentStylesheets();
}
});
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doUpdatePeer() {
final NGGroup peer = getPeer();
if (Utils.assertionEnabled()) {
List pgnodes = peer.getChildren();
if (pgnodes.size() != pgChildrenSize) {
java.lang.System.err.println("*** pgnodes.size() [" + pgnodes.size() + "] != pgChildrenSize [" + pgChildrenSize + "]");
}
}
if (isDirty(DirtyBits.PARENT_CHILDREN)) {
// Whether a permutation, or children having been added or
// removed, we'll want to clear out the PG side starting
// from startIdx. We know that everything up to but not
// including startIdx is identical between the FX and PG
// sides, so we only need to update the remaining portion.
peer.clearFrom(startIdx);
for (int idx = startIdx; idx < children.size(); idx++) {
peer.add(idx, children.get(idx).getPeer());
}
if (removedChildrenOptimizationDisabled) {
peer.markDirty();
removedChildrenOptimizationDisabled = false;
} else {
if (removed != null && !removed.isEmpty()) {
for(int i = 0; i < removed.size(); i++) {
peer.addToRemoved(removed.get(i).getPeer());
}
}
}
if (removed != null) {
removed.clear();
}
pgChildrenSize = children.size();
startIdx = pgChildrenSize;
}
if (isDirty(DirtyBits.PARENT_CHILDREN_VIEW_ORDER)) {
computeViewOrderChildren();
peer.setViewOrderChildren(viewOrderChildren);
}
if (Utils.assertionEnabled()) validatePG();
}
/* *********************************************************************
* Scenegraph Structure *
* *
* Functions and variables related to the scenegraph structure, *
* modifying the structure, and walking the structure. *
* *
**********************************************************************/
// Used to check for duplicate nodes
private final Set childSet = new HashSet<>();
// starting child index from which we need to send the children to the PGGroup
private int startIdx = 0;
// double of children in the PGGroup as of the last update
private int pgChildrenSize = 0;
void validatePG() {
boolean assertionFailed = false;
final NGGroup peer = getPeer();
List pgnodes = peer.getChildren();
if (pgnodes.size() != children.size()) {
java.lang.System.err.println("*** pgnodes.size validatePG() [" + pgnodes.size() + "] != children.size() [" + children.size() + "]");
assertionFailed = true;
} else {
for (int idx = 0; idx < children.size(); idx++) {
Node n = children.get(idx);
if (n.getParent() != this) {
java.lang.System.err.println("*** this=" + this + " validatePG children[" + idx + "].parent= " + n.getParent());
assertionFailed = true;
}
if (n.getPeer() != pgnodes.get(idx)) {
java.lang.System.err.println("*** pgnodes[" + idx + "] validatePG != children[" + idx + "]");
assertionFailed = true;
}
}
}
if (assertionFailed) {
throw new java.lang.AssertionError("validation of PGGroup children failed");
}
}
void printSeq(String prefix, List nodes) {
String str = prefix;
for (Node nn : nodes) {
str += nn + " ";
}
System.out.println(str);
}
/**
* The viewOrderChildren is a list children sorted in decreasing viewOrder
* order if it is not empty. Its size should always be equal to
* children.size(). If viewOrderChildren is empty it implies that the
* rendering order of the children is the same as the order in the children
* list.
*/
private final List viewOrderChildren = new ArrayList(1);
void markViewOrderChildrenDirty() {
viewOrderChildren.clear();
NodeHelper.markDirty(this, DirtyBits.PARENT_CHILDREN_VIEW_ORDER);
}
private void computeViewOrderChildren() {
boolean viewOrderSet = false;
for (Node child : children) {
double vo = child.getViewOrder();
if (!viewOrderSet && vo != 0) {
viewOrderSet = true;
}
}
viewOrderChildren.clear();
if (viewOrderSet) {
viewOrderChildren.addAll(children);
// Sort in descending order (or big-to-small order)
Collections.sort(viewOrderChildren, (Node a, Node b)
-> a.getViewOrder() < b.getViewOrder() ? 1
: a.getViewOrder() == b.getViewOrder() ? 0 : -1);
}
}
// Call this method if children view order is needed for picking.
// The returned list should be treated as read only.
private List getOrderedChildren() {
if (isDirty(DirtyBits.PARENT_CHILDREN_VIEW_ORDER)) {
//Fix for JDK-8205092
computeViewOrderChildren();
}
if (!viewOrderChildren.isEmpty()) {
return viewOrderChildren;
}
return children;
}
// Variable used to indicate that the change to the children ObservableList is
// a simple permutation as the result of a toFront or toBack operation.
// We can avoid almost all of the processing of the on replace trigger in
// this case.
private boolean childrenTriggerPermutation = false;
//accumulates all removed nodes between pulses, for dirty area calculation.
private List removed;
// set to true if either childRemoved or childAdded returns
// true. These functions will indicate whether the geom
// bounds for the parent have changed
private boolean geomChanged;
private boolean childSetModified;
private final ObservableList children = new VetoableListDecorator(new TrackableObservableList() {
@Override
protected void onChanged(Change c) {
// proceed with updating the scene graph
unmodifiableManagedChildren = null;
boolean relayout = false;
boolean viewOrderChildrenDirty = false;
if (childSetModified) {
while (c.next()) {
int from = c.getFrom();
int to = c.getTo();
for (int i = from; i < to; ++i) {
Node n = children.get(i);
if (n.getParent() != null && n.getParent() != Parent.this) {
if (warnOnAutoMove) {
java.lang.System.err.println("WARNING added to a new parent without first removing it from its current");
java.lang.System.err.println(" parent. It will be automatically removed from its current parent.");
java.lang.System.err.println(" node=" + n + " oldparent= " + n.getParent() + " newparent=" + this);
}
n.getParent().children.remove(n);
if (warnOnAutoMove) {
Thread.dumpStack();
}
}
}
List removed = c.getRemoved();
int removedSize = removed.size();
for (int i = 0; i < removedSize; ++i) {
final Node n = removed.get(i);
if (n.isManaged()) {
relayout = true;
}
}
// Mark viewOrderChildrenDirty if there is modification to children list
// and view order was set on one or more of the children prior to this change
if (((removedSize > 0) || (to - from) > 0) && !viewOrderChildren.isEmpty()) {
viewOrderChildrenDirty = true;
}
// update the parent and scene for each new node
for (int i = from; i < to; ++i) {
Node node = children.get(i);
// Newly added node has view order set.
if (node.getViewOrder() != 0) {
viewOrderChildrenDirty = true;
}
if (node.isManaged() || (node instanceof Parent && ((Parent) node).layoutFlag != LayoutFlags.CLEAN)) {
relayout = true;
}
node.setParent(Parent.this);
node.setScenes(getScene(), getSubScene());
// assert !node.boundsChanged;
if (node.isVisible()) {
geomChanged = true;
childIncluded(node);
}
}
}
// check to see if the number of children exceeds
// DIRTY_CHILDREN_THRESHOLD and dirtyChildren is null.
// If so, then we need to create dirtyChildren and
// populate it.
if (dirtyChildren == null && children.size() > DIRTY_CHILDREN_THRESHOLD) {
dirtyChildren
= new ArrayList<>(2 * DIRTY_CHILDREN_THRESHOLD);
// only bother populating children if geom has
// changed, otherwise there is no need
if (dirtyChildrenCount > 0) {
int size = children.size();
for (int i = 0; i < size; ++i) {
Node ch = children.get(i);
if (ch.isVisible() && ch.boundsChanged) {
dirtyChildren.add(ch);
}
}
}
}
} else {
// If childSet was not modified, we still need to check whether the permutation
// did change the layout
layout_loop:while (c.next()) {
List removed = c.getRemoved();
for (int i = 0, removedSize = removed.size(); i < removedSize; ++i) {
if (removed.get(i).isManaged()) {
relayout = true;
break layout_loop;
}
}
for (int i = c.getFrom(), to = c.getTo(); i < to; ++i) {
if (children.get(i).isManaged()) {
relayout = true;
break layout_loop;
}
}
}
}
//
// Note that the styles of a child do not affect the parent or
// its siblings. Thus, it is only necessary to reapply css to
// the Node just added and not to this parent and all of its
// children. So the following call to reapplyCSS was moved
// to Node.parentProperty. The original comment and code were
// purposely left here as documentation should there be any
// question about how the code used to work and why the change
// was made.
//
// if children have changed then I need to reapply
// CSS from this node on down
// reapplyCSS();
//
// request layout if a Group subclass has overridden doLayout OR
// if one of the new children needs layout, in which case need to ensure
// the needsLayout flag is set all the way to the root so the next layout
// pass will reach the child.
if (relayout) {
requestLayout();
}
if (geomChanged) {
NodeHelper.geomChanged(Parent.this);
}
// 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();
}
NodeHelper.markDirty(Parent.this, DirtyBits.PARENT_CHILDREN);
// Force synchronization to include the handling of invisible node
// so that removed list will get cleanup to prevent memory leak.
NodeHelper.markDirty(Parent.this, DirtyBits.NODE_FORCE_SYNC);
if (viewOrderChildrenDirty) {
markViewOrderChildrenDirty();
}
}
}) {
@Override
protected void onProposedChange(final List newNodes, int... toBeRemoved) {
final Scene scene = getScene();
if (scene != null) {
Window w = scene.getWindow();
if (w != null && WindowHelper.getPeer(w) != null) {
Toolkit.getToolkit().checkFxUserThread();
}
}
geomChanged = false;
long newLength = children.size() + newNodes.size();
int removedLength = 0;
for (int i = 0; i < toBeRemoved.length; i += 2) {
removedLength += toBeRemoved[i + 1] - toBeRemoved[i];
}
newLength -= removedLength;
// If the childrenTriggerPermutation flag is set, then we know it
// is a simple permutation and no further checking is needed.
if (childrenTriggerPermutation) {
childSetModified = false;
return;
}
// If the childrenTriggerPermutation flag is not set, then we will
// check to see whether any element in the ObservableList has changed,
// or whether the new ObservableList is a permutation on the existing
// ObservableList. Note that even if the childrenModified flag is false,
// we still have to check for duplicates. If it is a simple
// permutation, we can avoid checking for cycles or other parents.
childSetModified = true;
if (newLength == childSet.size()) {
childSetModified = false;
for (int i = newNodes.size() - 1; i >= 0; --i ) {
Node n = newNodes.get(i);
if (!childSet.contains(n)) {
childSetModified = true;
break;
}
}
}
// Enforce scene graph invariants, and check for structural errors.
//
// 1. If a child has been added to this parent more than once,
// then it is an error
//
// 2. If a child is a target of a clip, then it is an error.
//
// 3. If a node would cause a cycle, then it is an error.
//
// 4. If a node is null
//
// Note that if a node is the child of another parent, we will
// implicitly remove the node from its former Parent after first
// checking for errors.
// iterate over the nodes that were removed and remove them from
// the hash set.
for (int i = 0; i < toBeRemoved.length; i += 2) {
for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
childSet.remove(children.get(j));
}
}
try {
if (childSetModified) {
// check individual children before duplication test
// if done in this order, the exception is more specific
for (int i = newNodes.size() - 1; i >= 0; --i ) {
Node node = newNodes.get(i);
if (node == null) {
throw new NullPointerException(
constructExceptionMessage(
"child node is null", null));
}
if (node.getClipParent() != null) {
throw new IllegalArgumentException(
constructExceptionMessage(
"node already used as a clip", node));
}
if (wouldCreateCycle(Parent.this, node)) {
throw new IllegalArgumentException(
constructExceptionMessage(
"cycle detected", node));
}
}
}
childSet.addAll(newNodes);
if (childSet.size() != newLength) {
throw new IllegalArgumentException(
constructExceptionMessage(
"duplicate children added", null));
}
} catch (RuntimeException e) {
//Return children to it's original state
childSet.clear();
childSet.addAll(children);
// rethrow
throw e;
}
// Done with error checking
if (!childSetModified) {
return;
}
// iterate over the nodes that were removed and clear their
// parent and scene. Add to them also to removed list for further
// dirty regions calculation.
if (removed == null) {
removed = new ArrayList<>();
}
if (removed.size() + removedLength > REMOVED_CHILDREN_THRESHOLD || !isTreeVisible()) {
//do not populate too many children in removed list
removedChildrenOptimizationDisabled = true;
}
for (int i = 0; i < toBeRemoved.length; i += 2) {
for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
Node old = children.get(j);
final Scene oldScene = old.getScene();
if (oldScene != null) {
oldScene.generateMouseExited(old);
}
if (dirtyChildren != null) {
dirtyChildren.remove(old);
}
if (old.isVisible()) {
geomChanged = true;
childExcluded(old);
}
if (old.getParent() == Parent.this) {
old.setParent(null);
old.setScenes(null, null);
}
// Do not add node with null scene to the removed list.
// It will not be processed in the list and its memory
// will not be freed.
if (scene != null && !removedChildrenOptimizationDisabled) {
removed.add(old);
}
}
}
}
private String constructExceptionMessage(
String cause, Node offendingNode) {
final StringBuilder sb = new StringBuilder("Children: ");
sb.append(cause);
sb.append(": parent = ").append(Parent.this);
if (offendingNode != null) {
sb.append(", node = ").append(offendingNode);
}
return sb.toString();
}
};
/**
* A constant reference to an unmodifiable view of the children, such that every time
* we ask for an unmodifiable list of children, we don't actually create a new
* collection and return it. The memory overhead is pretty lightweight compared
* to all the garbage we would otherwise generate.
*/
private final ObservableList unmodifiableChildren =
FXCollections.unmodifiableObservableList(children);
/**
* A cached reference to the unmodifiable managed children of this Parent. This is
* created whenever first asked for, and thrown away whenever children are added
* or removed or when their managed state changes. This could be written
* differently, such that this list is essentially a filtered copy of the
* main children, but that additional overhead might not be worth it.
*/
private List unmodifiableManagedChildren = null;
/**
* Gets the list of children of this {@code Parent}.
*
*
* See the class documentation for {@link Node} for scene graph structure
* restrictions on setting a {@link Parent}'s children list.
* If these restrictions are violated by a change to the list of children,
* the change is ignored and the previous value of the children list is
* restored. An {@link IllegalArgumentException} is thrown in this case.
*
*
* If this {@link Parent} node is attached to a {@link Scene} attached to a {@link Window}
* that is showning ({@link javafx.stage.Window#isShowing()}), then its
* list of children must only be modified on the JavaFX Application Thread.
* An {@link IllegalStateException} is thrown if this restriction is
* violated.
*
*
* Note to subclasses: if you override this method, you must return from
* your implementation the result of calling this super method. The actual
* list instance returned from any getChildren() implementation must be
* the list owned and managed by this Parent. The only typical purpose
* for overriding this method is to promote the method to be public.
*
* @return the list of children of this {@code Parent}.
*/
protected ObservableList getChildren() {
return children;
}
/**
* Gets the list of children of this {@code Parent} as a read-only
* list.
*
* @return read-only access to this parent's children ObservableList
*/
public ObservableList getChildrenUnmodifiable() {
return unmodifiableChildren;
}
/**
* Gets the list of all managed children of this {@code Parent}.
*
* @param the type of the children nodes
* @return list of all managed children in this parent
*/
protected List getManagedChildren() {
if (unmodifiableManagedChildren == null) {
unmodifiableManagedChildren = new ArrayList<>();
for (int i=0, max=children.size(); i)unmodifiableManagedChildren;
}
/**
* Called by Node whenever its managed state may have changed, this
* method will cause the view of managed children to be updated
* such that it properly includes or excludes this child.
*/
final void managedChildChanged() {
requestLayout();
unmodifiableManagedChildren = null;
}
// implementation of Node.toFront function
final void toFront(Node node) {
if (Utils.assertionEnabled()) {
if (!childSet.contains(node)) {
throw new java.lang.AssertionError(
"specified node is not in the list of children");
}
}
if (children.get(children.size() - 1) != node) {
childrenTriggerPermutation = true;
try {
children.remove(node);
children.add(node);
} finally {
childrenTriggerPermutation = false;
}
}
}
// implementation of Node.toBack function
final void toBack(Node node) {
if (Utils.assertionEnabled()) {
if (!childSet.contains(node)) {
throw new java.lang.AssertionError(
"specified node is not in the list of children");
}
}
if (children.get(0) != node) {
childrenTriggerPermutation = true;
try {
children.remove(node);
children.add(0, node);
} finally {
childrenTriggerPermutation = false;
}
}
}
@Override
void scenesChanged(final Scene newScene, final SubScene newSubScene,
final Scene oldScene, final SubScene oldSubScene) {
if (oldScene != null && newScene == null) {
// RT-34863 - clean up CSS cache when Parent is removed from scene-graph
StyleManager.getInstance().forget(this);
// Clear removed list on parent who is no longer in a scene
if (removed != null) {
removed.clear();
}
}
for (int i=0; i orderedChildren = getOrderedChildren();
for (int i = orderedChildren.size() - 1; i >= 0; i--) {
orderedChildren.get(i).pickNode(pickRay, result);
if (result.isClosed()) {
return false;
}
}
return true;
}
/*
* Note: This method MUST only be called via its accessor method.
*/
private void doPickNodeLocal(PickRay pickRay, PickResultChooser result) {
double boundsDistance = intersectsBounds(pickRay);
if (!Double.isNaN(boundsDistance) && pickChildrenNode(pickRay, result)) {
if (isPickOnBounds()) {
result.offer(this, boundsDistance, PickResultChooser.computePoint(pickRay, boundsDistance));
}
}
}
@Override boolean isConnected() {
return super.isConnected() || sceneRoot;
}
@Override public Node lookup(String selector) {
Node n = super.lookup(selector);
if (n == null) {
for (int i=0, max=children.size(); i lookupAll(Selector selector, List results) {
results = super.lookupAll(selector, results);
for (int i=0, max=children.size(); i
* If this parent is either a layout root or unmanaged, then it will be
* added directly to the scene's dirty layout list, otherwise requestParentLayout
* will be invoked.
* @since JavaFX 8.0
*/
public void requestLayout() {
clearSizeCache();
markDirtyLayout(false, forceParentLayout);
}
private boolean forceParentLayout = false;
/**
* A package scope method used by Node and serves as a helper method for
* requestLayout() (see above). If forceParentLayout is true it will
* propagate this force layout flag to its parent.
*/
void requestLayout(boolean forceParentLayout) {
boolean savedForceParentLayout = this.forceParentLayout;
this.forceParentLayout = forceParentLayout;
requestLayout();
this.forceParentLayout = savedForceParentLayout;
}
/**
* Requests a layout pass of the parent to be performed before the next scene is
* rendered. This is batched up asynchronously to happen once per
* "pulse", or frame of animation.
*
* This may be used when the current parent have changed it's min/max/preferred width/height,
* but doesn't know yet if the change will lead to it's actual size change. This will be determined
* when it's parent recomputes the layout with the new hints.
*/
protected final void requestParentLayout() {
requestParentLayout(false);
}
/**
* A package scope method used by Node and serves as a helper method for
* requestParentLayout() (see above). If forceParentLayout is true it will
* force a request layout call on its parent if its parent is not null.
*/
void requestParentLayout(boolean forceParentLayout) {
if (!layoutRoot) {
final Parent p = getParent();
if (p != null && (!p.performingLayout || forceParentLayout)) {
p.requestLayout();
}
}
}
void clearSizeCache() {
if (sizeCacheClear) {
return;
}
sizeCacheClear = true;
prefWidthCache = -1;
prefHeightCache = -1;
minWidthCache = -1;
minHeightCache = -1;
}
@Override public double prefWidth(double height) {
if (height == -1) {
if (prefWidthCache == -1) {
prefWidthCache = computePrefWidth(-1);
if (Double.isNaN(prefWidthCache) || prefWidthCache < 0) prefWidthCache = 0;
sizeCacheClear = false;
}
return prefWidthCache;
} else {
double result = computePrefWidth(height);
return Double.isNaN(result) || result < 0 ? 0 : result;
}
}
@Override public double prefHeight(double width) {
if (width == -1) {
if (prefHeightCache == -1) {
prefHeightCache = computePrefHeight(-1);
if (Double.isNaN(prefHeightCache) || prefHeightCache < 0) prefHeightCache = 0;
sizeCacheClear = false;
}
return prefHeightCache;
} else {
double result = computePrefHeight(width);
return Double.isNaN(result) || result < 0 ? 0 : result;
}
}
@Override public double minWidth(double height) {
if (height == -1) {
if (minWidthCache == -1) {
minWidthCache = computeMinWidth(-1);
if (Double.isNaN(minWidthCache) || minWidthCache < 0) minWidthCache = 0;
sizeCacheClear = false;
}
return minWidthCache;
} else {
double result = computeMinWidth(height);
return Double.isNaN(result) || result < 0 ? 0 : result;
}
}
@Override public double minHeight(double width) {
if (width == -1) {
if (minHeightCache == -1) {
minHeightCache = computeMinHeight(-1);
if (Double.isNaN(minHeightCache) || minHeightCache < 0) minHeightCache = 0;
sizeCacheClear = false;
}
return minHeightCache;
} else {
double result = computeMinHeight(width);
return Double.isNaN(result) || result < 0 ? 0 : result;
}
}
// PENDING_DOC_REVIEW
/**
* Calculates the preferred width of this {@code Parent}. The default
* implementation calculates this width as the width of the area occupied
* by its managed children when they are positioned at their
* current positions at their preferred widths.
*
* @param height the height that should be used if preferred width depends
* on it
* @return the calculated preferred width
*/
protected double computePrefWidth(double height) {
double minX = 0;
double maxX = 0;
for (int i=0, max=children.size(); i
* Subclasses should override this function to layout content as needed.
*/
protected void layoutChildren() {
for (int i=0, max=children.size(); iCSS Reference
* Guide.
*/
private final ObservableList stylesheets = new TrackableObservableList<>() {
@Override
protected void onChanged(Change c) {
final Scene scene = getScene();
if (scene != null) {
// Notify the StyleManager if stylesheets change. This Parent's
// styleManager will get recreated in NodeHelper.processCSS.
StyleManager.getInstance().stylesheetsChanged(Parent.this, c);
// RT-9784 - if stylesheet is removed, reset styled properties to
// their initial value.
c.reset();
while(c.next()) {
if (c.wasRemoved() == false) {
continue;
}
break; // no point in resetting more than once...
}
reapplyCSS();
}
}
};
/**
* Gets an observable list of string URLs linking to the stylesheets to use
* with this Parent's contents. See {@link Scene#getStylesheets()} for details.
* For additional information about using CSS
* with the scene graph, see the CSS Reference
* Guide.
*
* @return the list of stylesheets to use with this Parent
* @since JavaFX 2.1
*/
public final ObservableList getStylesheets() { return stylesheets; }
/*
* This method recurses up the parent chain until parent is null. As the
* stack unwinds, if the Parent has stylesheets, they are added to the
* list.
*
* It is possible to override this method to stop the recursion. This allows
* a Parent to have a set of stylesheets distinct from its Parent.
*
* Note: This method MUST only be called via its accessor method.
*/
// SB-dependency: RT-21247 has been filed to track this
private List doGetAllParentStylesheets() {
List list = null;
final Parent myParent = getParent();
if (myParent != null) {
//
// recurse so that stylesheets of Parents closest to the root are
// added to the list first. The ensures that declarations for
// stylesheets further down the tree (closer to the leaf) have
// a higer ordinal in the cascade.
//
list = ParentHelper.getAllParentStylesheets(myParent);
}
if (stylesheets != null && stylesheets.isEmpty() == false) {
if (list == null) {
list = new ArrayList<>(stylesheets.size());
}
for (int n=0,nMax=stylesheets.size(); n 0) {
child.cssFlag = CssFlags.UPDATE;
}
NodeHelper.processCSS(child);
}
}
/* *********************************************************************
* Misc *
* *
* Initialization and other functions *
* *
**********************************************************************/
{
// To initialize the class helper at the begining each constructor of this class
ParentHelper.initHelper(this);
}
/**
* Constructs a new {@code Parent}.
*/
protected Parent() {
layoutFlag = LayoutFlags.NEEDS_LAYOUT;
setAccessibleRole(AccessibleRole.PARENT);
}
private NGNode doCreatePeer() {
return new NGGroup();
}
@Override
void nodeResolvedOrientationChanged() {
for (int i = 0, max = children.size(); i < max; ++i) {
children.get(i).parentResolvedOrientationInvalidated();
}
}
/* *************************************************************************
* *
* Bounds Computations *
* *
* This code originated in GroupBoundsHelper (part of javafx-sg-common) *
* but has been ported here to the FX side since we cannot rely on the PG *
* side for computing the bounds (due to the decoupling of the two *
* scenegraphs for threading and other purposes). *
* *
* Unfortunately, we cannot simply reuse GroupBoundsHelper without some *
* major (and hacky) modification due to the fact that GroupBoundsHelper *
* relies on PG state and we need to do similar things here that rely on *
* core scenegraph state. Unfortunately, that means we made a port. *
* *
**************************************************************************/
private BaseBounds tmp = new RectBounds();
/**
* The cached bounds for the Group. If the cachedBounds are invalid
* then we have no history of what the bounds are, or were.
*/
private BaseBounds cachedBounds = new RectBounds();
/**
* Indicates that the cachedBounds is invalid (or old) and need to be recomputed.
* If cachedBoundsInvalid is true and dirtyChildrenCount is non-zero,
* then when we recompute the cachedBounds we can consider the
* values in cachedBounds to represent the last valid bounds for the group.
* This is useful for several fast paths.
*/
private boolean cachedBoundsInvalid;
/**
* The number of dirty children which bounds haven't been incorporated
* into the cached bounds yet. Can be used even when dirtyChildren is null.
*/
private int dirtyChildrenCount;
/**
* This set is used to track all of the children of this group which are
* dirty. It is only used in cases where the number of children is > some
* value (currently 10). For very wide trees, this can provide a very
* important speed boost. For the sake of memory consumption, this is
* null unless the number of children ever crosses the threshold where
* it will be activated.
*/
private ArrayList dirtyChildren;
private Node top;
private Node left;
private Node bottom;
private Node right;
private Node near;
private Node far;
private BaseBounds doComputeGeomBounds(BaseBounds bounds, BaseTransform tx) {
// If we have no children, our bounds are invalid
if (children.isEmpty()) {
return bounds.makeEmpty();
}
if (tx.isTranslateOrIdentity()) {
// this is a transform which is only doing translations, or nothing
// at all (no scales, rotates, or shears)
// so in this case we can easily use the cached bounds
if (cachedBoundsInvalid) {
recomputeBounds();
if (dirtyChildren != null) {
dirtyChildren.clear();
}
cachedBoundsInvalid = false;
dirtyChildrenCount = 0;
}
if (!tx.isIdentity()) {
bounds = bounds.deriveWithNewBounds((float)(cachedBounds.getMinX() + tx.getMxt()),
(float)(cachedBounds.getMinY() + tx.getMyt()),
(float)(cachedBounds.getMinZ() + tx.getMzt()),
(float)(cachedBounds.getMaxX() + tx.getMxt()),
(float)(cachedBounds.getMaxY() + tx.getMyt()),
(float)(cachedBounds.getMaxZ() + tx.getMzt()));
} else {
bounds = bounds.deriveWithNewBounds(cachedBounds);
}
return bounds;
} else {
// there is a scale, shear, or rotation happening, so need to
// do the full transform!
double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE, maxZ = Double.MIN_VALUE;
boolean first = true;
for (int i=0, max=children.size(); i dirtyNodes,
int remainingDirtyNodes) {
// fast path for untransformed bounds calculation
if (cachedBounds.isEmpty()) {
createCachedBounds(dirtyNodes);
return true;
}
int invalidEdges = 0;
if ((left == null) || left.boundsChanged) {
invalidEdges |= LEFT_INVALID;
}
if ((top == null) || top.boundsChanged) {
invalidEdges |= TOP_INVALID;
}
if ((near == null) || near.boundsChanged) {
invalidEdges |= NEAR_INVALID;
}
if ((right == null) || right.boundsChanged) {
invalidEdges |= RIGHT_INVALID;
}
if ((bottom == null) || bottom.boundsChanged) {
invalidEdges |= BOTTOM_INVALID;
}
if ((far == null) || far.boundsChanged) {
invalidEdges |= FAR_INVALID;
}
// These indicate the bounds of the Group as computed by this
// function
float minX = cachedBounds.getMinX();
float minY = cachedBounds.getMinY();
float minZ = cachedBounds.getMinZ();
float maxX = cachedBounds.getMaxX();
float maxY = cachedBounds.getMaxY();
float maxZ = cachedBounds.getMaxZ();
// this checks the newly added nodes first, so if dirtyNodes is the
// whole children list, we can end early
for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) {
final Node node = dirtyNodes.get(i);
if (node.boundsChanged) {
// assert node.isVisible();
node.boundsChanged = false;
--remainingDirtyNodes;
tmp = getChildTransformedBounds(node, BaseTransform.IDENTITY_TRANSFORM, tmp);
if (!tmp.isEmpty()) {
float tmpx = tmp.getMinX();
float tmpy = tmp.getMinY();
float tmpz = tmp.getMinZ();
float tmpx2 = tmp.getMaxX();
float tmpy2 = tmp.getMaxY();
float tmpz2 = tmp.getMaxZ();
// If this node forms an edge, then we will set it to be the
// node for this edge and update the min/max values
if (tmpx <= minX) {
minX = tmpx;
left = node;
invalidEdges &= ~LEFT_INVALID;
}
if (tmpy <= minY) {
minY = tmpy;
top = node;
invalidEdges &= ~TOP_INVALID;
}
if (tmpz <= minZ) {
minZ = tmpz;
near = node;
invalidEdges &= ~NEAR_INVALID;
}
if (tmpx2 >= maxX) {
maxX = tmpx2;
right = node;
invalidEdges &= ~RIGHT_INVALID;
}
if (tmpy2 >= maxY) {
maxY = tmpy2;
bottom = node;
invalidEdges &= ~BOTTOM_INVALID;
}
if (tmpz2 >= maxZ) {
maxZ = tmpz2;
far = node;
invalidEdges &= ~FAR_INVALID;
}
}
}
}
if (invalidEdges != 0) {
// failed to validate some edges
return false;
}
cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
maxX, maxY, maxZ);
return true;
}
private void createCachedBounds(final List fromNodes) {
// These indicate the bounds of the Group as computed by this function
float minX, minY, minZ;
float maxX, maxY, maxZ;
final int nodeCount = fromNodes.size();
int i;
// handle first visible non-empty node
for (i = 0; i < nodeCount; ++i) {
final Node node = fromNodes.get(i);
node.boundsChanged = false;
if (node.isVisible()) {
tmp = node.getTransformedBounds(
tmp, BaseTransform.IDENTITY_TRANSFORM);
if (!tmp.isEmpty()) {
left = top = near = right = bottom = far = node;
break;
}
}
}
if (i == nodeCount) {
left = top = near = right = bottom = far = null;
cachedBounds.makeEmpty();
return;
}
minX = tmp.getMinX();
minY = tmp.getMinY();
minZ = tmp.getMinZ();
maxX = tmp.getMaxX();
maxY = tmp.getMaxY();
maxZ = tmp.getMaxZ();
// handle remaining visible non-empty nodes
for (++i; i < nodeCount; ++i) {
final Node node = fromNodes.get(i);
node.boundsChanged = false;
if (node.isVisible()) {
tmp = node.getTransformedBounds(
tmp, BaseTransform.IDENTITY_TRANSFORM);
if (!tmp.isEmpty()) {
final float tmpx = tmp.getMinX();
final float tmpy = tmp.getMinY();
final float tmpz = tmp.getMinZ();
final float tmpx2 = tmp.getMaxX();
final float tmpy2 = tmp.getMaxY();
final float tmpz2 = tmp.getMaxZ();
if (tmpx < minX) { minX = tmpx; left = node; }
if (tmpy < minY) { minY = tmpy; top = node; }
if (tmpz < minZ) { minZ = tmpz; near = node; }
if (tmpx2 > maxX) { maxX = tmpx2; right = node; }
if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; }
if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; }
}
}
}
cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
maxX, maxY, maxZ);
}
/**
* Updates the bounds of this {@code Parent} and its children.
*/
@Override protected void updateBounds() {
for (int i=0, max=children.size(); i test_getRemoved() {
return removed;
}
/**
* Note: The only user of this method is in unit test:
* Parent_viewOrderChildren_sync_Test.
*/
List test_getViewOrderChildren() {
return viewOrderChildren;
}
}