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