javafx.scene.Parent.java Source code

Java tutorial

Introduction

Here is the source code for javafx.scene.Parent.java

Source

/*
 * Copyright (c) 2010, 2018, 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.
 * <p>
 * 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.
 * <p>
 * There are two direct concrete Parent subclasses
 * <ul>
 * <li>{@link Group} effects and transforms to be applied to a collection of child nodes.</li>
 * <li>{@link javafx.scene.layout.Region} class for nodes that can be styled with CSS and layout children. </li>
 * </ul>
 *
 * @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<String> 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<NGNode> 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)) {
            computeViewOrderChidrenAndUpdatePeer();
        }

        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<Node> childSet = new HashSet<Node>();

    // 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<NGNode> 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<Node> 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<Node> viewOrderChildren = new ArrayList(1);

    void markViewOrderChildrenDirty() {
        NodeHelper.markDirty(this, DirtyBits.PARENT_CHILDREN_VIEW_ORDER);
    }

    private void computeViewOrderChidrenAndUpdatePeer() {
        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);
        }

        final NGGroup peer = getPeer();
        peer.setViewOrderChildren(viewOrderChildren);
    }

    // Call this method if children view order is needed for picking.
    // The returned list should be treated as read only.
    private List<Node> getOrderedChildren() {
        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<Node> 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<Node> children = new VetoableListDecorator<Node>(
            new TrackableObservableList<Node>() {

                protected void onChanged(Change<Node> 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<Node> 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<Node>(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<Node> 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) {
                        NodeHelper.markDirty(Parent.this, DirtyBits.PARENT_CHILDREN_VIEW_ORDER);
                    }
                }

            }) {
        @Override
        protected void onProposedChange(final List<Node> 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<Node>();
            }
            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<Node> 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<Node> unmodifiableManagedChildren = null;

    /**
     * Gets the list of children of this {@code Parent}.
     *
     * <p>
     * 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.
     *
     * <p>
     * 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.
     *
     * <p>
     * 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<Node> 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<Node> getChildrenUnmodifiable() {
        return unmodifiableChildren;
    }

    /**
     * Gets the list of all managed children of this {@code Parent}.
     *
     * @param <E> the type of the children nodes
     * @return list of all managed children in this parent
     */
    protected <E extends Node> List<E> getManagedChildren() {
        if (unmodifiableManagedChildren == null) {
            unmodifiableManagedChildren = new ArrayList<Node>();
            for (int i = 0, max = children.size(); i < max; i++) {
                Node e = children.get(i);
                if (e.isManaged()) {
                    unmodifiableManagedChildren.add(e);
                }
            }
        }
        return (List<E>) 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 < children.size(); i++) {
            children.get(i).setScenes(newScene, newSubScene);
        }

        final boolean awaitingLayout = layoutFlag != LayoutFlags.CLEAN;

        sceneRoot = (newSubScene != null && newSubScene.getRoot() == this)
                || (newScene != null && newScene.getRoot() == this);
        layoutRoot = !isManaged() || sceneRoot;

        if (awaitingLayout) {
            // If this node is dirty and the new scene or subScene is not null
            // then add this node to the new scene's dirty list
            if (newScene != null && layoutRoot) {
                if (newSubScene != null) {
                    newSubScene.setDirtyLayout(this);
                }
            }
        }
    }

    @Override
    void setDerivedDepthTest(boolean value) {
        super.setDerivedDepthTest(value);

        for (int i = 0, max = children.size(); i < max; i++) {
            final Node node = children.get(i);
            node.computeDerivedDepthTest();
        }
    }

    boolean pickChildrenNode(PickRay pickRay, PickResultChooser result) {
        List<Node> 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 < max; i++) {
                final Node node = children.get(i);
                n = node.lookup(selector);
                if (n != null)
                    return n;
            }
        }
        return n;
    }

    /**
     * Please Note: This method should never create the results set,
     * let the Node class implementation do this!
     */
    @Override
    List<Node> lookupAll(Selector selector, List<Node> results) {
        results = super.lookupAll(selector, results);
        for (int i = 0, max = children.size(); i < max; i++) {
            final Node node = children.get(i);
            results = node.lookupAll(selector, results);
        }
        return results;
    }

    private ParentTraversalEngine traversalEngine;

    private final void setTraversalEngine(ParentTraversalEngine value) {
        this.traversalEngine = value;
    }

    private final ParentTraversalEngine getTraversalEngine() {
        return traversalEngine;
    }

    /***********************************************************************
     *                               Layout                                *
     *                                                                     *
     *  Functions and variables related to the layout scheme used by       *
     *  JavaFX. Includes both public and private API.                      *
     *                                                                     *
     **********************************************************************/
    /**
     * Indicates that this Node and its subnodes requires a layout pass on
     * the next pulse.
     */
    private ReadOnlyBooleanWrapper needsLayout;
    LayoutFlags layoutFlag = LayoutFlags.CLEAN;

    protected final void setNeedsLayout(boolean value) {
        if (value) {
            markDirtyLayout(true, false);
        } else if (layoutFlag == LayoutFlags.NEEDS_LAYOUT) {
            boolean hasBranch = false;
            for (int i = 0, max = children.size(); i < max; i++) {
                final Node child = children.get(i);
                if (child instanceof Parent) {
                    if (((Parent) child).layoutFlag != LayoutFlags.CLEAN) {
                        hasBranch = true;
                        break;
                    }

                }
            }
            setLayoutFlag(hasBranch ? LayoutFlags.DIRTY_BRANCH : LayoutFlags.CLEAN);
        }
    }

    public final boolean isNeedsLayout() {
        return layoutFlag == LayoutFlags.NEEDS_LAYOUT;
    }

    public final ReadOnlyBooleanProperty needsLayoutProperty() {
        if (needsLayout == null) {
            needsLayout = new ReadOnlyBooleanWrapper(this, "needsLayout", layoutFlag == LayoutFlags.NEEDS_LAYOUT);
        }
        return needsLayout;
    }

    /**
     * This is used only by CCS in Node. It is set to true while
     * the layout() function is processing and set to false on the conclusion.
     * It is used by the Node to decide whether to perform CSS updates
     * synchronously or asynchronously.
     */
    private boolean performingLayout = false;

    boolean isPerformingLayout() {
        return performingLayout;
    }

    private boolean sizeCacheClear = true;
    private double prefWidthCache = -1;
    private double prefHeightCache = -1;
    private double minWidthCache = -1;
    private double minHeightCache = -1;

    void setLayoutFlag(LayoutFlags flag) {
        if (needsLayout != null) {
            needsLayout.set(flag == LayoutFlags.NEEDS_LAYOUT);
        }
        layoutFlag = flag;
    }

    private void markDirtyLayout(boolean local, boolean forceParentLayout) {
        setLayoutFlag(LayoutFlags.NEEDS_LAYOUT);
        if (local || layoutRoot) {
            if (sceneRoot) {
                Toolkit.getToolkit().requestNextPulse();
                if (getSubScene() != null) {
                    getSubScene().setDirtyLayout(this);
                }
            } else {
                markDirtyLayoutBranch();
            }
        } else {
            requestParentLayout(forceParentLayout);
        }
    }

    /**
     * Requests a layout pass to be performed before the next scene is
     * rendered. This is batched up asynchronously to happen once per
     * "pulse", or frame of animation.
     * <p>
     * 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.
     * <p>
     * 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 < max; i++) {
            Node node = children.get(i);
            if (node.isManaged()) {
                final double x = node.getLayoutBounds().getMinX() + node.getLayoutX();
                minX = Math.min(minX, x);
                maxX = Math.max(maxX, x + boundedSize(node.prefWidth(-1), node.minWidth(-1), node.maxWidth(-1)));
            }
        }
        return maxX - minX;
    }

    // PENDING_DOC_REVIEW
    /**
     * Calculates the preferred height of this {@code Parent}. The default
     * implementation calculates this height as the height of the area occupied
     * by its managed children when they are positioned at their current
     * positions at their preferred heights.
     *
     * @param width the width that should be used if preferred height depends
     *      on it
     * @return the calculated preferred height
     */
    protected double computePrefHeight(double width) {
        double minY = 0;
        double maxY = 0;
        for (int i = 0, max = children.size(); i < max; i++) {
            Node node = children.get(i);
            if (node.isManaged()) {
                final double y = node.getLayoutBounds().getMinY() + node.getLayoutY();
                minY = Math.min(minY, y);
                maxY = Math.max(maxY, y + boundedSize(node.prefHeight(-1), node.minHeight(-1), node.maxHeight(-1)));
            }
        }
        return maxY - minY;
    }

    /**
     * Calculates the minimum width of this {@code Parent}. The default
     * implementation simply returns the pref width.
     *
     * @param height the height that should be used if min width depends
     *      on it
     * @return the calculated min width
     * @since JavaFX 2.1
     */
    protected double computeMinWidth(double height) {
        return prefWidth(height);
    }

    // PENDING_DOC_REVIEW
    /**
     * Calculates the min height of this {@code Parent}. The default
     * implementation simply returns the pref height;
     *
     * @param width the width that should be used if min height depends
     *      on it
     * @return the calculated min height
     * @since JavaFX 2.1
     */
    protected double computeMinHeight(double width) {
        return prefHeight(width);
    }

    /**
     * Calculates the baseline offset based on the first managed child. If there
     * is no such child, returns {@link Node#getBaselineOffset()}.
     *
     * @return baseline offset
     */
    @Override
    public double getBaselineOffset() {
        for (int i = 0, max = children.size(); i < max; i++) {
            final Node child = children.get(i);
            if (child.isManaged()) {
                double offset = child.getBaselineOffset();
                if (offset == BASELINE_OFFSET_SAME_AS_HEIGHT) {
                    continue;
                }
                return child.getLayoutBounds().getMinY() + child.getLayoutY() + offset;
            }
        }
        return super.getBaselineOffset();
    }

    /***
     * It stores the reference to the current child being laid out by its parent.
     * This reference is important to differentiate whether a layout is triggered
     * by its parent or other events.
     */
    private Node currentLayoutChild = null;

    boolean isCurrentLayoutChild(Node node) {
        return node == currentLayoutChild;
    }

    /**
     * Executes a top-down layout pass on the scene graph under this parent.
     *
     * Calling this method while the Parent is doing layout is a no-op.
     */
    public final void layout() {
        // layoutFlag can be accessed or changed during layout processing.
        // Hence we need to cache and reset it before performing layout.
        LayoutFlags flag = layoutFlag;
        setLayoutFlag(LayoutFlags.CLEAN);
        switch (flag) {
        case CLEAN:
            break;
        case NEEDS_LAYOUT:
            if (performingLayout) {
                /* This code is here mainly to avoid infinite loops as layout() is public and the call might be (indirectly) invoked accidentally
                 * while doing the layout.
                 * One example might be an invocation from Group layout bounds recalculation
                 *  (e.g. during the localToScene/localToParent calculation).
                 * The layout bounds will thus return layout bounds that are "old" (i.e. before the layout changes, that are just being done),
                 * which is likely what the code would expect.
                 * The changes will invalidate the layout bounds again however, so the layout bounds query after layout pass will return correct answer.
                 */
                break;
            }
            performingLayout = true;
            layoutChildren();
            // Intended fall-through
        case DIRTY_BRANCH:
            for (int i = 0, max = children.size(); i < max; i++) {
                final Node child = children.get(i);
                currentLayoutChild = child;
                if (child instanceof Parent) {
                    ((Parent) child).layout();
                } else if (child instanceof SubScene) {
                    ((SubScene) child).layoutPass();
                }
            }
            currentLayoutChild = null;
            performingLayout = false;
            break;
        }
    }

    /**
     * Invoked during the layout pass to layout the children in this
     * {@code Parent}. By default it will only set the size of managed,
     * resizable content to their preferred sizes and does not do any node
     * positioning.
     * <p>
     * Subclasses should override this function to layout content as needed.
     */
    protected void layoutChildren() {
        for (int i = 0, max = children.size(); i < max; i++) {
            final Node node = children.get(i);
            currentLayoutChild = node;
            if (node.isResizable() && node.isManaged()) {
                node.autosize();
            }
        }
        currentLayoutChild = null;
    }

    /**
     * This field is managed by the Scene, and set on any node which is the
     * root of a Scene.
     */
    private boolean sceneRoot = false;

    /**
     * Keeps track of whether this node is a layout root. This is updated
     * whenever the sceneRoot field changes, or whenever the managed
     * property changes.
     */
    boolean layoutRoot = false;

    @Override
    final void notifyManagedChanged() {
        layoutRoot = !isManaged() || sceneRoot;
    }

    final boolean isSceneRoot() {
        return sceneRoot;
    }

    /***********************************************************************
     *                                                                     *
     *                         Stylesheet Handling                         *
     *                                                                     *
     **********************************************************************/

    /**
     * A ObservableList of string URLs linking to the stylesheets to use with this scene's
     * contents. For additional information about using CSS with the
     * scene graph, see the <a href="doc-files/cssref.html">CSS Reference
     * Guide</a>.
     */
    private final ObservableList<String> stylesheets = new TrackableObservableList<String>() {
        @Override
        protected void onChanged(Change<String> 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.
     * <p>For additional information about using CSS
     * with the scene graph, see the <a href="doc-files/cssref.html">CSS Reference
     * Guide</a>.</p>
     *
     * @return the list of stylesheets to use with this Parent
     * @since JavaFX 2.1
     */
    public final ObservableList<String> 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<String> doGetAllParentStylesheets() {

        List<String> 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<String>(stylesheets.size());
            }
            for (int n = 0, nMax = stylesheets.size(); n < nMax; n++) {
                list.add(stylesheets.get(n));
            }
        }

        return list;

    }

    /*
     * Note: This method MUST only be called via its accessor method.
     */
    private void doProcessCSS() {

        // Nothing to do...
        if (cssFlag == CssFlags.CLEAN)
            return;

        // RT-29254 - If DIRTY_BRANCH, pass control to Node#processCSS. This avoids calling NodeHelper.processCSS on
        // this node and all of its children when css doesn't need updated, recalculated, or reapplied.
        if (cssFlag == CssFlags.DIRTY_BRANCH) {
            super.processCSS();
            return;
        }

        // Let the super implementation handle CSS for this node
        ParentHelper.superProcessCSS(this);

        // avoid the following call to children.toArray if there are no children
        if (children.isEmpty())
            return;

        //
        // RT-33103
        //
        // It is possible for a child to be removed from children in the middle of
        // the following loop. Iterating over the children may result in an IndexOutOfBoundsException.
        // So a copy is made and the copy is iterated over.
        //
        // Note that we don't want the fail-fast feature of an iterator, not to mention the general iterator overhead.
        //
        final Node[] childArray = children.toArray(new Node[children.size()]);

        // For each child, process CSS
        for (int i = 0; i < childArray.length; i++) {

            final Node child = childArray[i];

            //  If a child no longer has this as its parent, then it is skipped.
            final Parent childParent = child.getParent();
            if (childParent == null || childParent != this)
                continue;

            // If the parent styles are being updated, recalculated or
            // reapplied, then make sure the children get the same treatment.
            // Unless the child is already more dirty than this parent (RT-29074).
            if (CssFlags.UPDATE.compareTo(child.cssFlag) > 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<Node> 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 < max; i++) {
                final Node node = children.get(i);
                if (node.isVisible()) {
                    bounds = getChildTransformedBounds(node, tx, bounds);
                    // if the bounds of the child are invalid, we don't want
                    // to use those in the remaining computations.
                    if (bounds.isEmpty())
                        continue;
                    if (first) {
                        minX = bounds.getMinX();
                        minY = bounds.getMinY();
                        minZ = bounds.getMinZ();
                        maxX = bounds.getMaxX();
                        maxY = bounds.getMaxY();
                        maxZ = bounds.getMaxZ();
                        first = false;
                    } else {
                        minX = Math.min(bounds.getMinX(), minX);
                        minY = Math.min(bounds.getMinY(), minY);
                        minZ = Math.min(bounds.getMinZ(), minZ);
                        maxX = Math.max(bounds.getMaxX(), maxX);
                        maxY = Math.max(bounds.getMaxY(), maxY);
                        maxZ = Math.max(bounds.getMaxZ(), maxZ);
                    }
                }
            }
            // if "first" is still true, then we didn't have any children with
            // non-empty bounds and thus we must return an empty bounds,
            // otherwise we have non-empty bounds so go for it.
            if (first)
                bounds.makeEmpty();
            else
                bounds = bounds.deriveWithNewBounds((float) minX, (float) minY, (float) minZ, (float) maxX,
                        (float) maxY, (float) maxZ);

            return bounds;
        }
    }

    private void setChildDirty(final Node node, final boolean dirty) {
        if (node.boundsChanged == dirty) {
            return;
        }

        node.boundsChanged = dirty;
        if (dirty) {
            if (dirtyChildren != null) {
                dirtyChildren.add(node);
            }
            ++dirtyChildrenCount;
        } else {
            if (dirtyChildren != null) {
                dirtyChildren.remove(node);
            }
            --dirtyChildrenCount;
        }
    }

    private void childIncluded(final Node node) {
        // assert node.isVisible();
        cachedBoundsInvalid = true;
        setChildDirty(node, true);
    }

    // This is called when either the child is actually removed, OR IF IT IS
    // TOGGLED TO BE INVISIBLE. This is because in both cases it needs to be
    // cleared from the state which manages bounds.
    private void childExcluded(final Node node) {
        if (node == left) {
            left = null;
            cachedBoundsInvalid = true;
        }
        if (node == top) {
            top = null;
            cachedBoundsInvalid = true;
        }
        if (node == near) {
            near = null;
            cachedBoundsInvalid = true;
        }
        if (node == right) {
            right = null;
            cachedBoundsInvalid = true;
        }
        if (node == bottom) {
            bottom = null;
            cachedBoundsInvalid = true;
        }
        if (node == far) {
            far = null;
            cachedBoundsInvalid = true;
        }

        setChildDirty(node, false);
    }

    /**
     * Recomputes the bounds from scratch and saves the cached bounds.
     */
    private void recomputeBounds() {
        // fast path for case of no children
        if (children.isEmpty()) {
            cachedBounds.makeEmpty();
            return;
        }

        // fast path for case of 1 child
        if (children.size() == 1) {
            Node node = children.get(0);
            node.boundsChanged = false;
            if (node.isVisible()) {
                cachedBounds = getChildTransformedBounds(node, BaseTransform.IDENTITY_TRANSFORM, cachedBounds);
                top = left = bottom = right = near = far = node;
            } else {
                cachedBounds.makeEmpty();
                // no need to null edge nodes here, it was done in childExcluded
                // top = left = bottom = right = near = far = null;
            }
            return;
        }

        if ((dirtyChildrenCount == 0)
                || !updateCachedBounds(dirtyChildren != null ? dirtyChildren : children, dirtyChildrenCount)) {
            // failed to update cached bounds, recreate them
            createCachedBounds(children);
        }
    }

    private final int LEFT_INVALID = 1;
    private final int TOP_INVALID = 1 << 1;
    private final int NEAR_INVALID = 1 << 2;
    private final int RIGHT_INVALID = 1 << 3;
    private final int BOTTOM_INVALID = 1 << 4;
    private final int FAR_INVALID = 1 << 5;

    private boolean updateCachedBounds(final List<Node> 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<Node> 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 < max; i++) {
            children.get(i).updateBounds();
        }
        super.updateBounds();
    }

    // Note: this marks the currently processed child in terms of transformed bounds. In rare situations like
    // in RT-37879, it might happen that the child bounds will be marked as invalid. Due to optimizations,
    // the invalidation must *always* be propagated to the parent, because the parent with some transformation
    // calls child's getTransformedBounds non-idenitity transform and the child's transformed bounds are thus not validated.
    // This does not apply to the call itself however, because the call will yield the correct result even if something
    // was invalidated during the computation. We can safely ignore such invalidations from that Node in this case
    private Node currentlyProcessedChild;

    private BaseBounds getChildTransformedBounds(Node node, BaseTransform tx, BaseBounds bounds) {
        currentlyProcessedChild = node;
        bounds = node.getTransformedBounds(bounds, tx);
        currentlyProcessedChild = null;
        return bounds;
    }

    /**
     * Called by Node whenever its bounds have changed.
     */
    void childBoundsChanged(Node node) {
        // See comment above at "currentlyProcessedChild" field
        if (node == currentlyProcessedChild) {
            return;
        }

        cachedBoundsInvalid = true;

        // mark the node such that the parent knows that the child's bounds
        // are not in sync with this parent. In this way, when the bounds
        // need to be computed, we'll come back and figure out the new bounds
        // for all the children which have boundsChanged set to true
        setChildDirty(node, true);

        // go ahead and indicate that the geom has changed for this parent,
        // even though once we figure it all out it may be that the bounds
        // have not changed
        NodeHelper.geomChanged(this);
    }

    /**
     * Called by node whenever the visibility of the node changes.
     */
    void childVisibilityChanged(Node node) {
        if (node.isVisible()) {
            childIncluded(node);
        } else {
            childExcluded(node);
        }

        NodeHelper.geomChanged(this);
    }

    /*
     * Note: This method MUST only be called via its accessor method.
     */
    private boolean doComputeContains(double localX, double localY) {
        final Point2D tempPt = TempState.getInstance().point;
        for (int i = 0, max = children.size(); i < max; i++) {
            final Node node = children.get(i);
            tempPt.x = (float) localX;
            tempPt.y = (float) localY;
            try {
                node.parentToLocal(tempPt);
            } catch (NoninvertibleTransformException e) {
                continue;
            }
            if (node.contains(tempPt.x, tempPt.y)) {
                return true;
            }
        }
        return false;
    }

    /** {@inheritDoc} */
    @Override
    public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
        switch (attribute) {
        case CHILDREN:
            return getChildrenUnmodifiable();
        default:
            return super.queryAccessibleAttribute(attribute, parameters);
        }
    }

    void releaseAccessible() {
        for (int i = 0, max = children.size(); i < max; i++) {
            final Node node = children.get(i);
            node.releaseAccessible();
        }
        super.releaseAccessible();
    }

    /**
     * Note: The only user of this method is in unit test: Parent_structure_sync_Test.
     */
    List<Node> test_getRemoved() {
        return removed;
    }

    /**
     * Note: The only user of this method is in unit test:
     * Parent_viewOrderChildren_sync_Test.
     */
    List<Node> test_getViewOrderChildren() {
        return viewOrderChildren;
    }
}