Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene;
027
028import java.util.ArrayList;
029import java.util.HashSet;
030import java.util.List;
031import java.util.Set;
032import javafx.beans.property.ObjectProperty;
033import javafx.beans.property.ReadOnlyBooleanProperty;
034import javafx.beans.property.ReadOnlyBooleanWrapper;
035import javafx.beans.property.SimpleObjectProperty;
036import javafx.collections.FXCollections;
037import javafx.collections.ListChangeListener.Change;
038import javafx.collections.ObservableList;
039import com.sun.javafx.Logging;
040import com.sun.javafx.TempState;
041import com.sun.javafx.Utils;
042import com.sun.javafx.collections.TrackableObservableList;
043import com.sun.javafx.collections.VetoableListDecorator;
044import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
045import com.sun.javafx.css.Selector;
046import com.sun.javafx.css.StyleManager;
047import com.sun.javafx.geom.BaseBounds;
048import com.sun.javafx.geom.PickRay;
049import com.sun.javafx.geom.Point2D;
050import com.sun.javafx.geom.RectBounds;
051import com.sun.javafx.geom.transform.BaseTransform;
052import com.sun.javafx.geom.transform.NoninvertibleTransformException;
053import com.sun.javafx.jmx.MXNodeAlgorithm;
054import com.sun.javafx.jmx.MXNodeAlgorithmContext;
055import sun.util.logging.PlatformLogger;
056import com.sun.javafx.scene.CssFlags;
057import com.sun.javafx.scene.DirtyBits;
058import com.sun.javafx.scene.input.PickResultChooser;
059import com.sun.javafx.scene.traversal.TraversalEngine;
060import com.sun.javafx.sg.PGGroup;
061import com.sun.javafx.sg.PGNode;
062import com.sun.javafx.tk.Toolkit;
063
064import static com.sun.javafx.logging.PulseLogger.*;
065
066/**
067 * The base class for all nodes that have children in the scene graph.
068 * <p>
069 * This class handles all hierarchical scene graph operations, including adding/removing
070 * child nodes, marking branches dirty for layout and rendering, picking,
071 * bounds calculations, and executing the layout pass on each pulse.
072 * <p>
073 * There are two direct concrete Parent subclasses
074 * <ul>
075 * <li>{@link Group} effects and transforms to be applied to a collection of child nodes.</li>
076 * <li>{@link javafx.scene.layout.Region} class for nodes that can be styled with CSS and layout children. </li>
077 * </ul>
078 *
079 */
080public abstract class Parent extends Node {
081    // package private for testing
082    static final int DIRTY_CHILDREN_THRESHOLD = 10;
083
084    // If set to true, generate a warning message whenever adding a node to a
085    // parent if it is currently a child of another parent.
086    private static final boolean warnOnAutoMove = PropertyHelper.getBooleanProperty("javafx.sg.warn");
087
088    /**
089     * Threshold when it's worth to populate list of removed children.
090     */
091    private static final int REMOVED_CHILDREN_THRESHOLD = 20;
092
093    /**
094     * Do not populate list of removed children when its number exceeds threshold,
095     * but mark whole parent dirty.
096     */
097    private boolean removedChildrenExceedsThreshold = false;
098    
099    /**
100     * @treatAsPrivate implementation detail
101     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
102     */
103    @Deprecated
104    @Override public void impl_updatePG() {
105        super.impl_updatePG();
106
107        if (Utils.assertionEnabled()) {
108            List<PGNode> pgnodes = getPGGroup().getChildren();
109            if (pgnodes.size() != pgChildrenSize) {
110                java.lang.System.err.println("*** pgnodes.size() [" + pgnodes.size() + "] != pgChildrenSize [" + pgChildrenSize + "]");
111            }
112        }
113
114        if (impl_isDirty(DirtyBits.PARENT_CHILDREN)) {
115            PGGroup peer = getPGGroup();
116            // Whether a permutation, or children having been added or
117            // removed, we'll want to clear out the PG side starting
118            // from startIdx. We know that everything up to but not
119            // including startIdx is identical between the FX and PG
120            // sides, so we only need to update the remaining portion.
121            peer.clearFrom(startIdx);
122            for (int idx = startIdx; idx < children.size(); idx++) {
123                peer.add(idx, children.get(idx).impl_getPGNode());
124            }
125            if (removedChildrenExceedsThreshold) {
126                peer.markDirty();
127                removedChildrenExceedsThreshold = false;
128            } else {
129                if (removed != null && !removed.isEmpty()) {
130                    for(int i = 0; i < removed.size(); i++) {
131                        peer.addToRemoved(removed.get(i).impl_getPGNode());
132                    }
133                }
134            }
135            if (removed != null) {
136                removed.clear();
137            }
138            pgChildrenSize = children.size();
139            startIdx = pgChildrenSize;
140        }
141
142        if (Utils.assertionEnabled()) validatePG();
143    }
144
145
146    /***********************************************************************
147     *                        Scenegraph Structure                         *
148     *                                                                     *
149     *  Functions and variables related to the scenegraph structure,       *
150     *  modifying the structure, and walking the structure.                *
151     *                                                                     *
152     **********************************************************************/
153
154    // Used to check for duplicate nodes
155    private final Set<Node> childSet = new HashSet<Node>();
156
157    // starting child index from which we need to send the children to the PGGroup
158    private int startIdx = 0;
159
160    // double of children in the PGGroup as of the last update
161    private int pgChildrenSize = 0;
162
163    void validatePG() {
164        boolean assertionFailed = false;
165        List<PGNode> pgnodes = getPGGroup().getChildren();
166        if (pgnodes.size() != children.size()) {
167            java.lang.System.err.println("*** pgnodes.size validatePG() [" + pgnodes.size() + "] != children.size() [" + children.size() + "]");
168            assertionFailed = true;
169        } else {
170            for (int idx = 0; idx < children.size(); idx++) {
171                Node n = children.get(idx);
172                if (n.getParent() != this) {
173                    java.lang.System.err.println("*** this=" + this + " validatePG children[" + idx + "].parent= " + n.getParent());
174                    assertionFailed = true;
175                }
176                if (n.impl_getPGNode() != pgnodes.get(idx)) {
177                    java.lang.System.err.println("*** pgnodes[" + idx + "] validatePG != children[" + idx + "]");
178                    assertionFailed = true;
179                }
180            }
181        }
182        if (assertionFailed) {
183            throw new java.lang.AssertionError("validation of PGGroup children failed");
184        }
185
186    }
187
188    void printSeq(String prefix, List<Node> nodes) {
189        String str = prefix;
190        for (Node nn : nodes) {
191            str += nn + " ";
192        }
193        System.out.println(str);
194    }
195
196    /**
197     * Variable used to avoid executing the body of the on replace trigger on
198     * children. This is specifically used when we know that changes to the
199     * children is going to be valid so as to avoid all the scenegraph surgery
200     * validation routines in the trigger.
201     */
202    private boolean ignoreChildrenTrigger = false;
203
204    // Variable used to indicate that the change to the children ObservableList is
205    // a simple permutation as the result of a toFront or toBack operation.
206    // We can avoid almost all of the processing of the on replace trigger in
207    // this case.
208    private boolean childrenTriggerPermutation = false;
209
210    //accumulates all removed nodes between pulses, for dirty area calculation.
211    private List<Node> removed;
212
213    /**
214     * A ObservableList of child {@code Node}s.
215     * <p>
216     * See the class documentation for {@link Node} for scene graph structure
217     * restrictions on setting a {@link Parent}'s children ObservableList.
218     * If these restrictions are violated by a change to the children ObservableList,
219     * the change is ignored and the previous value of the child ObservableList is
220     * restored.
221     *
222     * {@code <p>Throws AssignToBoundException} if the same node
223     * appears in two different bound ObservableList.
224     *
225     * @defaultValue empty
226     * @since JavaFX 1.3
227     */
228
229    // set to true if either childRemoved or childAdded returns
230    // true. These functions will indicate whether the geom
231    // bounds for the parent have changed
232    private boolean geomChanged;
233    private boolean childrenModified;
234    private final ObservableList<Node> children = new VetoableListDecorator<Node>(new TrackableObservableList<Node>() {
235
236
237        protected void onChanged(Change<Node> c) {
238            // proceed with updating the scene graph
239            if (childrenModified) {
240                unmodifiableManagedChildren = null;
241                boolean relayout = false;
242
243                while (c.next()) {
244                    int from = c.getFrom();
245                    int to = c.getTo();
246                    for (int i = from; i < to; ++i) {
247                        Node n = children.get(i);
248                        if (n.getParent() != null && n.getParent() != Parent.this) {
249                            if (warnOnAutoMove) {
250                                java.lang.System.err.println("WARNING added to a new parent without first removing it from its current");
251                                java.lang.System.err.println("    parent. It will be automatically removed from its current parent.");
252                                java.lang.System.err.println("    node=" + n + " oldparent= " + n.getParent() + " newparent=" + this);
253                            }
254                            n.getParent().children.remove(n);
255                            if (n.isManaged()) {
256                                relayout = true;
257                            }
258                            if (warnOnAutoMove) {
259                                Thread.dumpStack();
260                            }
261                        }
262                    }
263
264                    List<Node> removed = c.getRemoved();
265                    int removedSize = removed.size();
266                    for (int i = 0; i < removedSize; ++i) {
267                        if (removed.get(i).isManaged()) {
268                            relayout = true;
269                        }
270                    }
271
272                    // update the parent and scene for each new node
273                    for (int i = from; i < to; ++i) {
274                        Node node = children.get(i);
275                        if (node.isManaged()) {
276                            relayout = true;
277                        }
278                        node.setParent(Parent.this);
279                        node.setScenes(getScene(), getSubScene());
280                        // assert !node.boundsChanged;
281                        if (node.isVisible()) {
282                            geomChanged = true;
283                            childIncluded(node);
284                        }
285                    }
286                }
287
288                // check to see if the number of children exceeds
289                // DIRTY_CHILDREN_THRESHOLD and dirtyChildren is null.
290                // If so, then we need to create dirtyChildren and
291                // populate it.
292                if (dirtyChildren == null && children.size() > DIRTY_CHILDREN_THRESHOLD) {
293                    dirtyChildren =
294                            new ArrayList<Node>(2 * DIRTY_CHILDREN_THRESHOLD);
295                    // only bother populating children if geom has
296                    // changed, otherwise there is no need
297                    if (dirtyChildrenCount > 0) {
298                        int size = children.size();
299                        for (int i = 0; i < size; ++i) {
300                            Node ch = children.get(i);
301                            if (ch.isVisible() && ch.boundsChanged) {
302                                dirtyChildren.add(ch);
303                            }
304                        }
305                    }
306                }
307
308                if (geomChanged) {
309                    impl_geomChanged();
310                }
311
312                //
313                // Note that the styles of a child do not affect the parent or
314                // its siblings. Thus, it is only necessary to reapply css to
315                // the Node just added and not to this parent and all of its
316                // children. So the following call to impl_reapplyCSS was moved
317                // to Node.parentProperty. The original comment and code were
318                // purposely left here as documentation should there be any
319                // question about how the code used to work and why the change
320                // was made.
321                //
322                // if children have changed then I need to reapply
323                // CSS from this node on down
324//                impl_reapplyCSS();
325                //
326
327                // request layout if a Group subclass has overridden doLayout OR
328                // if one of the new children needs layout, in which case need to ensure
329                // the needsLayout flag is set all the way to the root so the next layout
330                // pass will reach the child.
331                if (relayout) {
332                    requestLayout();
333                }
334            }
335
336            // Note the starting index at which we need to update the
337            // PGGroup on the next update, and mark the children dirty
338            c.reset();
339            c.next();
340            if (startIdx > c.getFrom()) {
341                startIdx = c.getFrom();
342            }
343
344            impl_markDirty(DirtyBits.PARENT_CHILDREN);
345        }
346
347    }) {
348        @Override
349        protected void onProposedChange(final List<Node> newNodes, int[] toBeRemoved) {
350            if (ignoreChildrenTrigger) {
351                return;
352            }
353            if (Parent.this.getScene() != null) {
354                // NOTE: this will throw IllegalStateException if we are on the wrong thread
355                Toolkit.getToolkit().checkFxUserThread();
356            }
357            geomChanged = false;
358
359            long newLength = children.size() + newNodes.size();
360            int removedLength = 0;
361            for (int i = 0; i < toBeRemoved.length; i += 2) {
362                removedLength += toBeRemoved[i + 1] - toBeRemoved[i];
363            }
364            newLength -= removedLength;
365
366            // If the childrenTriggerPermutation flag is set, then we know it
367            // is a simple permutation and no further checking is needed.
368            if (childrenTriggerPermutation) {
369                childrenModified = false;
370                return;
371            }
372
373            // If the childrenTriggerPermutation flag is not set, then we will
374            // check to see whether any element in the ObservableList has changed,
375            // or whether the new ObservableList is a permutation on the existing
376            // ObservableList. Note that even if the childrenModified flag is false,
377            // we still have to check for duplicates. If it is a simple
378            // permutation, we can avoid checking for cycles or other parents.
379            childrenModified = true;
380            if (newLength == childSet.size()) {
381                childrenModified = false;
382                for (int i = newNodes.size() - 1; i >= 0; --i ) {
383                    Node n = newNodes.get(i);
384                    if (!childSet.contains(n)) {
385                        childrenModified = true;
386                        break;
387                    }
388                }
389            }
390
391            // Enforce scene graph invariants, and check for structural errors.
392            //
393            // 1. If a child has been added to this parent more than once,
394            // then it is an error
395            //
396            // 2. If a child is a target of a clip, then it is an error.
397            //
398            // 3. If a node would cause a cycle, then it is an error.
399            //
400            // 4. If a node is null
401            //
402            // Note that if a node is the child of another parent, we will
403            // implicitly remove the node from its former Parent after first
404            // checking for errors.
405
406            // iterate over the nodes that were removed and remove them from
407            // the hash set.
408            for (int i = 0; i < toBeRemoved.length; i += 2) {
409                for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
410                    childSet.remove(children.get(j));
411                }
412            }
413
414            try {
415                if (childrenModified) {
416                    // check individual children before duplication test
417                    // if done in this order, the exception is more specific
418                    for (int i = newNodes.size() - 1; i >= 0; --i ) {
419                        Node node = newNodes.get(i);
420                        if (node == null) {
421                            throw new NullPointerException(
422                                    constructExceptionMessage(
423                                        "child node is null", null));
424                        }
425                        if (node.getClipParent() != null) {
426                            throw new IllegalArgumentException(
427                                    constructExceptionMessage(
428                                        "node already used as a clip", node));
429                        }
430                        if (wouldCreateCycle(Parent.this, node)) {
431                            throw new IllegalArgumentException(
432                                    constructExceptionMessage(
433                                        "cycle detected", node));
434                        }
435                    }
436                }
437
438                childSet.addAll(newNodes);
439                if (childSet.size() != newLength) {
440                    throw new IllegalArgumentException(
441                            constructExceptionMessage(
442                                "duplicate children added", null));
443                }
444            } catch (RuntimeException e) {
445                //Return children to it's original state
446                childSet.clear();
447                childSet.addAll(children);
448
449                // rethrow
450                throw e;
451            }
452
453            // Done with error checking
454
455            if (!childrenModified) {
456                return;
457            }
458
459            // iterate over the nodes that were removed and clear their
460            // parent and scene. Add to them also to removed list for further
461            // dirty regions calculation.
462            if (removed == null) {
463                removed = new ArrayList<Node>();
464            }
465            if (removed.size() + removedLength > REMOVED_CHILDREN_THRESHOLD) {
466                //do not populate too many children in removed list
467                removedChildrenExceedsThreshold = true;
468            }
469            for (int i = 0; i < toBeRemoved.length; i += 2) {
470                for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
471                    Node old = children.get(j);
472                    if (dirtyChildren != null) {
473                        dirtyChildren.remove(old);
474                    }
475                    if (old.isVisible()) {
476                        geomChanged = true;
477                        childExcluded(old);
478                    }
479                    if (old.getParent() == Parent.this) {
480                        old.setParent(null);
481                        old.setScenes(null, null);
482                    }
483                    if (!removedChildrenExceedsThreshold) {
484                        removed.add(old);
485                    }
486                }
487            }
488        }
489
490        private String constructExceptionMessage(
491                String cause, Node offendingNode) {
492            final StringBuilder sb = new StringBuilder("Children: ");
493            sb.append(cause);
494            sb.append(": parent = ").append(Parent.this);
495            if (offendingNode != null) {
496                sb.append(", node = ").append(offendingNode);
497            }
498
499            return sb.toString();
500        }
501    };
502
503    /**
504     * A constant reference to an unmodifiable view of the children, such that every time
505     * we ask for an unmodifiable list of children, we don't actually create a new
506     * collection and return it. The memory overhead is pretty lightweight compared
507     * to all the garbage we would otherwise generate.
508     */
509    private final ObservableList<Node> unmodifiableChildren =
510            FXCollections.unmodifiableObservableList(children);
511
512    /**
513     * A cached reference to the unmodifiable managed children of this Parent. This is
514     * created whenever first asked for, and thrown away whenever children are added
515     * or removed or when their managed state changes. This could be written
516     * differently, such that this list is essentially a filtered copy of the
517     * main children, but that additional overhead might not be worth it.
518     */
519    private List<Node> unmodifiableManagedChildren = null;
520
521    /**
522     * Gets the list of children of this {@code Parent}.
523     *
524     * <p>
525     * See the class documentation for {@link Node} for scene graph structure
526     * restrictions on setting a {@link Parent}'s children list.
527     * If these restrictions are violated by a change to the list of children,
528     * the change is ignored and the previous value of the children list is
529     * restored. An {@link IllegalArgumentException} is thrown in this case.
530     *
531     * <p>
532     * If this {@link Parent} node is attached to a {@link Scene}, then its
533     * list of children must only be modified on the JavaFX Application Thread.
534     * An {@link IllegalStateException} is thrown if this restriction is
535     * violated.
536     *
537     * <p>
538     * Note to subclasses: if you override this method, you must return from
539     * your implementation the result of calling this super method. The actual
540     * list instance returned from any getChildren() implementation must be
541     * the list owned and managed by this Parent. The only typical purpose
542     * for overriding this method is to promote the method to be public.
543     *
544     * @return the list of children of this {@code Parent}.
545     */
546    protected ObservableList<Node> getChildren() {
547        return children;
548    }
549
550    /**
551     * Gets the list of children of this {@code Parent} as a read-only
552     * list.
553     *
554     * @return read-only access to this parent's children ObservableList
555     */
556    @ReturnsUnmodifiableCollection
557    public ObservableList<Node> getChildrenUnmodifiable() {
558        return unmodifiableChildren;
559    }
560
561    /**
562     * Gets the list of all managed children of this {@code Parent}.
563     *
564     * @param <E> the type of the children nodes
565     * @return list of all managed children in this parent
566     */
567    @ReturnsUnmodifiableCollection
568    protected <E extends Node> List<E> getManagedChildren() {
569        if (unmodifiableManagedChildren == null) {
570            unmodifiableManagedChildren = new ArrayList<Node>();
571            for (int i=0, max=children.size(); i<max; i++) {
572                Node e = children.get(i);
573                if (e.isManaged()) {
574                    unmodifiableManagedChildren.add(e);
575                }
576            }
577        }
578        return (List<E>)unmodifiableManagedChildren;
579    }
580
581    /**
582     * Called by Node whenever its managed state may have changed, this
583     * method will cause the view of managed children to be updated
584     * such that it properly includes or excludes this child.
585     */
586    final void managedChildChanged() {
587        requestLayout();
588        unmodifiableManagedChildren = null;
589    }
590
591    // implementation of Node.toFront function
592    final void impl_toFront(Node node) {
593        if (Utils.assertionEnabled()) {
594            if (!childSet.contains(node)) {
595                throw new java.lang.AssertionError(
596                        "specified node is not in the list of children");
597            }
598        }
599
600        if (children.get(children.size() - 1) != node) {
601            childrenTriggerPermutation = true;
602            try {
603                children.remove(node);
604                children.add(node);
605            } finally {
606                childrenTriggerPermutation = false;
607            }
608        }
609    }
610
611    // implementation of Node.toBack function
612    final void impl_toBack(Node node) {
613        if (Utils.assertionEnabled()) {
614            if (!childSet.contains(node)) {
615                throw new java.lang.AssertionError(
616                        "specified node is not in the list of children");
617            }
618        }
619
620        if (children.get(0) != node) {
621            childrenTriggerPermutation = true;
622            try {
623                children.remove(node);
624                children.add(0, node);
625            } finally {
626                childrenTriggerPermutation = false;
627            }
628        }
629    }
630
631    @Override
632    void scenesChanged(final Scene newScene, final SubScene newSubScene,
633                       final Scene oldScene, final SubScene oldSubScene) {
634        for (int i=0; i<children.size(); i++) {
635            children.get(i).setScenes(newScene, newSubScene);
636        }
637
638        // If this node was in the old scene's dirty layout
639        // list, then remove it from that list so that it is
640        // not processed on the next pulse
641        final boolean awaitingLayout = isNeedsLayout();
642
643        sceneRoot = (newSubScene != null && newSubScene.getRoot() == this) ||
644                    (newScene != null && newScene.getRoot() == this);
645        layoutRoot = !isManaged() || sceneRoot;
646
647        if (awaitingLayout) {
648            boolean sceneChanged = oldScene != newScene;
649            if (oldScene != null && sceneChanged) {
650                oldScene.removeFromDirtyLayoutList(this);
651            }
652            // If this node is dirty and the new scene or subScene is not null
653            // then add this node to the new scene's dirty list
654            if (newScene != null && layoutRoot) {
655                if (newSubScene != null) {
656                    newSubScene.setDirtyLayout(this);
657                }
658                if (sceneChanged) {
659                    newScene.addToDirtyLayoutList(this);
660                }
661            }
662        }
663    }
664
665    @Override
666    void setDerivedDepthTest(boolean value) {
667        super.setDerivedDepthTest(value);
668
669        for (int i=0, max=children.size(); i<max; i++) {
670            final Node node = children.get(i);
671            node.computeDerivedDepthTest();
672        }
673    }
674
675    /**
676     * @treatAsPrivate implementation detail
677     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
678     */
679    @Deprecated
680    @Override protected void impl_pickNodeLocal(PickRay pickRay, PickResultChooser result) {
681
682        double boundsDistance = impl_intersectsBounds(pickRay);
683
684        if (!Double.isNaN(boundsDistance)) {
685            for (int i = children.size()-1; i >= 0; i--) {
686                children.get(i).impl_pickNode(pickRay, result);
687                if (result.isClosed()) {
688                    return;
689                }
690            }
691
692            if (isPickOnBounds()) {
693                result.offer(this, boundsDistance, PickResultChooser.computePoint(pickRay, boundsDistance));
694            }
695        }
696    }
697
698    @Override boolean isConnected() {
699        return super.isConnected() || sceneRoot;
700    }
701
702    @Override public Node lookup(String selector) {
703        Node n = super.lookup(selector);
704        if (n == null) {
705            for (int i=0, max=children.size(); i<max; i++) {
706                final Node node = children.get(i);
707                n = node.lookup(selector);
708                if (n != null) return n;
709            }
710        }
711        return n;
712    }
713
714    /**
715     * Please Note: This method should never create the results set,
716     * let the Node class implementation do this!
717     */
718    @Override List<Node> lookupAll(Selector selector, List<Node> results) {
719        results = super.lookupAll(selector, results);
720        for (int i=0, max=children.size(); i<max; i++) {
721            final Node node = children.get(i);
722            results = node.lookupAll(selector, results);
723        }
724        return results;
725    }
726
727    /** @treatAsPrivate implementation detail */
728    private javafx.beans.property.ObjectProperty<TraversalEngine> impl_traversalEngine;
729
730    /**
731     * @treatAsPrivate implementation detail
732     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
733     */
734    // SB-dependency: RT-21209 has been filed to track this
735    @Deprecated
736    public final void setImpl_traversalEngine(TraversalEngine value) {
737        impl_traversalEngineProperty().set(value);
738    }
739
740    /**
741     * @treatAsPrivate implementation detail
742     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
743     */
744    @Deprecated
745    public final TraversalEngine getImpl_traversalEngine() {
746        return impl_traversalEngine == null ? null : impl_traversalEngine.get();
747    }
748
749    /**
750     * @treatAsPrivate implementation detail
751     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
752     */
753    @Deprecated
754    public final ObjectProperty<TraversalEngine> impl_traversalEngineProperty() {
755        if (impl_traversalEngine == null) {
756            impl_traversalEngine =
757                    new SimpleObjectProperty<TraversalEngine>(
758                            this, "impl_traversalEngine");
759        }
760        return impl_traversalEngine;
761    }
762
763    /***********************************************************************
764     *                               Layout                                *
765     *                                                                     *
766     *  Functions and variables related to the layout scheme used by       *
767     *  JavaFX. Includes both public and private API.                      *
768     *                                                                     *
769     **********************************************************************/
770    /**
771     * Indicates that this Node and its subnodes requires a layout pass on
772     * the next pulse.
773     */
774    private ReadOnlyBooleanWrapper needsLayout = new ReadOnlyBooleanWrapper(this, "needsLayout", true);
775
776    protected final void setNeedsLayout(boolean value) {
777        needsLayout.set(value);
778    }
779
780    public final boolean isNeedsLayout() {
781        return needsLayout.get();
782    }
783
784    public final ReadOnlyBooleanProperty needsLayoutProperty() {
785        return needsLayout.getReadOnlyProperty();
786    }
787
788    /**
789     * This package levelis used only by Node. It is set to true while
790     * the layout() function is processing and set to false on the conclusion.
791     * It is used by the Node to decide whether to perform CSS updates
792     * synchronously or asynchronously.
793     */
794    boolean performingLayout = false;
795
796    private boolean sizeCacheClear = true;
797    private double prefWidthCache = -1;
798    private double prefHeightCache = -1;
799    private double minWidthCache = -1;
800    private double minHeightCache = -1;
801
802    /**
803     * Requests a layout pass to be performed before the next scene is
804     * rendered. This is batched up asynchronously to happen once per
805     * "pulse", or frame of animation.
806     * <p>
807     * If this parent is either a layout root or unmanaged, then it will be
808     * added directly to the scene's dirty layout list, otherwise requestLayout
809     * will be invoked on its parent.
810     */
811    public void requestLayout() {
812        if (!isNeedsLayout()) {
813            prefWidthCache = -1;
814            prefHeightCache = -1;
815            minWidthCache = -1;
816            minHeightCache = -1;
817            PlatformLogger logger = Logging.getLayoutLogger();
818            if (logger.isLoggable(PlatformLogger.FINER)) {
819                logger.finer(this.toString());
820            }
821
822            setNeedsLayout(true);
823            if (layoutRoot) {
824                final Scene scene = getScene();
825                final SubScene subScene = getSubScene();
826                if (subScene != null) {
827                    if (logger.isLoggable(PlatformLogger.FINER)) {
828                        logger.finer(this.toString()+" layoutRoot added to SubScene dirty layout list");
829                    }
830                    subScene.setDirtyLayout(this);
831                }
832                if (scene != null) {
833                    if (logger.isLoggable(PlatformLogger.FINER)) {
834                        logger.finer(this.toString()+" layoutRoot added to scene dirty layout list");
835                    }
836                    scene.addToDirtyLayoutList(this);
837                }
838            } else {
839                final Parent parent = getParent();
840                if (parent != null) {
841                    parent.requestLayout();
842                }
843            }
844        } else {
845            clearSizeCache();
846        }
847    }
848
849    void clearSizeCache() {
850        if (sizeCacheClear) {
851            return;
852        }
853        sizeCacheClear = true;
854        prefWidthCache = -1;
855        prefHeightCache = -1;
856        minWidthCache = -1;
857        minHeightCache = -1;
858        if (!layoutRoot) {
859            final Parent parent = getParent();
860            if (parent != null) {
861                parent.clearSizeCache();
862            }
863        }
864    }
865
866    @Override public double prefWidth(double height) {
867        if (height == -1) {
868            if (prefWidthCache == -1) {
869                prefWidthCache = computePrefWidth(-1);
870                sizeCacheClear = false;
871            }
872            return prefWidthCache;
873        } else {
874            return computePrefWidth(height);
875        }
876    }
877
878    @Override public double prefHeight(double width) {
879        if (width == -1) {
880            if (prefHeightCache == -1) {
881                prefHeightCache = computePrefHeight(-1);
882                sizeCacheClear = false;
883            }
884            return prefHeightCache;
885        } else {
886            return computePrefHeight(width);
887        }
888    }
889
890    @Override public double minWidth(double height) {
891        if (height == -1) {
892            if (minWidthCache == -1) {
893                minWidthCache = computeMinWidth(-1);
894                sizeCacheClear = false;
895            }
896            return minWidthCache;
897        } else {
898            return computeMinWidth(height);
899        }
900    }
901
902    @Override public double minHeight(double width) {
903        if (width == -1) {
904            if (minHeightCache == -1) {
905                minHeightCache = computeMinHeight(-1);
906                sizeCacheClear = false;
907            }
908            return minHeightCache;
909        } else {
910            return computeMinHeight(width);
911        }
912    }
913
914    // PENDING_DOC_REVIEW
915    /**
916     * Calculates the preferred width of this {@code Parent}. The default
917     * implementation calculates this width as the width of the area occupied
918     * by its managed children when they are positioned at their
919     * current positions at their preferred widths.
920     *
921     * @param height the height that should be used if preferred width depends
922     *      on it
923     * @return the calculated preferred width
924     */
925    protected double computePrefWidth(double height) {
926        double minX = 0;
927        double maxX = 0;
928        for (int i=0, max=children.size(); i<max; i++) {
929            Node node = children.get(i);
930            if (node.isManaged()) {
931                final double x = node.getLayoutBounds().getMinX() + node.getLayoutX();
932                minX = Math.min(minX, x);
933                maxX = Math.max(maxX, x + boundedSize(node.prefWidth(-1), node.minWidth(-1), node.maxWidth(-1)));
934            }
935        }
936        return maxX - minX;
937    }
938
939    // PENDING_DOC_REVIEW
940    /**
941     * Calculates the preferred height of this {@code Parent}. The default
942     * implementation calculates this height as the height of the area occupied
943     * by its managed children when they are positioned at their current
944     * positions at their preferred heights.
945     *
946     * @param width the width that should be used if preferred height depends
947     *      on it
948     * @return the calculated preferred height
949     */
950    protected double computePrefHeight(double width) {
951        double minY = 0;
952        double maxY = 0;
953        for (int i=0, max=children.size(); i<max; i++) {
954            Node node = children.get(i);
955            if (node.isManaged()) {
956                final double y = node.getLayoutBounds().getMinY() + node.getLayoutY();
957                minY = Math.min(minY, y);
958                maxY = Math.max(maxY, y + boundedSize(node.prefHeight(-1), node.minHeight(-1), node.maxHeight(-1)));
959            }
960        }
961        return maxY - minY;
962    }
963
964    /**
965     * Calculates the minimum width of this {@code Parent}. The default
966     * implementation simply returns the pref width.
967     *
968     * @param height the height that should be used if min width depends
969     *      on it
970     * @return the calculated min width
971     */
972    protected double computeMinWidth(double height) {
973        return prefWidth(height);
974    }
975
976    // PENDING_DOC_REVIEW
977    /**
978     * Calculates the min height of this {@code Parent}. The default
979     * implementation simply returns the pref height;
980     *
981     * @param width the width that should be used if min height depends
982     *      on it
983     * @return the calculated min height
984     */
985    protected double computeMinHeight(double width) {
986        return prefHeight(width);
987    }
988
989    /**
990     * Calculates the baseline offset based on the first managed child. If there
991     * is no such child, returns {@link Node#getBaselineOffset()}.
992     *
993     * @return baseline offset
994     */
995    @Override public double getBaselineOffset() {
996        for (int i=0, max=children.size(); i<max; i++) {
997            final Node child = children.get(i);
998            if (child.isManaged()) {
999                return child.getLayoutBounds().getMinY() + child.getLayoutY() + child.getBaselineOffset();
1000            }
1001        }
1002        return super.getBaselineOffset();
1003    }
1004
1005    /**
1006     * Executes a top-down layout pass on the scene graph under this parent.
1007     */
1008    public final void layout() {
1009        if (isNeedsLayout()) {
1010            if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.fxIncrementCounter("Parent#layout() on dirty Node");
1011            performingLayout = true;
1012
1013            PlatformLogger logger = Logging.getLayoutLogger();
1014            if (logger.isLoggable(PlatformLogger.FINE)) {
1015                logger.fine(this+" size: "+
1016                        getLayoutBounds().getWidth()+" x "+getLayoutBounds().getHeight());
1017            }
1018
1019            // layout the children in this parent.
1020            layoutChildren();
1021            // clear flag before recursing down so requestLayout calls arn't swallowed
1022            setNeedsLayout(false);
1023
1024            // Perform layout on each child, hoping it has random access performance!
1025            for (int i=0, max=children.size(); i<max; i++) {
1026                final Node child = children.get(i);
1027                if (child instanceof Parent) {
1028                    ((Parent) child).layout();
1029                }
1030            }
1031            performingLayout = false;
1032        } else {
1033            if (PULSE_LOGGING_ENABLED) PULSE_LOGGER.fxIncrementCounter("Parent#layout() on clean Node");
1034        }
1035    }
1036
1037    /**
1038     * Invoked during the layout pass to layout the children in this
1039     * {@code Parent}. By default it will only set the size of managed,
1040     * resizable content to their preferred sizes and does not do any node
1041     * positioning.
1042     * <p>
1043     * Subclasses should override this function to layout content as needed.
1044     */
1045    protected void layoutChildren() {
1046        for (int i=0, max=children.size(); i<max; i++) {
1047            final Node node = children.get(i);
1048            if (node.isResizable() && node.isManaged()) {
1049                node.autosize();
1050            }
1051        }
1052    }
1053
1054    /**
1055     * This field is managed by the Scene, and set on any node which is the
1056     * root of a Scene.
1057     */
1058    private boolean sceneRoot = false;
1059
1060    /**
1061     * Keeps track of whether this node is a layout root. This is updated
1062     * whenever the sceneRoot field changes, or whenever the managed
1063     * property changes.
1064     */
1065    private boolean layoutRoot = false;
1066    @Override final void notifyManagedChanged() {
1067        layoutRoot = !isManaged() || sceneRoot;
1068    }
1069
1070    /***********************************************************************
1071     *                                                                     *
1072     *                         Stylesheet Handling                         *
1073     *                                                                     *
1074     **********************************************************************/
1075
1076    
1077    /**
1078     * A ObservableList of string URLs linking to the stylesheets to use with this scene's
1079     * contents. For additional information about using CSS with the
1080     * scene graph, see the <a href="doc-files/cssref.html">CSS Reference
1081     * Guide</a>.
1082     */
1083    private final ObservableList<String> stylesheets = new TrackableObservableList<String>() {
1084        @Override
1085        protected void onChanged(Change<String> c) {
1086            final Scene scene = getScene();
1087            if (scene != null) {
1088
1089                // Notify the StyleManager if stylesheets change. This Parent's
1090                // styleManager will get recreated in impl_processCSS.
1091                StyleManager.getInstance().stylesheetsChanged(Parent.this, c);
1092                
1093                // RT-9784 - if stylesheet is removed, reset styled properties to 
1094                // their initial value.
1095                c.reset();
1096                while(c.next()) {
1097                    if (c.wasRemoved() == false) {
1098                        continue;
1099                    }
1100                    break; // no point in resetting more than once...
1101                }
1102                
1103                impl_reapplyCSS();
1104            }
1105        }
1106    };
1107
1108    /**
1109     * Gets an observable list of string URLs linking to the stylesheets to use 
1110     * with this Parent's contents. For additional information about using CSS 
1111     * with the scene graph, see the <a href="doc-files/cssref.html">CSS Reference
1112     * Guide</a>.
1113     *
1114     * @return the list of stylesheets to use with this Parent
1115     */
1116    public final ObservableList<String> getStylesheets() { return stylesheets; }
1117    
1118    /**
1119     * This method recurses up the parent chain until parent is null. As the
1120     * stack unwinds, if the Parent has stylesheets, they are added to the
1121     * list.
1122     * 
1123     * It is possible to override this method to stop the recursion. This allows
1124     * a Parent to have a set of stylesheets distinct from its Parent. 
1125     * 
1126     * @treatAsPrivate implementation detail
1127     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1128     */
1129    @Deprecated // SB-dependency: RT-21247 has been filed to track this
1130    public /* Do not make this final! */ List<String> impl_getAllParentStylesheets() {
1131        
1132        List<String> list = null;
1133        final Parent myParent = getParent();
1134        if (myParent != null) {
1135
1136            //
1137            // recurse so that stylesheets of Parents closest to the root are 
1138            // added to the list first. The ensures that declarations for 
1139            // stylesheets further down the tree (closer to the leaf) have
1140            // a higer ordinal in the cascade.
1141            //
1142            list = myParent.impl_getAllParentStylesheets();
1143        }
1144        
1145        if (stylesheets != null && stylesheets.isEmpty() == false) {
1146            if (list == null) {
1147                list = new ArrayList<String>(stylesheets.size());
1148            }
1149            for (int n=0,nMax=stylesheets.size(); n<nMax; n++) {
1150                list.add(stylesheets.get(n));
1151            }
1152        }
1153        
1154        return list;
1155        
1156    }
1157    
1158    /**
1159     * @treatAsPrivate implementation detail
1160     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1161     */
1162    @Deprecated
1163    @Override protected void impl_processCSS() {
1164
1165        // Nothing to do...
1166        if (cssFlag == CssFlags.CLEAN) return;
1167
1168        // RT-29254 - If DIRTY_BRANCH, pass control to Node#processCSS. This avoids calling impl_processCSS on
1169        // this node and all of its children when css doesn't need updated, recalculated, or reapplied.
1170        if (cssFlag == CssFlags.DIRTY_BRANCH) {
1171            super.processCSS();
1172            return;
1173        }
1174        // remember the flag we started with since super.impl_processCSS
1175        // resets it to CLEAN and we need it for setting children.
1176        CssFlags flag = cssFlag;
1177
1178        // Let the super implementation handle CSS for this node
1179        super.impl_processCSS();
1180        
1181        // For each child, process CSS
1182        for (int i=0, max=children.size(); i<max; i++) {
1183            final Node child = children.get(i);
1184            
1185            // If the parent styles are being updated, recalculated or 
1186            // reapplied, then make sure the children get the same treatment.
1187            // Unless the child is already more dirty than this parent (RT-29074).
1188            if(flag.compareTo(child.cssFlag) > 0) {
1189                child.cssFlag = flag;
1190            }
1191            child.impl_processCSS();
1192        }
1193    }
1194
1195    /***********************************************************************
1196     *                               Misc                                  *
1197     *                                                                     *
1198     *  Initialization and other functions                                 *
1199     *                                                                     *
1200     **********************************************************************/
1201
1202
1203    /**
1204     * Constructs a new {@code Parent}.
1205     */
1206    protected Parent() { 
1207    }
1208
1209    /**
1210     * @treatAsPrivate implementation detail
1211     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1212     */
1213    @Deprecated
1214    protected @Override PGNode impl_createPGNode() {
1215        return Toolkit.getToolkit().createPGGroup();
1216    }
1217
1218    PGGroup getPGGroup() {
1219        return ((PGGroup) impl_getPGNode());
1220    }
1221
1222    @Override
1223    void nodeResolvedOrientationChanged() {
1224        for (int i = 0, max = children.size(); i < max; ++i) {
1225            children.get(i).parentResolvedOrientationInvalidated();
1226        }
1227    }
1228
1229    /***************************************************************************
1230     *                                                                         *
1231     *                         Bounds Computations                             *
1232     *                                                                         *
1233     *  This code originated in GroupBoundsHelper (part of javafx-sg-common)   *
1234     *  but has been ported here to the FX side since we cannot rely on the PG *
1235     *  side for computing the bounds (due to the decoupling of the two        *
1236     *  scenegraphs for threading and other purposes).                         *
1237     *                                                                         *
1238     *  Unfortunately, we cannot simply reuse GroupBoundsHelper without some  *
1239     *  major (and hacky) modification due to the fact that GroupBoundsHelper  *
1240     *  relies on PG state and we need to do similar things here that rely on  *
1241     *  core scenegraph state. Unfortunately, that means we made a port.       *
1242     *                                                                         *
1243     **************************************************************************/
1244
1245    private BaseBounds tmp = new RectBounds();
1246
1247    /**
1248     * The cached bounds for the Group. If the cachedBounds are invalid
1249     * then we have no history of what the bounds are, or were.
1250     */
1251    private BaseBounds cachedBounds = new RectBounds();
1252
1253    /**
1254     * Indicates that the cachedBounds is invalid (or old) and need to be recomputed.
1255     * If cachedBoundsInvalid is true and dirtyChildrenCount is non-zero,
1256     * then when we recompute the cachedBounds we can consider the
1257     * values in cachedBounds to represent the last valid bounds for the group.
1258     * This is useful for several fast paths.
1259     */
1260    private boolean cachedBoundsInvalid;
1261
1262    /**
1263     * The number of dirty children which bounds haven't been incorporated
1264     * into the cached bounds yet. Can be used even when dirtyChildren is null.
1265     */
1266    private int dirtyChildrenCount;
1267
1268    /**
1269     * This set is used to track all of the children of this group which are
1270     * dirty. It is only used in cases where the number of children is > some
1271     * value (currently 10). For very wide trees, this can provide a very
1272     * important speed boost. For the sake of memory consumption, this is
1273     * null unless the number of children ever crosses the threshold where
1274     * it will be activated.
1275     */
1276    private ArrayList<Node> dirtyChildren;
1277
1278    private Node top;
1279    private Node left;
1280    private Node bottom;
1281    private Node right;
1282    private Node near;
1283    private Node far;
1284
1285    /**
1286     * @treatAsPrivate implementation detail
1287     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1288     */
1289    @Deprecated
1290    @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
1291        // If we have no children, our bounds are invalid
1292        if (children.isEmpty()) {
1293            return bounds.makeEmpty();
1294        }
1295
1296        if (tx.isTranslateOrIdentity()) {
1297            // this is a transform which is only doing translations, or nothing
1298            // at all (no scales, rotates, or shears)
1299            // so in this case we can easily use the cached bounds
1300            if (cachedBoundsInvalid) {
1301                recomputeBounds();
1302
1303                if (dirtyChildren != null) {
1304                    dirtyChildren.clear();
1305                }
1306                cachedBoundsInvalid = false;
1307                dirtyChildrenCount = 0;
1308            }
1309            if (!tx.isIdentity()) {
1310                bounds = bounds.deriveWithNewBounds((float)(cachedBounds.getMinX() + tx.getMxt()),
1311                                 (float)(cachedBounds.getMinY() + tx.getMyt()),
1312                                 (float)(cachedBounds.getMinZ() + tx.getMzt()),
1313                                 (float)(cachedBounds.getMaxX() + tx.getMxt()),
1314                                 (float)(cachedBounds.getMaxY() + tx.getMyt()),
1315                                 (float)(cachedBounds.getMaxZ() + tx.getMzt()));
1316            } else {
1317                bounds = bounds.deriveWithNewBounds(cachedBounds);
1318            }
1319
1320            return bounds;
1321        } else {
1322            // there is a scale, shear, or rotation happening, so need to
1323            // do the full transform!
1324            double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE;
1325            double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE, maxZ = Double.MIN_VALUE;
1326            boolean first = true;
1327            for (int i=0, max=children.size(); i<max; i++) {
1328                final Node node = children.get(i);
1329                if (node.isVisible()) {
1330                    bounds = node.getTransformedBounds(bounds, tx);
1331                    // if the bounds of the child are invalid, we don't want
1332                    // to use those in the remaining computations.
1333                    if (bounds.isEmpty()) continue;
1334                    if (first) {
1335                        minX = bounds.getMinX();
1336                        minY = bounds.getMinY();
1337                        minZ = bounds.getMinZ();
1338                        maxX = bounds.getMaxX();
1339                        maxY = bounds.getMaxY();
1340                        maxZ = bounds.getMaxZ();
1341                        first = false;
1342                    } else {
1343                        minX = Math.min(bounds.getMinX(), minX);
1344                        minY = Math.min(bounds.getMinY(), minY);
1345                        minZ = Math.min(bounds.getMinZ(), minZ);
1346                        maxX = Math.max(bounds.getMaxX(), maxX);
1347                        maxY = Math.max(bounds.getMaxY(), maxY);
1348                        maxZ = Math.max(bounds.getMaxZ(), maxZ);
1349                    }
1350                }
1351            }
1352            // if "first" is still true, then we didn't have any children with
1353            // non-empty bounds and thus we must return an empty bounds,
1354            // otherwise we have non-empty bounds so go for it.
1355            if (first)
1356                bounds.makeEmpty();
1357            else
1358                bounds = bounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
1359                        (float)maxX, (float)maxY, (float)maxZ);
1360
1361            return bounds;
1362        }
1363    }
1364
1365    private void setChildDirty(final Node node, final boolean dirty) {
1366        if (node.boundsChanged == dirty) {
1367            return;
1368        }
1369
1370        node.boundsChanged = dirty;
1371        if (dirty) {
1372            if (dirtyChildren != null) {
1373                dirtyChildren.add(node);
1374            }
1375            ++dirtyChildrenCount;
1376        } else {
1377            if (dirtyChildren != null) {
1378                dirtyChildren.remove(node);
1379            }
1380            --dirtyChildrenCount;
1381        }
1382    }
1383
1384    private void childIncluded(final Node node) {
1385        // assert node.isVisible();
1386        cachedBoundsInvalid = true;
1387        setChildDirty(node, true);
1388    }
1389
1390    // This is called when either the child is actually removed, OR IF IT IS
1391    // TOGGLED TO BE INVISIBLE. This is because in both cases it needs to be
1392    // cleared from the state which manages bounds.
1393    private void childExcluded(final Node node) {
1394        if (node == left) {
1395            left = null;
1396            cachedBoundsInvalid = true;
1397        }
1398        if (node == top) {
1399            top = null;
1400            cachedBoundsInvalid = true;
1401        }
1402        if (node == near) {
1403            near = null;
1404            cachedBoundsInvalid = true;
1405        }
1406        if (node == right) {
1407            right = null;
1408            cachedBoundsInvalid = true;
1409        }
1410        if (node == bottom) {
1411            bottom = null;
1412            cachedBoundsInvalid = true;
1413        }
1414        if (node == far) {
1415            far = null;
1416            cachedBoundsInvalid = true;
1417        }
1418
1419        setChildDirty(node, false);
1420    }
1421
1422    /**
1423     * Recomputes the bounds from scratch and saves the cached bounds.
1424     */
1425    private void recomputeBounds() {
1426        // fast path for case of no children
1427        if (children.isEmpty()) {
1428            cachedBounds.makeEmpty();
1429            return;
1430        }
1431
1432        // fast path for case of 1 child
1433        if (children.size() == 1) {
1434            Node node = children.get(0);
1435            node.boundsChanged = false;
1436            if (node.isVisible()) {
1437                cachedBounds = node.getTransformedBounds(
1438                                        cachedBounds,
1439                                        BaseTransform.IDENTITY_TRANSFORM);
1440                top = left = bottom = right = near = far = node;
1441            } else {
1442                cachedBounds.makeEmpty();
1443                // no need to null edge nodes here, it was done in childExcluded
1444                // top = left = bottom = right = near = far = null;
1445            }
1446            return;
1447        }
1448
1449        if ((dirtyChildrenCount == 0) ||
1450                !updateCachedBounds(dirtyChildren != null
1451                                        ? dirtyChildren : children,
1452                                    dirtyChildrenCount)) {
1453            // failed to update cached bounds, recreate them
1454            createCachedBounds(children);
1455        }
1456    }
1457
1458    private final int LEFT_INVALID = 1;
1459    private final int TOP_INVALID = 1 << 1;
1460    private final int NEAR_INVALID = 1 << 2;
1461    private final int RIGHT_INVALID = 1 << 3;
1462    private final int BOTTOM_INVALID = 1 << 4;
1463    private final int FAR_INVALID = 1 << 5;
1464
1465    private boolean updateCachedBounds(final List<Node> dirtyNodes,
1466                                       int remainingDirtyNodes) {
1467        // fast path for untransformed bounds calculation
1468        if (cachedBounds.isEmpty()) {
1469            createCachedBounds(dirtyNodes);
1470            return true;
1471        }
1472
1473        int invalidEdges = 0;
1474
1475        if ((left == null) || left.boundsChanged) {
1476            invalidEdges |= LEFT_INVALID;
1477        }
1478        if ((top == null) || top.boundsChanged) {
1479            invalidEdges |= TOP_INVALID;
1480        }
1481        if ((near == null) || near.boundsChanged) {
1482            invalidEdges |= NEAR_INVALID;
1483        }
1484        if ((right == null) || right.boundsChanged) {
1485            invalidEdges |= RIGHT_INVALID;
1486        }
1487        if ((bottom == null) || bottom.boundsChanged) {
1488            invalidEdges |= BOTTOM_INVALID;
1489        }
1490        if ((far == null) || far.boundsChanged) {
1491            invalidEdges |= FAR_INVALID;
1492        }
1493
1494        // These indicate the bounds of the Group as computed by this
1495        // function
1496        float minX = cachedBounds.getMinX();
1497        float minY = cachedBounds.getMinY();
1498        float minZ = cachedBounds.getMinZ();
1499        float maxX = cachedBounds.getMaxX();
1500        float maxY = cachedBounds.getMaxY();
1501        float maxZ = cachedBounds.getMaxZ();
1502
1503        // this checks the newly added nodes first, so if dirtyNodes is the
1504        // whole children list, we can end early
1505        for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) {
1506            final Node node = dirtyNodes.get(i);
1507            if (node.boundsChanged) {
1508                // assert node.isVisible();
1509                node.boundsChanged = false;
1510                --remainingDirtyNodes;
1511                tmp = node.getTransformedBounds(
1512                               tmp, BaseTransform.IDENTITY_TRANSFORM);
1513                if (!tmp.isEmpty()) {
1514                    float tmpx = tmp.getMinX();
1515                    float tmpy = tmp.getMinY();
1516                    float tmpz = tmp.getMinZ();
1517                    float tmpx2 = tmp.getMaxX();
1518                    float tmpy2 = tmp.getMaxY();
1519                    float tmpz2 = tmp.getMaxZ();
1520                    
1521                    // If this node forms an edge, then we will set it to be the
1522                    // node for this edge and update the min/max values
1523                    if (tmpx <= minX) {
1524                        minX = tmpx;
1525                        left = node;
1526                        invalidEdges &= ~LEFT_INVALID;
1527                    }
1528                    if (tmpy <= minY) {
1529                        minY = tmpy;
1530                        top = node;
1531                        invalidEdges &= ~TOP_INVALID;
1532                    }
1533                    if (tmpz <= minZ) {
1534                        minZ = tmpz;
1535                        near = node;
1536                        invalidEdges &= ~NEAR_INVALID;
1537                    }
1538                    if (tmpx2 >= maxX) {
1539                        maxX = tmpx2;
1540                        right = node;
1541                        invalidEdges &= ~RIGHT_INVALID;
1542                    }
1543                    if (tmpy2 >= maxY) {
1544                        maxY = tmpy2;
1545                        bottom = node;
1546                        invalidEdges &= ~BOTTOM_INVALID;
1547                    }
1548                    if (tmpz2 >= maxZ) {
1549                        maxZ = tmpz2;
1550                        far = node;
1551                        invalidEdges &= ~FAR_INVALID;
1552                    }
1553                }
1554            }
1555        }
1556
1557        if (invalidEdges != 0) {
1558            // failed to validate some edges
1559            return false;
1560        }
1561
1562        cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
1563                                                        maxX, maxY, maxZ);
1564        return true;
1565    }
1566
1567    private void createCachedBounds(final List<Node> fromNodes) {
1568        // These indicate the bounds of the Group as computed by this function
1569        float minX, minY, minZ;
1570        float maxX, maxY, maxZ;
1571
1572        final int nodeCount = fromNodes.size();
1573        int i;
1574
1575        // handle first visible non-empty node
1576        for (i = 0; i < nodeCount; ++i) {
1577            final Node node = fromNodes.get(i);
1578            node.boundsChanged = false;
1579            if (node.isVisible()) {
1580                tmp = node.getTransformedBounds(
1581                               tmp, BaseTransform.IDENTITY_TRANSFORM);
1582                if (!tmp.isEmpty()) {
1583                    left = top = near = right = bottom = far = node;
1584                    break;
1585                }
1586            }
1587        }
1588
1589        if (i == nodeCount) {
1590            left = top = near = right = bottom = far = null;
1591            cachedBounds.makeEmpty();
1592            return;
1593        }
1594
1595        minX = tmp.getMinX();
1596        minY = tmp.getMinY();
1597        minZ = tmp.getMinZ();
1598        maxX = tmp.getMaxX();
1599        maxY = tmp.getMaxY();
1600        maxZ = tmp.getMaxZ();
1601
1602        // handle remaining visible non-empty nodes
1603        for (++i; i < nodeCount; ++i) {
1604            final Node node = fromNodes.get(i);
1605            node.boundsChanged = false;
1606            if (node.isVisible()) {
1607                tmp = node.getTransformedBounds(
1608                               tmp, BaseTransform.IDENTITY_TRANSFORM);
1609                if (!tmp.isEmpty()) {
1610                    final float tmpx = tmp.getMinX();
1611                    final float tmpy = tmp.getMinY();
1612                    final float tmpz = tmp.getMinZ();
1613                    final float tmpx2 = tmp.getMaxX();
1614                    final float tmpy2 = tmp.getMaxY();
1615                    final float tmpz2 = tmp.getMaxZ();
1616
1617                    if (tmpx < minX) { minX = tmpx; left = node; }
1618                    if (tmpy < minY) { minY = tmpy; top = node; }
1619                    if (tmpz < minZ) { minZ = tmpz; near = node; }
1620                    if (tmpx2 > maxX) { maxX = tmpx2; right = node; }
1621                    if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; }
1622                    if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; }
1623                }
1624            }
1625        }
1626
1627        cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ,
1628                                                        maxX, maxY, maxZ);
1629    }
1630
1631    @Override void updateBounds() {
1632        for (int i=0, max=children.size(); i<max; i++) {
1633            children.get(i).updateBounds();
1634        }
1635        super.updateBounds();
1636    }
1637
1638    /**
1639     * Called by Node whenever its bounds have changed.
1640     */
1641    void childBoundsChanged(Node node) {
1642        // assert node.isVisible();
1643
1644        cachedBoundsInvalid = true;
1645
1646        // mark the node such that the parent knows that the child's bounds
1647        // are not in sync with this parent. In this way, when the bounds
1648        // need to be computed, we'll come back and figure out the new bounds
1649        // for all the children which have boundsChanged set to true
1650        setChildDirty(node, true);
1651
1652        // go ahead and indicate that the geom has changed for this parent,
1653        // even though once we figure it all out it may be that the bounds
1654        // have not changed
1655        impl_geomChanged();
1656    }
1657
1658    /**
1659     * Called by node whenever the visibility of the node changes.
1660     */
1661    void childVisibilityChanged(Node node) {
1662        if (node.isVisible()) {
1663            childIncluded(node);
1664        } else {
1665            childExcluded(node);
1666        }
1667
1668        impl_geomChanged();
1669    }
1670
1671    /**
1672     * @treatAsPrivate implementation detail
1673     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1674     */
1675    @Deprecated
1676    @Override
1677    protected boolean impl_computeContains(double localX, double localY) {
1678        final Point2D tempPt = TempState.getInstance().point;
1679        for (int i=0, max=children.size(); i<max; i++) {
1680            final Node node = children.get(i);
1681            tempPt.x = (float)localX;
1682            tempPt.y = (float)localY;
1683            try {
1684                node.parentToLocal(tempPt);
1685            } catch (NoninvertibleTransformException e) {
1686                continue;
1687            }
1688            if (node.contains(tempPt.x, tempPt.y)) {
1689                return true;
1690            }
1691        }
1692        return false;
1693    }
1694
1695    /**
1696     * @treatAsPrivate implementation detail
1697     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
1698     */
1699    @Deprecated
1700    public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
1701        return alg.processContainerNode(this, ctx);
1702    }
1703}