       1 /*
       2  * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved.
       3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       4  *
       5  * This code is free software; you can redistribute it and/or modify it
       6  * under the terms of the GNU General Public License version 2 only, as
       7  * published by the Free Software Foundation.  Oracle designates this
       8  * particular file as subject to the "Classpath" exception as provided
       9  * by Oracle in the LICENSE file that accompanied this code.
      10  *
      11  * This code is distributed in the hope that it will be useful, but WITHOUT
      12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
      13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
      14  * version 2 for more details (a copy is included in the LICENSE file that
      15  * accompanied this code).
      16  *
      17  * You should have received a copy of the GNU General Public License version
      18  * 2 along with this work; if not, write to the Free Software Foundation,
      19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
      20  *
      21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
      22  * or visit www.oracle.com if you need additional information or have any
      23  * questions.
      24  */
      25
      26 package javafx.scene;
      27
      28 import java.util.ArrayList;
      29 import java.util.HashSet;
      30 import java.util.LinkedHashSet;
      31 import java.util.List;
      32 import java.util.Set;
      33
      34 import com.sun.javafx.jmx.MXNodeAlgorithm;
      35 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
      36 import javafx.beans.property.*;
      37 import javafx.collections.FXCollections;
      38 import javafx.collections.ListChangeListener.Change;
      39 import javafx.collections.ObservableList;
      40
      41 import com.sun.javafx.Logging;
      42 import com.sun.javafx.TempState;
      43 import com.sun.javafx.Utils;
      44 import com.sun.javafx.collections.TrackableObservableList;
      45 import com.sun.javafx.collections.VetoableObservableList;
      46 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
      47 import com.sun.javafx.css.Selector;
      48 import com.sun.javafx.css.StyleManager;
      49 import com.sun.javafx.geom.BaseBounds;
      50 import com.sun.javafx.geom.PickRay;
      51 import com.sun.javafx.geom.Point2D;
      52 import com.sun.javafx.geom.RectBounds;
      53 import com.sun.javafx.geom.transform.BaseTransform;
      54 import com.sun.javafx.geom.transform.NoninvertibleTransformException;
      55 import com.sun.javafx.logging.PlatformLogger;
      56 import com.sun.javafx.scene.CSSFlags;
      57 import com.sun.javafx.scene.DirtyBits;
      58 import com.sun.javafx.scene.traversal.TraversalEngine;
      59 import com.sun.javafx.sg.PGGroup;
      60 import com.sun.javafx.sg.PGNode;
      61 import com.sun.javafx.tk.Toolkit;
      62
      63 /**
      64  * The base class for all nodes that have children in the scene graph.
      65  * <p>
      66  * This class handles all hierarchical scene graph operations, including adding/removing
      67  * child nodes, marking branches dirty for layout and rendering, picking,
      68  * bounds calculations, and executing the layout pass on each pulse.
      69  * <p>
      70  * There are two direct concrete Parent subclasses
      71  * <ul>
      72  * <li>{@link Group} effects and transforms to be applied to a collection of child nodes.</li>
      73  * <li>{@link javafx.scene.layout.Region} class for nodes that can be styled with CSS and layout children. </li>
      74  * </ul>
      75  *
      76  */
      77 public abstract class Parent extends Node {
      78
      79     private static final int DIRTY_CHILDREN_THRESHOLD = 10;
      80
      81     // If set to true, generate a warning message whenever adding a node to a
      82     // parent if it is currently a child of another parent.
      83     private static final boolean warnOnAutoMove = PropertyHelper.getBooleanProperty("javafx.sg.warn");
      84
      85     /**
      86      * Threshold when it's worth to populate list of removed children.
      87      */
      88     private static final int REMOVED_CHILDREN_THRESHOLD = 20;
      89
      90     /**
      91      * Do not populate list of removed children when its number exceeds threshold,
      92      * but mark whole parent dirty.
      93      */
      94     private boolean removedChildrenExceedsThreshold = false;
      95
      96     /**
      97      * @treatAsPrivate implementation detail
      98      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
      99      */
     100     @Deprecated
     101     @Override public void impl_updatePG() {
     102         super.impl_updatePG();
     103
     104         if (Utils.assertionEnabled()) {
     105             List<PGNode> pgnodes = getPGGroup().getChildren();
     106             if (pgnodes.size() != pgChildrenSize) {
     107                 java.lang.System.err.println("*** pgnodes.size() [" + pgnodes.size() + "] != pgChildrenSize [" + pgChildrenSize + "]");
     108             }
     109         }
     110
     111         if (impl_isDirty(DirtyBits.PARENT_CHILDREN)) {
     112             PGGroup peer = getPGGroup();
     113             // Whether a permutation, or children having been added or
     114             // removed, we'll want to clear out the PG side starting
     115             // from startIdx. We know that everything up to but not
     116             // including startIdx is identical between the FX and PG
     117             // sides, so we only need to update the remaining portion.
     118             peer.clearFrom(startIdx);
     119             for (int idx = startIdx; idx < children.size(); idx++) {
     120                     Node n = children.get(idx);
     121                     if (n != null) {
     122                         peer.add(idx, n.impl_getPGNode());
     123                     }
     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                     removed.clear();
     134                 }
     135             }
     136             pgChildrenSize = children.size();
     137             startIdx = pgChildrenSize;
     138         }
     139
     140         if (Utils.assertionEnabled()) validatePG();
     141     }
     142
     143
     144     /***********************************************************************
     145      *                        Scenegraph Structure                         *
     146      *                                                                     *
     147      *  Functions and variables related to the scenegraph structure,       *
     148      *  modifying the structure, and walking the structure.                *
     149      *                                                                     *
     150      **********************************************************************/
     151
     152     // Used to check for duplicate nodes
     153     private final Set<Node> childSet = new HashSet<Node>();
     154
     155     // starting child index from which we need to send the children to the PGGroup
     156     private int startIdx = 0;
     157
     158     // double of children in the PGGroup as of the last update
     159     private int pgChildrenSize = 0;
     160
     161     void validatePG() {
     162         boolean assertionFailed = false;
     163         List<PGNode> pgnodes = getPGGroup().getChildren();
     164         if (pgnodes.size() != children.size()) {
     165             java.lang.System.err.println("*** pgnodes.size validatePG() [" + pgnodes.size() + "] != children.size() [" + children.size() + "]");
     166             assertionFailed = true;
     167         } else {
     168             for (int idx = 0; idx < children.size(); idx++) {
     169                 Node n = children.get(idx);
     170                 if (n.getParent() != this) {
     171                     java.lang.System.err.println("*** this=" + this + " validatePG children[" + idx + "].parent= " + n.getParent());
     172                     assertionFailed = true;
     173                 }
     174                 if (n.impl_getPGNode() != pgnodes.get(idx)) {
     175                     java.lang.System.err.println("*** pgnodes[" + idx + "] validatePG != children[" + idx + "]");
     176                     assertionFailed = true;
     177                 }
     178             }
     179         }
     180         if (assertionFailed) {
     181             throw new java.lang.AssertionError("validation of PGGroup children failed");
     182         }
     183
     184     }
     185
     186     void printSeq(String prefix, List<Node> nodes) {
     187         String str = prefix;
     188         for (Node nn : nodes) {
     189             str += nn + " ";
     190         }
     191         System.out.println(str);
     192     }
     193
     194     /**
     195      * Variable used to avoid executing the body of the on replace trigger on
     196      * children. This is specifically used when we know that changes to the
     197      * children is going to be valid so as to avoid all the scenegraph surgery
     198      * validation routines in the trigger.
     199      */
     200     private boolean ignoreChildrenTrigger = false;
     201
     202     // Variable used to indicate that the change to the children ObservableList is
     203     // a simple permutation as the result of a toFront or toBack operation.
     204     // We can avoid almost all of the processing of the on replace trigger in
     205     // this case.
     206     private boolean childrenTriggerPermutation = false;
     207
     208     //accumulates all removed nodes between pulses, for dirty area calculation.
     209     private List<Node> removed;
     210
     211     /**
     212      * A ObservableList of child {@code Node}s.
     213      * <p>
     214      * See the class documentation for {@link Node} for scene graph structure
     215      * restrictions on setting a {@link Parent}'s children ObservableList.
     216      * If these restrictions are violated by a change to the children ObservableList,
     217      * the change is ignored and the previous value of the child ObservableList is
     218      * restored.
     219      *
     220      * {@code <p>Throws AssignToBoundException} if the same node
     221      * appears in two different bound ObservableList.
     222      *
     223      * @defaultValue empty
     224      * @since JavaFX 1.3
     225      */
     226     private final ObservableList<Node> children = new VetoableObservableList<Node>() {
     227         // set to true if either childRemoved or childAdded returns
     228         // true. These functions will indicate whether the geom
     229         // bounds for the parent have changed
     230         private boolean geomChanged;
     231         private boolean childrenModified;
     232
     233         @Override
     234         protected void onProposedChange(final List<Node> newNodes, int[] toBeRemoved) {
     235             if (ignoreChildrenTrigger) {
     236                 return;
     237             }
     238             if (Parent.this.getScene() != null) {
     239                 // NOTE: this will throw IllegalStateException if we are on the wrong thread
     240                 Toolkit.getToolkit().checkFxUserThread();
     241             }
     242             geomChanged = false;
     243
     244             long newLength = children.size() + newNodes.size();
     245             int removedLength = 0;
     246             for (int i = 0; i < toBeRemoved.length; i += 2) {
     247                 removedLength += toBeRemoved[i + 1] - toBeRemoved[i];
     248             }
     249             newLength -= removedLength;
     250
     251             // If the childrenTriggerPermutation flag is set, then we know it
     252             // is a simple permutation and no further checking is needed.
     253             if (childrenTriggerPermutation) {
     254                 childrenModified = false;
     255                 return;
     256             }
     257
     258             // If the childrenTriggerPermutation flag is not set, then we will
     259             // check to see whether any element in the ObservableList has changed,
     260             // or whether the new ObservableList is a permutation on the existing
     261             // ObservableList. Note that even if the childrenModified flag is false,
     262             // we still have to check for duplicates. If it is a simple
     263             // permutation, we can avoid checking for cycles or other parents.
     264             childrenModified = true;
     265             if (newLength == childSet.size()) {
     266                 childrenModified = false;
     267                 for (int i = newNodes.size() - 1; i >= 0; --i ) {
     268                     Node n = newNodes.get(i);
     269                     if (!childSet.contains(n)) {
     270                         childrenModified = true;
     271                         break;
     272                     }
     273                 }
     274             }
     275
     276             // Enforce scene graph invariants, and check for structural errors.
     277             //
     278             // 1. If a child has been added to this parent more than once,
     279             // then it is an error
     280             //
     281             // 2. If a child is a target of a clip, then it is an error.
     282             //
     283             // 3. If a node would cause a cycle, then it is an error.
     284             //
     285             // 4. If a node is null
     286             //
     287             // Note that if a node is the child of another parent, we will
     288             // implicitly remove the node from its former Parent after first
     289             // checking for errors.
     290
     291             // iterate over the nodes that were removed and remove them from
     292             // the hash set.
     293             for (int i = 0; i < toBeRemoved.length; i += 2) {
     294                 for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
     295                     childSet.remove(children.get(j));
     296                 }
     297             }
     298
     299             try {
     300                 childSet.addAll(newNodes);
     301                 if (childSet.size() != newLength) {
     302                     throw new IllegalArgumentException(
     303                             constructExceptionMessage(
     304                                 "duplicate children added", null));
     305                 }
     306
     307                 if (childrenModified) {
     308                     for (int i = newNodes.size() - 1; i >= 0; --i ) {
     309                         Node node = newNodes.get(i);
     310                         if (node == null) {
     311                             throw new NullPointerException(
     312                                     constructExceptionMessage(
     313                                         "child node is null", null));
     314                         }
     315                         if (node.getClipParent() != null) {
     316                             throw new IllegalArgumentException(
     317                                     constructExceptionMessage(
     318                                         "node already used as a clip", node));
     319                         }
     320                         if (wouldCreateCycle(Parent.this, node)) {
     321                             throw new IllegalArgumentException(
     322                                     constructExceptionMessage(
     323                                         "cycle detected", node));
     324                         }
     325                     }
     326                 }
     327             } catch (RuntimeException e) {
     328                 //Return children to it's original state
     329                 childSet.clear();
     330                 childSet.addAll(children);
     331
     332                 // rethrow
     333                 throw e;
     334             }
     335
     336             // Done with error checking
     337
     338             if (!childrenModified) {
     339                 return;
     340             }
     341
     342             // iterate over the nodes that were removed and clear their
     343             // parent and scene. Add to them also to removed list for further
     344             // dirty regions calculation.
     345             if (removed == null) {
     346                 removed = new ArrayList<Node>();
     347             }
     348             if (removed.size() + removedLength > REMOVED_CHILDREN_THRESHOLD) {
     349                 //do not populate too many children in removed list
     350                 removedChildrenExceedsThreshold = true;
     351             }
     352             for (int i = 0; i < toBeRemoved.length; i += 2) {
     353                 for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) {
     354                     Node old = children.get(j);
     355                     if (dirtyChildren != null) {
     356                         dirtyChildren.remove(old);
     357                     }
     358                     if (old == null) {
     359                         continue;
     360                     }
     361
     362                     if (!geomChanged && old.isVisible()) {
     363                         geomChanged = childRemoved(old);
     364                     }
     365                     if (old.getParent() == Parent.this) {
     366                         old.setParent(null);
     367                         old.setScene(null);
     368                     }
     369                     if (!removedChildrenExceedsThreshold) {
     370                         removed.add(old);
     371                     }
     372                 }
     373             }
     374         }
     375
     376         @Override
     377         protected void onChanged(Change<Node> c) {
     378             // proceed with updating the scene graph
     379             if (childrenModified) {
     380                 unmodifiableManagedChildren = null;
     381                 boolean relayout = false;
     382
     383                 while (c.next()) {
     384                     int from = c.getFrom();
     385                     int to = c.getTo();
     386                     for (int i = from; i < to; ++i) {
     387                         Node n = children.get(i);
     388                         if (n != null && n.getParent() != null && n.getParent() != Parent.this) {
     389                             if (warnOnAutoMove) {
     390                                 java.lang.System.err.println("WARNING added to a new parent without first removing it from its current");
     391                                 java.lang.System.err.println("    parent. It will be automatically removed from its current parent.");
     392                                 java.lang.System.err.println("    node=" + n + " oldparent= " + n.getParent() + " newparent=" + this);
     393                             }
     394                             n.getParent().children.remove(n);
     395                             if (n.isManaged()) {
     396                                 relayout = true;
     397                             }
     398                             if (warnOnAutoMove) {
     399                                 Thread.dumpStack();
     400                             }
     401                         }
     402                     }
     403
     404                     List<Node> removed = c.getRemoved();
     405                     int removedSize = removed.size();
     406                     for (int i = 0; i < removedSize; ++i) {
     407                         Node node = removed.get(i);
     408                         if (node != null && node.isManaged()) {
     409                             relayout = true;
     410                         }
     411                     }
     412
     413                     // update the parent and scene for each new node
     414                     for (int i = from; i < to; ++i) {
     415                         Node node = children.get(i);
     416                         if (node != null) {
     417                             if (node.isManaged()) {
     418                                 relayout = true;
     419                             }
     420                             node.setParent(Parent.this);
     421                             node.setScene(getScene());
     422                             if (dirtyChildren != null) {
     423                                 dirtyChildren.add(node);
     424                             }
     425                             if (!geomChanged && node.isVisible()) {
     426                                 geomChanged = childAdded(node);
     427                             }
     428                         }
     429                     }
     430                 }
     431
     432                 // check to see if the number of children exceeds
     433                 // DIRTY_CHILDREN_THRESHOLD and dirtyChildren is null.
     434                 // If so, then we need to create dirtyChildren and
     435                 // populate it.
     436                 if (dirtyChildren == null && children.size() > DIRTY_CHILDREN_THRESHOLD) {
     437                     dirtyChildren = new LinkedHashSet<Node>(2 * children.size());
     438                     // only bother populating children if geom has
     439                     // changed, otherwise there is no need
     440                     if (geomChanged) {
     441                         int size = children.size();
     442                         for (int i = 0; i < size; ++i) {
     443                             Node ch = children.get(i);
     444                             if (ch.isVisible() && ch.boundsChanged) {
     445                                 dirtyChildren.add(ch);
     446                             }
     447                         }
     448                     }
     449                 }
     450
     451                 if (geomChanged) {
     452                     impl_geomChanged();
     453                 }
     454
     455                 //
     456                 // Note that the styles of a child do not affect the parent or
     457                 // its siblings. Thus, it is only necessary to reapply css to
     458                 // the Node just added and not to this parent and all of its
     459                 // children. So the following call to impl_reapplyCSS was moved
     460                 // to Node.parentProperty. The original comment and code were
     461                 // purposely left here as documentation should there be any
     462                 // question about how the code used to work and why the change
     463                 // was made.
     464                 //
     465                 // if children have changed then I need to reapply
     466                 // CSS from this node on down
     467 //                impl_reapplyCSS();
     468                 //
     469
     470                 // request layout if a Group subclass has overridden doLayout OR
     471                 // if one of the new children needs layout, in which case need to ensure
     472                 // the needsLayout flag is set all the way to the root so the next layout
     473                 // pass will reach the child.
     474                 if (relayout) {
     475                     requestLayout();
     476                 }
     477             }
     478
     479             // Note the starting index at which we need to update the
     480             // PGGroup on the next update, and mark the children dirty
     481             c.reset();
     482             c.next();
     483             if (startIdx > c.getFrom()) {
     484                 startIdx = c.getFrom();
     485             }
     486
     487             impl_markDirty(DirtyBits.PARENT_CHILDREN);
     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 != null && 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 void sceneChanged(final Scene old) {
     632         // Give all of the children the new scene
     633         final Scene scene = getScene();
     634         for (int i=0, max=children.size(); i<max; i++) {
     635             final Node node = children.get(i);
     636             if (node != null) {
     637                 node.setScene(scene);
     638             }
     639         }
     640
     641         // If this node was in the old scene's dirty layout
     642         // list, then remove it from that list so that it is
     643         // not processed on the next pulse
     644         final boolean awaitingLayout = isNeedsLayout();
     645         if (awaitingLayout && old != null) {
     646             old.removeFromDirtyLayoutList(this);
     647         }
     648
     649         sceneRoot = scene != null && scene.getRoot() == this;
     650         layoutRoot = !isManaged() || sceneRoot;
     651
     652         // If this node is dirty and the new scene is not null
     653         // then add this node to the new scene's dirty list
     654         if (scene != null && awaitingLayout && layoutRoot) {
     655             scene.addToDirtyLayoutList(this);
     656         }
     657     }
     658
     659     @Override
     660     void setDerivedDepthTest(boolean value) {
     661         super.setDerivedDepthTest(value);
     662
     663         for (int i=0, max=children.size(); i<max; i++) {
     664             final Node node = children.get(i);
     665             node.computeDerivedDepthTest();
     666         }
     667     }
     668
     669     /**
     670      * @treatAsPrivate implementation detail
     671      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     672      */
     673     @Deprecated
     674     @Override protected Node impl_pickNodeLocal(double localX, double localY) {
     675         if (containsBounds(localX, localY)) {
     676             for (int i = children.size()-1; i >= 0; i--) {
     677                 Node picked = children.get(i).impl_pickNode(localX, localY);
     678                 if (picked != null) {
     679                     return picked;
     680                 }
     681             }
     682         }
     683         return null;
     684     }
     685
     686     /**
     687      * @treatAsPrivate implementation detail
     688      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     689      */
     690     @Deprecated
     691     @Override protected Node impl_pickNodeLocal(PickRay pickRay) {
     692         for (int i = children.size()-1; i >= 0; i--) {
     693             Node picked = children.get(i).impl_pickNode(pickRay);
     694
     695             if (picked != null) {
     696                 return picked;
     697             }
     698         }
     699         return null;
     700     }
     701
     702     @Override boolean isConnected() {
     703         return super.isConnected() || sceneRoot;
     704     }
     705
     706     @Override public Node lookup(String selector) {
     707         Node n = super.lookup(selector);
     708         if (n == null) {
     709             for (int i=0, max=children.size(); i<max; i++) {
     710                 final Node node = children.get(i);
     711                 n = node.lookup(selector);
     712                 if (n != null) return n;
     713             }
     714         }
     715         return n;
     716     }
     717
     718     /**
     719      * Please Note: This method should never create the results set,
     720      * let the Node class implementation do this!
     721      */
     722     @Override List<Node> lookupAll(Selector selector, List<Node> results) {
     723         results = super.lookupAll(selector, results);
     724         for (int i=0, max=children.size(); i<max; i++) {
     725             final Node node = children.get(i);
     726             results = node.lookupAll(selector, results);
     727         }
     728         return results;
     729     }
     730
     731     /** @treatAsPrivate implementation detail */
     732     private javafx.beans.property.ObjectProperty<TraversalEngine> impl_traversalEngine;
     733
     734     /**
     735      * @treatAsPrivate implementation detail
     736      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     737      */
     738     // SB-dependency: RT-21209 has been filed to track this
     739     @Deprecated
     740     public final void setImpl_traversalEngine(TraversalEngine value) {
     741         impl_traversalEngineProperty().set(value);
     742     }
     743
     744     /**
     745      * @treatAsPrivate implementation detail
     746      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     747      */
     748     @Deprecated
     749     public final TraversalEngine getImpl_traversalEngine() {
     750         return impl_traversalEngine == null ? null : impl_traversalEngine.get();
     751     }
     752
     753     /**
     754      * @treatAsPrivate implementation detail
     755      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
     756      */
     757     @Deprecated
     758     public final ObjectProperty<TraversalEngine> impl_traversalEngineProperty() {
     759         if (impl_traversalEngine == null) {
     760             impl_traversalEngine =
     761                     new SimpleObjectProperty<TraversalEngine>(
     762                             this, "impl_traversalEngine");
     763         }
     764         return impl_traversalEngine;
     765     }
     766
     767     /***********************************************************************
     768      *                               Layout                                *
     769      *                                                                     *
     770      *  Functions and variables related to the layout scheme used by       *
     771      *  JavaFX. Includes both public and private API.                      *
     772      *                                                                     *
     773      **********************************************************************/
     774     /**
     775      * Indicates that this Node and its subnodes requires a layout pass on
     776      * the next pulse.
     777      */
     778     private ReadOnlyBooleanWrapper needsLayout = new ReadOnlyBooleanWrapper(this, "needsLayout", true);
     779
     780     protected final void setNeedsLayout(boolean value) {
     781         needsLayout.set(value);
     782     }
     783
     784     public final boolean isNeedsLayout() {
     785         return needsLayout.get();
     786     }
     787
     788     public final ReadOnlyBooleanProperty needsLayoutProperty() {
     789         return needsLayout.getReadOnlyProperty();
     790     }
     791
     792     /**
     793      * This package levelis used only by Node. It is set to true while
     794      * the layout() function is processing and set to false on the conclusion.
     795      * It is used by the Node to decide whether to perform CSS updates
     796      * synchronously or asynchronously.
     797      */
     798     boolean performingLayout = false;
     799
     800     private boolean sizeCacheClear = true;
     801     private double prefWidthCache = -1;
     802     private double prefHeightCache = -1;
     803     private double minWidthCache = -1;
     804     private double minHeightCache = -1;
     805
     806     /**
     807      * Requests a layout pass to be performed before the next scene is
     808      * rendered. This is batched up asynchronously to happen once per
     809      * "pulse", or frame of animation.
     810      * <p>
     811      * If this parent is either a layout root or unmanaged, then it will be
     812      * added directly to the scene's dirty layout list, otherwise requestLayout
     813      * will be invoked on its parent.
     814      */
     815     public void requestLayout() {
     816         if (!isNeedsLayout()) {
     817             prefWidthCache = -1;
     818             prefHeightCache = -1;
     819             minWidthCache = -1;
     820             minHeightCache = -1;
     821             PlatformLogger logger = Logging.getLayoutLogger();
     822             if (logger.isLoggable(PlatformLogger.FINER)) {
     823                 logger.finer(this.toString());
     824             }
     825
     826             setNeedsLayout(true);
     827             if (layoutRoot) {
     828                 final Scene scene = getScene();
     829                 if (scene != null) {
     830                     if (logger.isLoggable(PlatformLogger.FINER)) {
     831                         logger.finer(this.toString()+" layoutRoot added to scene dirty layout list");
     832                     }
     833                     scene.addToDirtyLayoutList(this);
     834                 }
     835             } else {
     836                 final Parent parent = getParent();
     837                 if (parent != null) {
     838                     parent.requestLayout();
     839                 }
     840             }
     841         } else {
     842             clearSizeCache();
     843         }
     844     }
     845
     846     void clearSizeCache() {
     847         if (sizeCacheClear) {
     848             return;
     849         }
     850         sizeCacheClear = true;
     851         prefWidthCache = -1;
     852         prefHeightCache = -1;
     853         minWidthCache = -1;
     854         minHeightCache = -1;
     855         if (!layoutRoot) {
     856             final Parent parent = getParent();
     857             if (parent != null) {
     858                 parent.clearSizeCache();
     859             }
     860         }
     861     }
     862
     863     @Override public double prefWidth(double height) {
     864         if (height == -1) {
     865             if (prefWidthCache == -1) {
     866                 prefWidthCache = computePrefWidth(-1);
     867                 sizeCacheClear = false;
     868             }
     869             return prefWidthCache;
     870         } else {
     871             return computePrefWidth(height);
     872         }
     873     }
     874
     875     @Override public double prefHeight(double width) {
     876         if (width == -1) {
     877             if (prefHeightCache == -1) {
     878                 prefHeightCache = computePrefHeight(-1);
     879                 sizeCacheClear = false;
     880             }
     881             return prefHeightCache;
     882         } else {
     883             return computePrefHeight(width);
     884         }
     885     }
     886
     887     @Override public double minWidth(double height) {
     888         if (height == -1) {
     889             if (minWidthCache == -1) {
     890                 minWidthCache = computeMinWidth(-1);
     891                 sizeCacheClear = false;
     892             }
     893             return minWidthCache;
     894         } else {
     895             return computeMinWidth(height);
     896         }
     897     }
     898
     899     @Override public double minHeight(double width) {
     900         if (width == -1) {
     901             if (minHeightCache == -1) {
     902                 minHeightCache = computeMinHeight(-1);
     903                 sizeCacheClear = false;
     904             }
     905             return minHeightCache;
     906         } else {
     907             return computeMinHeight(width);
     908         }
     909     }
     910
     911     // PENDING_DOC_REVIEW
     912     /**
     913      * Calculates the preferred width of this {@code Parent}. The default
     914      * implementation calculates this width as the width of the area occupied
     915      * by its managed children when they are positioned at their
     916      * current positions at their preferred widths.
     917      *
     918      * @param height the height that should be used if preferred width depends
     919      *      on it
     920      * @return the calculated preferred width
     921      */
     922     protected double computePrefWidth(double height) {
     923         double minX = 0;
     924         double maxX = 0;
     925         for (int i=0, max=children.size(); i<max; i++) {
     926             Node node = children.get(i);
     927             if (node.isManaged()) {
     928                 final double x = node.getLayoutBounds().getMinX() + node.getLayoutX();
     929                 minX = Math.min(minX, x);
     930                 maxX = Math.max(maxX, x + node.prefWidth(-1));
     931             }
     932         }
     933         return maxX - minX;
     934     }
     935
     936     // PENDING_DOC_REVIEW
     937     /**
     938      * Calculates the preferred height of this {@code Parent}. The default
     939      * implementation calculates this height as the height of the area occupied
     940      * by its managed children when they are positioned at their current
     941      * positions at their preferred heights.
     942      *
     943      * @param width the width that should be used if preferred height depends
     944      *      on it
     945      * @return the calculated preferred height
     946      */
     947     protected double computePrefHeight(double width) {
     948         double minY = 0;
     949         double maxY = 0;
     950         for (int i=0, max=children.size(); i<max; i++) {
     951             Node node = children.get(i);
     952             if (node.isManaged()) {
     953                 final double y = node.getLayoutBounds().getMinY() + node.getLayoutY();
     954                 minY = Math.min(minY, y);
     955                 maxY = Math.max(maxY, y + node.prefHeight(-1));
     956             }
     957         }
     958         return maxY - minY;
     959     }
     960
     961     /**
     962      * Calculates the minimum width of this {@code Parent}. The default
     963      * implementation simply returns the pref width.
     964      *
     965      * @param height the height that should be used if min width depends
     966      *      on it
     967      * @return the calculated min width
     968      */
     969     protected double computeMinWidth(double height) {
     970         return prefWidth(height);
     971     }
     972
     973     // PENDING_DOC_REVIEW
     974     /**
     975      * Calculates the min height of this {@code Parent}. The default
     976      * implementation simply returns the pref height;
     977      *
     978      * @param width the width that should be used if min height depends
     979      *      on it
     980      * @return the calculated min height
     981      */
     982     protected double computeMinHeight(double width) {
     983         return prefHeight(width);
     984     }
     985
     986     /**
     987      * Calculates the baseline offset based on the first managed child. If there
     988      * is no such child, returns {@link Node#getBaselineOffset()}.
     989      *
     990      * @return baseline offset
     991      */
     992     @Override public double getBaselineOffset() {
     993         for (int i=0, max=children.size(); i<max; i++) {
     994             final Node child = children.get(i);
     995             if (child.isManaged()) {
     996                 return child.getLayoutBounds().getMinY() + child.getLayoutY() + child.getBaselineOffset();
     997             }
     998         }
     999         return super.getBaselineOffset();
    1000     }
    1001
    1002     /**
    1003      * Executes a top-down layout pass on the scene graph under this parent.
    1004      */
    1005     public final void layout() {
    1006         if (isNeedsLayout()) {
    1007             performingLayout = true;
    1008
    1009             PlatformLogger logger = Logging.getLayoutLogger();
    1010             if (logger.isLoggable(PlatformLogger.FINE)) {
    1011                 logger.fine(this+" size: "+
    1012                         getLayoutBounds().getWidth()+" x "+getLayoutBounds().getHeight());
    1013             }
    1014
    1015             // layout the children in this parent.
    1016             layoutChildren();
    1017             // clear flag before recursing down so requestLayout calls arn't swallowed
    1018             setNeedsLayout(false);
    1019
    1020             // Perform layout on each child, hoping it has random access performance!
    1021             for (int i=0, max=children.size(); i<max; i++) {
    1022                 final Node child = children.get(i);
    1023                 if (child instanceof Parent) {
    1024                     ((Parent) child).layout();
    1025                 }
    1026             }
    1027             performingLayout = false;
    1028         }
    1029     }
    1030
    1031     /**
    1032      * Invoked during the layout pass to layout the children in this
    1033      * {@code Parent}. By default it will only set the size of managed,
    1034      * resizable content to their preferred sizes and does not do any node
    1035      * positioning.
    1036      * <p>
    1037      * Subclasses should override this function to layout content as needed.
    1038      */
    1039     protected void layoutChildren() {
    1040         for (int i=0, max=children.size(); i<max; i++) {
    1041             final Node node = children.get(i);
    1042             if (node.isResizable() && node.isManaged()) {
    1043                 node.autosize();
    1044             }
    1045         }
    1046     }
    1047
    1048     /**
    1049      * This field is managed by the Scene, and set on any node which is the
    1050      * root of a Scene.
    1051      */
    1052     private boolean sceneRoot = false;
    1053
    1054     /**
    1055      * Keeps track of whether this node is a layout root. This is updated
    1056      * whenever the sceneRoot field changes, or whenever the managed
    1057      * property changes.
    1058      */
    1059     private boolean layoutRoot = false;
    1060     @Override final void notifyManagedChanged() {
    1061         layoutRoot = !isManaged() || sceneRoot;
    1062     }
    1063
    1064     /***********************************************************************
    1065      *                                                                     *
    1066      *                         Stylesheet Handling                         *
    1067      *                                                                     *
    1068      **********************************************************************/
    1069
    1070
    1071     /**
    1072      * A ObservableList of string URLs linking to the stylesheets to use with this scene's
    1073      * contents. For additional information about using CSS with the
    1074      * scene graph, see the <a href="doc-files/cssref.html">CSS Reference
    1075      * Guide</a>.
    1076      */
    1077     private final ObservableList<String> stylesheets = new TrackableObservableList<String>() {
    1078         @Override
    1079         protected void onChanged(Change<String> c) {
    1080             final Scene scene = getScene();
    1081             if (scene != null) {
    1082
    1083                 scene.styleManager.parentStylesheetsChanged(Parent.this, c);
    1084                 // RT-9784 - if stylesheet is removed, reset styled properties to
    1085                 // their initial value.
    1086                 while(c.next()) {
    1087                     if (c.wasRemoved() == false) continue;
    1088                     impl_cssResetInitialValues();
    1089                     break; // no point in resetting more than once...
    1090                 }
    1091
    1092                 impl_reapplyCSS();
    1093             }
    1094         }
    1095     };
    1096
    1097     /**
    1098      * Gets an observable list of string URLs linking to the stylesheets to use
    1099      * with this Parent's contents. For additional information about using CSS
    1100      * with the scene graph, see the <a href="doc-files/cssref.html">CSS Reference
    1101      * Guide</a>.
    1102      *
    1103      * @return the list of stylesheets to use with this Parent
    1104      */
    1105     public final ObservableList<String> getStylesheets() { return stylesheets; }
    1106
    1107     /**
    1108      * This method recurses up the parent chain until parent is null. As the
    1109      * stack unwinds, if the Parent has stylesheets, they are added to the
    1110      * list.
    1111      *
    1112      * It is possible to override this method to stop the recursion. This allows
    1113      * a Parent to have a set of stylesheets distinct from its Parent.
    1114      *
    1115      * @treatAsPrivate implementation detail
    1116      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1117      */
    1118     @Deprecated // SB-dependency: RT-21247 has been filed to track this
    1119     public /* Do not make this final! */ List<String> impl_getAllParentStylesheets() {
    1120
    1121         List<String> list = null;
    1122         final Parent myParent = getParent();
    1123         if (myParent != null) {
    1124
    1125             //
    1126             // recurse so that stylesheets of Parents closest to the root are
    1127             // added to the list first. The ensures that declarations for
    1128             // stylesheets further down the tree (closer to the leaf) have
    1129             // a higer ordinal in the cascade.
    1130             //
    1131             list = myParent.impl_getAllParentStylesheets();
    1132         }
    1133
    1134         if (stylesheets != null && stylesheets.isEmpty() == false) {
    1135             if (list == null) list = new ArrayList<String>(stylesheets.size());
    1136             for (int n=0,nMax=stylesheets.size(); n<nMax; n++)
    1137                 list.add(stylesheets.get(n));
    1138         }
    1139
    1140         return list;
    1141
    1142     }
    1143
    1144     /**
    1145      * @treatAsPrivate implementation detail
    1146      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1147      */
    1148     @Deprecated
    1149     @Override public void impl_processCSS(boolean reapply) {
    1150         // Determine whether we will need to reapply from here on down
    1151         boolean flag = reapply || cssFlag == CSSFlags.REAPPLY;
    1152         // Let the super implementation handle CSS for this node
    1153         super.impl_processCSS(flag);
    1154         // For each child, process CSS
    1155         for (int i=0, max=children.size(); i<max; i++) {
    1156             final Node kid = children.get(i);
    1157             if (kid != null) {
    1158                 kid.impl_processCSS(flag);
    1159             }
    1160         }
    1161     }
    1162
    1163     /**
    1164      * @treatAsPrivate implementation detail
    1165      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1166      */
    1167     @Deprecated
    1168     @Override public void impl_cssResetInitialValues() {
    1169         // RT-9784
    1170         super.impl_cssResetInitialValues();
    1171
    1172         for (int i=0, max=children.size(); i<max; i++) {
    1173             final Node kid = children.get(i);
    1174             if (kid != null) {
    1175                 kid.impl_cssResetInitialValues();
    1176             }
    1177         }
    1178     }
    1179
    1180
    1181     /***********************************************************************
    1182      *                               Misc                                  *
    1183      *                                                                     *
    1184      *  Initialization and other functions                                 *
    1185      *                                                                     *
    1186      **********************************************************************/
    1187
    1188
    1189     /**
    1190      * Constructs a new {@code Parent}.
    1191      */
    1192     protected Parent() { }
    1193
    1194     /**
    1195      * @treatAsPrivate implementation detail
    1196      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1197      */
    1198     @Deprecated
    1199     protected @Override PGNode impl_createPGNode() {
    1200         return Toolkit.getToolkit().createPGGroup();
    1201     }
    1202
    1203     PGGroup getPGGroup() {
    1204         return ((PGGroup) impl_getPGNode());
    1205     }
    1206
    1207
    1208     /***************************************************************************
    1209      *                                                                         *
    1210      *                         Bounds Computations                             *
    1211      *                                                                         *
    1212      *  This code originated in GroupBoundsHelper (part of javafx-sg-common)   *
    1213      *  but has been ported here to the FX side since we cannot rely on the PG *
    1214      *  side for computing the bounds (due to the decoupling of the two        *
    1215      *  scenegraphs for threading and other purposes).                         *
    1216      *                                                                         *
    1217      *  Unfortunately, we cannot simply reuse GroupBoundsHelper without some  *
    1218      *  major (and hacky) modification due to the fact that GroupBoundsHelper  *
    1219      *  relies on PG state and we need to do similar things here that rely on  *
    1220      *  core scenegraph state. Unfortunately, that means we made a port.       *
    1221      *                                                                         *
    1222      **************************************************************************/
    1223
    1224     private BaseBounds tmp = new RectBounds();
    1225
    1226     /**
    1227      * The cached bounds for the Group. If the cachedBounds are invalid
    1228      * then we have no history of what the bounds are, or were.
    1229      */
    1230     private BaseBounds cachedBounds = new RectBounds();
    1231
    1232     /**
    1233      * Indicates that the cachedBounds is invalid (or old) and need to be recomputed.
    1234      * If cachedBoundsInvalid is true, but cachedBounds is not empty,
    1235      * then when we recompute the cachedBounds we can consider the
    1236      * values in cachedBounds to represent the last valid bounds for the group.
    1237      * This is useful for several fast paths.
    1238      */
    1239     private boolean cachedBoundsInvalid = true;
    1240
    1241     /**
    1242      * This set is used to track all of the children of this group which are
    1243      * dirty. It is only used in cases where the number of children is > some
    1244      * value (currently 10). For very wide trees, this can provide a very
    1245      * important speed boost. For the sake of memory consumption, this is
    1246      * null unless the number of children ever crosses the threshold where
    1247      * it will be activated.
    1248      */
    1249     private LinkedHashSet<Node> dirtyChildren = null;
    1250
    1251     private Node top;
    1252     private Node left;
    1253     private Node bottom;
    1254     private Node right;
    1255     private Node near;
    1256     private Node far;
    1257
    1258     /**
    1259      * @treatAsPrivate implementation detail
    1260      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1261      */
    1262     @Deprecated
    1263     @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
    1264         // If we have no children, our bounds are invalid
    1265         if (children.isEmpty()) {
    1266             return bounds.makeEmpty();
    1267         }
    1268
    1269         if (tx.isTranslateOrIdentity()) {
    1270             // this is a transform which is only doing translations, or nothing
    1271             // at all (no scales, rotates, or shears)
    1272             // so in this case we can easily use the cached bounds
    1273             if (cachedBoundsInvalid) recomputeBounds();
    1274             if (!tx.isIdentity()) {
    1275                 bounds = bounds.deriveWithNewBounds((float)(cachedBounds.getMinX() + tx.getMxt()),
    1276                                  (float)(cachedBounds.getMinY() + tx.getMyt()),
    1277                                  (float)(cachedBounds.getMinZ() + tx.getMzt()),
    1278                                  (float)(cachedBounds.getMaxX() + tx.getMxt()),
    1279                                  (float)(cachedBounds.getMaxY() + tx.getMyt()),
    1280                                  (float)(cachedBounds.getMaxZ() + tx.getMzt()));
    1281             } else {
    1282                 bounds = bounds.deriveWithNewBounds(cachedBounds);
    1283             }
    1284
    1285             return bounds;
    1286         } else {
    1287             // there is a scale, shear, or rotation happening, so need to
    1288             // do the full transform!
    1289             double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE;
    1290             double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE, maxZ = Double.MIN_VALUE;
    1291             boolean first = true;
    1292             for (int i=0, max=children.size(); i<max; i++) {
    1293                 final Node node = children.get(i);
    1294                 if (node.isVisible()) {
    1295                     bounds = node.getTransformedBounds(bounds, tx);
    1296                     // if the bounds of the child are invalid, we don't want
    1297                     // to use those in the remaining computations.
    1298                     if (bounds.isEmpty()) continue;
    1299                     if (first) {
    1300                         minX = bounds.getMinX();
    1301                         minY = bounds.getMinY();
    1302                         minZ = bounds.getMinZ();
    1303                         maxX = bounds.getMaxX();
    1304                         maxY = bounds.getMaxY();
    1305                         maxZ = bounds.getMaxZ();
    1306                         first = false;
    1307                     } else {
    1308                         minX = Math.min(bounds.getMinX(), minX);
    1309                         minY = Math.min(bounds.getMinY(), minY);
    1310                         minZ = Math.min(bounds.getMinZ(), minZ);
    1311                         maxX = Math.max(bounds.getMaxX(), maxX);
    1312                         maxY = Math.max(bounds.getMaxY(), maxY);
    1313                         maxZ = Math.max(bounds.getMaxZ(), maxZ);
    1314                     }
    1315                 }
    1316             }
    1317             // if "first" is still true, then we didn't have any children with
    1318             // non-empty bounds and thus we must return an empty bounds,
    1319             // otherwise we have non-empty bounds so go for it.
    1320             if (first)
    1321                 bounds.makeEmpty();
    1322             else
    1323                 bounds = bounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
    1324                         (float)maxX, (float)maxY, (float)maxZ);
    1325
    1326             return bounds;
    1327         }
    1328     }
    1329
    1330     // Returns TRUE if the bounds have changed, FALSE otherwise
    1331     boolean childAdded(Node node) {
    1332         // If there is no history as to what the bounds were (ie, if
    1333         // cachedBounds are empty) then we punt since the next call
    1334         // to get the bounds will force a full recomputation anyway.
    1335
    1336         if (cachedBounds.isEmpty()) {
    1337             return true;
    1338         }
    1339
    1340         // If one of the edges has been cleared by the removal of a node,
    1341         // then there is no point to continuing because we will have to
    1342         // completely revalidate later.
    1343         if (top == null || bottom == null || left == null || right == null
    1344                 || near == null || far == null) {
    1345             cachedBounds.makeEmpty();
    1346             cachedBoundsInvalid = true;
    1347             return true;
    1348         }
    1349
    1350         // If we got here then the top/bottom/left/right/near/far are still set
    1351         // to a valid node and we know the old size of the Group. So what we are
    1352         // going to do is execute a fastpath where we figure out if any of the
    1353         // new Nodes "extend" the bounds, and if so, they will form the new
    1354         // edges and the cachedBounds will be updated accordingly. We will
    1355         // also communicate that as a result of adding these Nodes, the
    1356         // bounds have changed.
    1357
    1358         boolean extended = false;
    1359
    1360         // set to indicate which edges need to be cleared so we can recompute
    1361         if (node.isVisible()) {
    1362             tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
    1363             if (!tmp.isEmpty()) {
    1364                 node.boundsChanged = false;
    1365                 double tmpx = tmp.getMinX();
    1366                 double tmpy = tmp.getMinY();
    1367                 double tmpz = tmp.getMinZ();
    1368                 double tmpx2 = tmp.getMaxX();
    1369                 double tmpy2 = tmp.getMaxY();
    1370                 double tmpz2 = tmp.getMaxZ();
    1371                 double cx = cachedBounds.getMinX();
    1372                 double cy = cachedBounds.getMinY();
    1373                 double cz = cachedBounds.getMinZ();
    1374                 double cx2 = cachedBounds.getMaxX();
    1375                 double cy2 = cachedBounds.getMaxY();
    1376                 double cz2 = cachedBounds.getMaxZ();
    1377
    1378                 double minX = Math.min(tmpx, cx);
    1379                 double minY = Math.min(tmpy, cy);
    1380                 double minZ = Math.min(tmpz, cz);
    1381                 double maxX = Math.max(tmpx2, cx2);
    1382                 double maxY = Math.max(tmpy2, cy2);
    1383                 double maxZ = Math.max(tmpz2, cz2);
    1384
    1385                 if (tmpy < cy) { extended = true; top = node; }
    1386                 if (tmpx < cx) { extended = true; left = node; }
    1387                 if (tmpz < cz) { extended = true; near = node; }
    1388                 if (tmpy2 > cy2) { extended = true; bottom = node; }
    1389                 if (tmpx2 > cx2) { extended = true; right = node; }
    1390                 if (tmpz2 > cz2) { extended = true; far = node; }
    1391
    1392                 // update the cached bounds
    1393                 cachedBounds = cachedBounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
    1394                         (float)maxX, (float)maxY, (float)maxZ);
    1395             }
    1396         }
    1397         // return whether or not the bounds have been extended (and thus, are
    1398         // dirty)
    1399         return extended;
    1400     }
    1401
    1402     // This is called when either the child is actually removed, OR IF IT IS
    1403     // TOGGLED TO BE INVISIBLE. This is because in both cases it needs to be
    1404     // cleared from the state which manages bounds.
    1405     boolean childRemoved(Node node) {
    1406         // clear this flag because Group no longer knows whether the bounds
    1407         // have changed or not relative to itself
    1408         node.boundsChanged = false;
    1409         // If there is no history as to what the bounds were (ie, if
    1410         // cachedBounds are empty) then we punt since the next call
    1411         // to get the bounds will force a full recomputation anyway.
    1412
    1413         if (cachedBounds.isEmpty()) {
    1414             return true;
    1415         }
    1416
    1417         // Since there was some prior size to the Group, all the nodes that
    1418         // were removed are checked to see if they formed one of the edges. If
    1419         // so, the edge variable is cleared
    1420         if (node == top) top = null;
    1421         if (node == left) left = null;
    1422         if (node == bottom) bottom = null;
    1423         if (node == right) right = null;
    1424         if (node == near) near = null;
    1425         if (node == far) far = null;
    1426
    1427         // If one of the edges has been cleared by the removal of a node,
    1428         // then there is no point to continuing because we will have to
    1429         // completely revalidate later.
    1430         if (top == null || bottom == null || left == null || right == null
    1431                 || near == null || far == null) {
    1432             cachedBounds.makeEmpty();
    1433             cachedBoundsInvalid = true;
    1434             top = left = bottom = right = near = far = null;
    1435             return true;
    1436         }
    1437
    1438         // Return false since nothing has changed that will affect the bounds.
    1439         return false;
    1440     }
    1441
    1442     /**
    1443      * Recomputes the bounds from scratch and saves the cached bounds.
    1444      */
    1445     void recomputeBounds() {
    1446         // fast path for case of no children
    1447         if (children.isEmpty()) {
    1448             if (dirtyChildren != null) dirtyChildren.clear();
    1449             cachedBoundsInvalid = false;
    1450             cachedBounds.makeEmpty();
    1451             return;
    1452         }
    1453
    1454         // fast path for case of 1 child
    1455         if (children.size() == 1) {
    1456             if (dirtyChildren != null) dirtyChildren.clear();
    1457             cachedBoundsInvalid = false;
    1458             Node node = children.get(0);
    1459             if (node == null) return;
    1460
    1461             if (node.isVisible()) {
    1462                 cachedBounds = node.getTransformedBounds(cachedBounds, BaseTransform.IDENTITY_TRANSFORM);
    1463             } else {
    1464                 cachedBounds.makeEmpty();
    1465             }
    1466             node.boundsChanged = false;
    1467             return;
    1468         }
    1469
    1470         // We will attempt to use a fastpath through this function. We can use
    1471         // the fastpath as long as we have an old size of the Group recorded
    1472         // (cachedBounds is invalid but not empty), and each of the edge
    1473         // variables have a value.
    1474         if (cachedBounds != null
    1475             && !cachedBounds.isEmpty()
    1476             && top != null
    1477             && left != null
    1478             && bottom != null
    1479             && right != null
    1480             && near != null
    1481             && far != null)
    1482         {
    1483             // The fastpath will simply iterate over each of the Nodes in the
    1484             // group. If any node has "dirty" bounds, then I know that I must
    1485             // recompute its bounds. When I do so, I will take special care
    1486             // if it was once one of my edges, and also special care if it has
    1487             // moved to become a new edge.
    1488
    1489             // First, look at the nodes which form the top, left, bottom, right,
    1490             // near and far edges. If any of them are now invisible (visible = false)
    1491             // then they no longer contribute to the bounds of the group and
    1492             // therefore, this fast path cannot be used
    1493             if (top.isVisible() && left.isVisible() && bottom.isVisible() && right.isVisible()
    1494                     && near.isVisible() && far.isVisible()) {
    1495                 // This fastpath becomes degenerate if I get to the end only to
    1496                 // discover that the very last node has moved inward, in which
    1497                 // case I have to throw away my work and simply sum up all the
    1498                 // nodes in the group.
    1499                 boolean quit = false;
    1500
    1501                 // These are local references to the cached bounds vars
    1502                 double cx = cachedBounds.getMinX();
    1503                 double cy = cachedBounds.getMinY();
    1504                 double cz = cachedBounds.getMinZ();
    1505                 double cx2 = cachedBounds.getMaxX();
    1506                 double cy2 = cachedBounds.getMaxY();
    1507                 double cz2 = cachedBounds.getMaxZ();
    1508
    1509                 // These indicate the bounds of the Group as computed by this
    1510                 // function (had to add the 'b' suffix to suppress a spurious
    1511                 // compiler error message)
    1512                 double minXb=cx;
    1513                 double minYb=cy;
    1514                 double minZb=cz;
    1515                 double maxXb=cx2;
    1516                 double maxYb=cy2;
    1517                 double maxZb=cz2;
    1518
    1519                 // iterate over all the dirty nodes
    1520                 if (dirtyChildren != null) {
    1521                     for (Node node : dirtyChildren) {
    1522                         if (node.isVisible() && node.boundsChanged) {
    1523                             tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
    1524                             if (tmp.isEmpty()) {
    1525                                 // If this node formed one of the edges, and if it has
    1526                                 // moved off that edge (inward), then we break out of this
    1527                                 // loop and quit the fast path
    1528                                 if (node == top) { quit = true; break; }
    1529                                 if (node == left) { quit = true; break; }
    1530                                 if (node == bottom) { quit = true; break; }
    1531                                 if (node == right) { quit = true; break; }
    1532                                 if (node == near) { quit = true; break; }
    1533                                 if (node == far) { quit = true; break; }
    1534                             } else {
    1535                                 double tmpx = tmp.getMinX();
    1536                                 double tmpy = tmp.getMinY();
    1537                                 double tmpz = tmp.getMinZ();
    1538                                 double tmpx2 = tmp.getMaxX();
    1539                                 double tmpy2 = tmp.getMaxY();
    1540                                 double tmpz2 = tmp.getMaxZ();
    1541
    1542                                 // If this node formed one of the edges, and if it has
    1543                                 // moved off that edge (inward), then we break out of this
    1544                                 // loop and quit the fast path
    1545                                 if (node == top && tmpy > cy) { quit = true; break; }
    1546                                 if (node == left && tmpx > cx) { quit = true; break; }
    1547                                 if (node == near && tmpz > cz) { quit = true; break; }
    1548                                 if (node == bottom && tmpy2 < cy2) { quit = true; break; }
    1549                                 if (node == right && tmpx2 < cx2) { quit = true; break; }
    1550                                 if (node == far && tmpz2 < cz2) { quit = true; break; }
    1551
    1552                                 // If this node forms an edge, then we will set it to be the
    1553                                 // node for this edge and update the min/max values
    1554                                 if (tmpy < minYb) { minYb = tmpy; top = node; }
    1555                                 if (tmpx < minXb) { minXb = tmpx; left = node; }
    1556                                 if (tmpz < minZb) { minZb = tmpz; near = node; }
    1557                                 if (tmpy2 > maxYb) { maxYb = tmpy2; bottom = node; }
    1558                                 if (tmpx2 > maxXb) { maxXb = tmpx2; right = node; }
    1559                                 if (tmpz2 > maxZb) { maxZb = tmpz2; far = node; }
    1560                             }
    1561                         }
    1562                         node.boundsChanged = false;
    1563                     }
    1564                 } else {
    1565                     for (int i=0, max=children.size(); i<max; i++) {
    1566                         final Node node = children.get(i);
    1567                         if (node == null)
    1568                             continue;
    1569                         if (node.isVisible() && node.boundsChanged) {
    1570                             tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
    1571                             if (tmp.isEmpty()) {
    1572                                 // If this node formed one of the edges, and if it has
    1573                                 // moved off that edge (inward), then we break out of this
    1574                                 // loop and quit the fast path
    1575                                 if (node == top) { quit = true; break; }
    1576                                 if (node == left) { quit = true; break; }
    1577                                 if (node == bottom) { quit = true; break; }
    1578                                 if (node == right) { quit = true; break; }
    1579                                 if (node == near) { quit = true; break; }
    1580                                 if (node == far) { quit = true; break; }
    1581                             } else {
    1582                                 double tmpx = tmp.getMinX();
    1583                                 double tmpy = tmp.getMinY();
    1584                                 double tmpz = tmp.getMinZ();
    1585                                 double tmpx2 = tmp.getMaxX();
    1586                                 double tmpy2 = tmp.getMaxY();
    1587                                 double tmpz2 = tmp.getMaxZ();
    1588
    1589                                 // If this node formed one of the edges, and if it has
    1590                                 // moved off that edge (inward), then we break out of this
    1591                                 // loop and quit the fast path
    1592                                 if (node == top && tmpy > cy) { quit = true; break; }
    1593                                 if (node == left && tmpx > cx) { quit = true; break; }
    1594                                 if (node == near && tmpz > cz) { quit = true; break; }
    1595                                 if (node == bottom && tmpy2 < cy2) { quit = true; break; }
    1596                                 if (node == right && tmpx2 < cx2) { quit = true; break; }
    1597                                 if (node == far && tmpz2 < cz2) { quit = true; break; }
    1598
    1599                                 // If this node forms an edge, then we will set it to be the
    1600                                 // node for this edge and update the min/max values
    1601                                 if (tmpy < minYb) { minYb = tmpy; top = node; }
    1602                                 if (tmpx < minXb) { minXb = tmpx; left = node; }
    1603                                 if (tmpz < minZb) { minZb = tmpz; near = node; }
    1604                                 if (tmpy2 > maxYb) { maxYb = tmpy2; bottom = node; }
    1605                                 if (tmpx2 > maxXb) { maxXb = tmpx2; right = node; }
    1606                                 if (tmpz2 > maxZb) { maxZb = tmpz2; far = node; }
    1607                             }
    1608                         }
    1609                         node.boundsChanged = false;
    1610                     }
    1611                 }
    1612
    1613                 if (dirtyChildren != null) dirtyChildren.clear();
    1614
    1615                 if (!quit) {
    1616                     // The fastpath succeeded, so we can simply update the
    1617                     // cachedBounds, clear the cachedBoundsInvalid flag and return
    1618                     cachedBoundsInvalid = false;
    1619                     cachedBounds = cachedBounds.deriveWithNewBounds((float)minXb, (float)minYb, (float)minZb,
    1620                             (float)maxXb, (float)maxYb, (float)maxZb);
    1621                     return;
    1622                 }
    1623             }
    1624             // fall through to the long case
    1625         }
    1626
    1627         // These indicate the bounds of the Group as computed by this function
    1628         double minX = 0;
    1629         double minY = 0;
    1630         double minZ = 0;
    1631         double maxX = 0;
    1632         double maxY = 0;
    1633         double maxZ = 0;
    1634
    1635         // The long case is pretty simple -- visit each and every node, get
    1636         // the bounds of each node, figure out the edge nodes, get the min/max
    1637         // for the x/y/x2/y2 and then update cachedBounds and cachedBoundsInvalid
    1638         boolean first = true;
    1639         for (int i=0, max=children.size(); i<max; i++) {
    1640             final Node node = children.get(i);
    1641             if (node == null)
    1642                 continue;
    1643
    1644             node.boundsChanged = false;
    1645             if (node.isVisible()) {
    1646                 tmp = node.getTransformedBounds(tmp, BaseTransform.IDENTITY_TRANSFORM);
    1647                 if (!tmp.isEmpty()) {
    1648                     double tmpx = tmp.getMinX();
    1649                     double tmpy = tmp.getMinY();
    1650                     double tmpz = tmp.getMinZ();
    1651                     double tmpx2 = tmp.getMaxX();
    1652                     double tmpy2 = tmp.getMaxY();
    1653                     double tmpz2 = tmp.getMaxZ();
    1654
    1655                     if (first) {
    1656                         top = left = bottom = right = near = far = node;
    1657                         minX = tmpx;
    1658                         minY = tmpy;
    1659                         minZ = tmpz;
    1660                         maxX = tmpx2;
    1661                         maxY = tmpy2;
    1662                         maxZ = tmpz2;
    1663                         first = false;
    1664                     } else {
    1665                         if (tmpy < minY) { minY = tmpy; top = node; }
    1666                         if (tmpx < minX) { minX = tmpx; left = node; }
    1667                         if (tmpz < minZ) { minZ = tmpz; near = node; }
    1668                         if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; }
    1669                         if (tmpx2 > maxX) { maxX = tmpx2; right = node; }
    1670                         if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; }
    1671                     }
    1672                 }
    1673             }
    1674         }
    1675
    1676         if (dirtyChildren != null) dirtyChildren.clear();
    1677         cachedBoundsInvalid = false;
    1678         if (first) {
    1679             top = left = bottom = right = near = far = null;
    1680             cachedBounds.makeEmpty();
    1681         } else {
    1682             cachedBounds = cachedBounds.deriveWithNewBounds((float)minX, (float)minY, (float)minZ,
    1683                     (float)maxX, (float)maxY, (float)maxZ);
    1684         }
    1685     }
    1686
    1687     /**
    1688      * Overridden to make sure boundsInvalid gets set to true
    1689      *
    1690      * @treatAsPrivate implementation detail
    1691      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1692      */
    1693     @Deprecated
    1694     @Override protected void impl_geomChanged() {
    1695         cachedBoundsInvalid = true;
    1696         super.impl_geomChanged();
    1697     }
    1698
    1699     /**
    1700      * Called by Node whenever its bounds have changed.
    1701      */
    1702     void childBoundsChanged(Node node) {
    1703         if (!node.isVisible()) {
    1704             // the change in bounds of this child node has no bearing on
    1705             // the bounds of the group, so forget it
    1706             return;
    1707         }
    1708
    1709         // mark the node such that the parent knows that the child's bounds
    1710         // are not in sync with this parent. In this way, when the bounds
    1711         // need to be computed, we'll come back and figure out the new bounds
    1712         // for all the children which have boundsChanged set to true
    1713         if (!node.boundsChanged) {
    1714             node.boundsChanged = true;
    1715             if (dirtyChildren != null) dirtyChildren.add(node);
    1716         }
    1717         // go ahead and indicate that the geom has changed for this parent,
    1718         // even though once we figure it all out it may be that the bounds
    1719         // have not changed
    1720         impl_geomChanged();
    1721     }
    1722
    1723     /**
    1724      * Called by node whenever the visibility of the node changes.
    1725      */
    1726     void childVisibilityChanged(Node node) {
    1727         // cachedBoundsInvalid will be set to true in impl_geomChanged();
    1728         cachedBounds.makeEmpty();
    1729         impl_geomChanged();
    1730     }
    1731
    1732     /**
    1733      * @treatAsPrivate implementation detail
    1734      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1735      */
    1736     @Deprecated
    1737     @Override
    1738     protected boolean impl_computeContains(double localX, double localY) {
    1739         final Point2D tempPt = TempState.getInstance().point;
    1740         for (int i=0, max=children.size(); i<max; i++) {
    1741             final Node node = children.get(i);
    1742             tempPt.x = (float)localX;
    1743             tempPt.y = (float)localY;
    1744             try {
    1745                 node.parentToLocal(tempPt);
    1746             } catch (NoninvertibleTransformException e) {
    1747                 continue;
    1748             }
    1749             if (node.contains(tempPt.x, tempPt.y)) {
    1750                 return true;
    1751             }
    1752         }
    1753         return false;
    1754     }
    1755
    1756     /**
    1757      * @treatAsPrivate implementation detail
    1758      * @deprecated This is an internal API that is not intended for use and will be removed in the next version
    1759      */
    1760     @Deprecated
    1761     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
    1762         return alg.processContainerNode(this, ctx);
    1763     }
    1764 }