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