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