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