/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javafx.scene; import com.sun.javafx.scene.traversal.ParentTraversalEngine; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.WritableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener.Change; import javafx.collections.ObservableList; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import com.sun.javafx.util.TempState; import com.sun.javafx.util.Utils; import com.sun.javafx.collections.TrackableObservableList; import com.sun.javafx.collections.VetoableListDecorator; import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection; import javafx.css.Selector; import com.sun.javafx.css.StyleManager; import com.sun.javafx.geom.BaseBounds; import com.sun.javafx.geom.PickRay; import com.sun.javafx.geom.Point2D; import com.sun.javafx.geom.RectBounds; import com.sun.javafx.geom.transform.BaseTransform; import com.sun.javafx.geom.transform.NoninvertibleTransformException; import com.sun.javafx.jmx.MXNodeAlgorithm; import com.sun.javafx.jmx.MXNodeAlgorithmContext; import com.sun.javafx.scene.CssFlags; import com.sun.javafx.scene.DirtyBits; import com.sun.javafx.scene.input.PickResultChooser; import com.sun.javafx.sg.prism.NGGroup; import com.sun.javafx.sg.prism.NGNode; import com.sun.javafx.tk.Toolkit; import com.sun.javafx.scene.LayoutFlags; import javafx.stage.Window; /** * The base class for all nodes that have children in the scene graph. *

* This class handles all hierarchical scene graph operations, including adding/removing * child nodes, marking branches dirty for layout and rendering, picking, * bounds calculations, and executing the layout pass on each pulse. *

* There are two direct concrete Parent subclasses *

* * @since JavaFX 2.0 */ public abstract class Parent extends Node { // package private for testing static final int DIRTY_CHILDREN_THRESHOLD = 10; // If set to true, generate a warning message whenever adding a node to a // parent if it is currently a child of another parent. private static final boolean warnOnAutoMove = PropertyHelper.getBooleanProperty("javafx.sg.warn"); /** * Threshold when it's worth to populate list of removed children. */ private static final int REMOVED_CHILDREN_THRESHOLD = 20; /** * Do not populate list of removed children when its number exceeds threshold, * but mark whole parent dirty. */ private boolean removedChildrenOptimizationDisabled = false; /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated @Override public void impl_updatePeer() { super.impl_updatePeer(); final NGGroup peer = impl_getPeer(); if (Utils.assertionEnabled()) { List pgnodes = peer.getChildren(); if (pgnodes.size() != pgChildrenSize) { java.lang.System.err.println("*** pgnodes.size() [" + pgnodes.size() + "] != pgChildrenSize [" + pgChildrenSize + "]"); } } if (impl_isDirty(DirtyBits.PARENT_CHILDREN)) { // Whether a permutation, or children having been added or // removed, we'll want to clear out the PG side starting // from startIdx. We know that everything up to but not // including startIdx is identical between the FX and PG // sides, so we only need to update the remaining portion. peer.clearFrom(startIdx); for (int idx = startIdx; idx < children.size(); idx++) { peer.add(idx, children.get(idx).impl_getPeer()); } if (removedChildrenOptimizationDisabled) { peer.markDirty(); removedChildrenOptimizationDisabled = false; } else { if (removed != null && !removed.isEmpty()) { for(int i = 0; i < removed.size(); i++) { peer.addToRemoved(removed.get(i).impl_getPeer()); } } } if (removed != null) { removed.clear(); } pgChildrenSize = children.size(); startIdx = pgChildrenSize; } if (Utils.assertionEnabled()) validatePG(); } /*********************************************************************** * Scenegraph Structure * * * * Functions and variables related to the scenegraph structure, * * modifying the structure, and walking the structure. * * * **********************************************************************/ // Used to check for duplicate nodes private final Set childSet = new HashSet(); // starting child index from which we need to send the children to the PGGroup private int startIdx = 0; // double of children in the PGGroup as of the last update private int pgChildrenSize = 0; void validatePG() { boolean assertionFailed = false; final NGGroup peer = impl_getPeer(); List pgnodes = peer.getChildren(); if (pgnodes.size() != children.size()) { java.lang.System.err.println("*** pgnodes.size validatePG() [" + pgnodes.size() + "] != children.size() [" + children.size() + "]"); assertionFailed = true; } else { for (int idx = 0; idx < children.size(); idx++) { Node n = children.get(idx); if (n.getParent() != this) { java.lang.System.err.println("*** this=" + this + " validatePG children[" + idx + "].parent= " + n.getParent()); assertionFailed = true; } if (n.impl_getPeer() != pgnodes.get(idx)) { java.lang.System.err.println("*** pgnodes[" + idx + "] validatePG != children[" + idx + "]"); assertionFailed = true; } } } if (assertionFailed) { throw new java.lang.AssertionError("validation of PGGroup children failed"); } } void printSeq(String prefix, List nodes) { String str = prefix; for (Node nn : nodes) { str += nn + " "; } System.out.println(str); } // Variable used to indicate that the change to the children ObservableList is // a simple permutation as the result of a toFront or toBack operation. // We can avoid almost all of the processing of the on replace trigger in // this case. private boolean childrenTriggerPermutation = false; //accumulates all removed nodes between pulses, for dirty area calculation. private List removed; /** * A ObservableList of child {@code Node}s. *

* See the class documentation for {@link Node} for scene graph structure * restrictions on setting a {@link Parent}'s children ObservableList. * If these restrictions are violated by a change to the children ObservableList, * the change is ignored and the previous value of the child ObservableList is * restored. * * {@code

Throws AssignToBoundException} if the same node * appears in two different bound ObservableList. * * @defaultValue empty */ // set to true if either childRemoved or childAdded returns // true. These functions will indicate whether the geom // bounds for the parent have changed private boolean geomChanged; private boolean childSetModified; private final ObservableList children = new VetoableListDecorator(new TrackableObservableList() { protected void onChanged(Change c) { // proceed with updating the scene graph unmodifiableManagedChildren = null; boolean relayout = false; if (childSetModified) { while (c.next()) { int from = c.getFrom(); int to = c.getTo(); for (int i = from; i < to; ++i) { Node n = children.get(i); if (n.getParent() != null && n.getParent() != Parent.this) { if (warnOnAutoMove) { java.lang.System.err.println("WARNING added to a new parent without first removing it from its current"); java.lang.System.err.println(" parent. It will be automatically removed from its current parent."); java.lang.System.err.println(" node=" + n + " oldparent= " + n.getParent() + " newparent=" + this); } n.getParent().children.remove(n); if (warnOnAutoMove) { Thread.dumpStack(); } } } List removed = c.getRemoved(); int removedSize = removed.size(); for (int i = 0; i < removedSize; ++i) { final Node n = removed.get(i); if (n.isManaged()) { relayout = true; } } // update the parent and scene for each new node for (int i = from; i < to; ++i) { Node node = children.get(i); if (node.isManaged()) { relayout = true; } node.setParent(Parent.this); node.setScenes(getScene(), getSubScene()); // assert !node.boundsChanged; if (node.isVisible()) { geomChanged = true; childIncluded(node); } } } // check to see if the number of children exceeds // DIRTY_CHILDREN_THRESHOLD and dirtyChildren is null. // If so, then we need to create dirtyChildren and // populate it. if (dirtyChildren == null && children.size() > DIRTY_CHILDREN_THRESHOLD) { dirtyChildren = new ArrayList(2 * DIRTY_CHILDREN_THRESHOLD); // only bother populating children if geom has // changed, otherwise there is no need if (dirtyChildrenCount > 0) { int size = children.size(); for (int i = 0; i < size; ++i) { Node ch = children.get(i); if (ch.isVisible() && ch.boundsChanged) { dirtyChildren.add(ch); } } } } } else { // If childSet was not modified, we still need to check whether the permutation // did change the layout layout_loop:while (c.next()) { List removed = c.getRemoved(); for (int i = 0, removedSize = removed.size(); i < removedSize; ++i) { if (removed.get(i).isManaged()) { relayout = true; break layout_loop; } } for (int i = c.getFrom(), to = c.getTo(); i < to; ++i) { if (children.get(i).isManaged()) { relayout = true; break layout_loop; } } } } // // Note that the styles of a child do not affect the parent or // its siblings. Thus, it is only necessary to reapply css to // the Node just added and not to this parent and all of its // children. So the following call to impl_reapplyCSS was moved // to Node.parentProperty. The original comment and code were // purposely left here as documentation should there be any // question about how the code used to work and why the change // was made. // // if children have changed then I need to reapply // CSS from this node on down // impl_reapplyCSS(); // // request layout if a Group subclass has overridden doLayout OR // if one of the new children needs layout, in which case need to ensure // the needsLayout flag is set all the way to the root so the next layout // pass will reach the child. if (relayout) { requestLayout(); } if (geomChanged) { impl_geomChanged(); } // Note the starting index at which we need to update the // PGGroup on the next update, and mark the children dirty c.reset(); c.next(); if (startIdx > c.getFrom()) { startIdx = c.getFrom(); } impl_markDirty(DirtyBits.PARENT_CHILDREN); // Force synchronization to include the handling of invisible node // so that removed list will get cleanup to prevent memory leak. impl_markDirty(DirtyBits.NODE_FORCE_SYNC); } }) { @Override protected void onProposedChange(final List newNodes, int[] toBeRemoved) { final Scene scene = getScene(); if (scene != null) { Window w = scene.getWindow(); if (w != null && w.impl_getPeer() != null) { Toolkit.getToolkit().checkFxUserThread(); } } geomChanged = false; long newLength = children.size() + newNodes.size(); int removedLength = 0; for (int i = 0; i < toBeRemoved.length; i += 2) { removedLength += toBeRemoved[i + 1] - toBeRemoved[i]; } newLength -= removedLength; // If the childrenTriggerPermutation flag is set, then we know it // is a simple permutation and no further checking is needed. if (childrenTriggerPermutation) { childSetModified = false; return; } // If the childrenTriggerPermutation flag is not set, then we will // check to see whether any element in the ObservableList has changed, // or whether the new ObservableList is a permutation on the existing // ObservableList. Note that even if the childrenModified flag is false, // we still have to check for duplicates. If it is a simple // permutation, we can avoid checking for cycles or other parents. childSetModified = true; if (newLength == childSet.size()) { childSetModified = false; for (int i = newNodes.size() - 1; i >= 0; --i ) { Node n = newNodes.get(i); if (!childSet.contains(n)) { childSetModified = true; break; } } } // Enforce scene graph invariants, and check for structural errors. // // 1. If a child has been added to this parent more than once, // then it is an error // // 2. If a child is a target of a clip, then it is an error. // // 3. If a node would cause a cycle, then it is an error. // // 4. If a node is null // // Note that if a node is the child of another parent, we will // implicitly remove the node from its former Parent after first // checking for errors. // iterate over the nodes that were removed and remove them from // the hash set. for (int i = 0; i < toBeRemoved.length; i += 2) { for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) { childSet.remove(children.get(j)); } } try { if (childSetModified) { // check individual children before duplication test // if done in this order, the exception is more specific for (int i = newNodes.size() - 1; i >= 0; --i ) { Node node = newNodes.get(i); if (node == null) { throw new NullPointerException( constructExceptionMessage( "child node is null", null)); } if (node.getClipParent() != null) { throw new IllegalArgumentException( constructExceptionMessage( "node already used as a clip", node)); } if (wouldCreateCycle(Parent.this, node)) { throw new IllegalArgumentException( constructExceptionMessage( "cycle detected", node)); } } } childSet.addAll(newNodes); if (childSet.size() != newLength) { throw new IllegalArgumentException( constructExceptionMessage( "duplicate children added", null)); } } catch (RuntimeException e) { //Return children to it's original state childSet.clear(); childSet.addAll(children); // rethrow throw e; } // Done with error checking if (!childSetModified) { return; } // iterate over the nodes that were removed and clear their // parent and scene. Add to them also to removed list for further // dirty regions calculation. if (removed == null) { removed = new ArrayList(); } if (removed.size() + removedLength > REMOVED_CHILDREN_THRESHOLD || !impl_isTreeVisible()) { //do not populate too many children in removed list removedChildrenOptimizationDisabled = true; } for (int i = 0; i < toBeRemoved.length; i += 2) { for (int j = toBeRemoved[i]; j < toBeRemoved[i + 1]; j++) { Node old = children.get(j); final Scene oldScene = old.getScene(); if (oldScene != null) { oldScene.generateMouseExited(old); } if (dirtyChildren != null) { dirtyChildren.remove(old); } if (old.isVisible()) { geomChanged = true; childExcluded(old); } if (old.getParent() == Parent.this) { old.setParent(null); old.setScenes(null, null); } if (!removedChildrenOptimizationDisabled) { removed.add(old); } } } } private String constructExceptionMessage( String cause, Node offendingNode) { final StringBuilder sb = new StringBuilder("Children: "); sb.append(cause); sb.append(": parent = ").append(Parent.this); if (offendingNode != null) { sb.append(", node = ").append(offendingNode); } return sb.toString(); } }; /** * A constant reference to an unmodifiable view of the children, such that every time * we ask for an unmodifiable list of children, we don't actually create a new * collection and return it. The memory overhead is pretty lightweight compared * to all the garbage we would otherwise generate. */ private final ObservableList unmodifiableChildren = FXCollections.unmodifiableObservableList(children); /** * A cached reference to the unmodifiable managed children of this Parent. This is * created whenever first asked for, and thrown away whenever children are added * or removed or when their managed state changes. This could be written * differently, such that this list is essentially a filtered copy of the * main children, but that additional overhead might not be worth it. */ private List unmodifiableManagedChildren = null; /** * Gets the list of children of this {@code Parent}. * *

* See the class documentation for {@link Node} for scene graph structure * restrictions on setting a {@link Parent}'s children list. * If these restrictions are violated by a change to the list of children, * the change is ignored and the previous value of the children list is * restored. An {@link IllegalArgumentException} is thrown in this case. * *

* If this {@link Parent} node is attached to a {@link Scene} attached to a {@link Window} * that is showning ({@link javafx.stage.Window#isShowing()}), then its * list of children must only be modified on the JavaFX Application Thread. * An {@link IllegalStateException} is thrown if this restriction is * violated. * *

* Note to subclasses: if you override this method, you must return from * your implementation the result of calling this super method. The actual * list instance returned from any getChildren() implementation must be * the list owned and managed by this Parent. The only typical purpose * for overriding this method is to promote the method to be public. * * @return the list of children of this {@code Parent}. */ protected ObservableList getChildren() { return children; } /** * Gets the list of children of this {@code Parent} as a read-only * list. * * @return read-only access to this parent's children ObservableList */ @ReturnsUnmodifiableCollection public ObservableList getChildrenUnmodifiable() { return unmodifiableChildren; } /** * Gets the list of all managed children of this {@code Parent}. * * @param the type of the children nodes * @return list of all managed children in this parent */ @ReturnsUnmodifiableCollection protected List getManagedChildren() { if (unmodifiableManagedChildren == null) { unmodifiableManagedChildren = new ArrayList(); for (int i=0, max=children.size(); i)unmodifiableManagedChildren; } /** * Called by Node whenever its managed state may have changed, this * method will cause the view of managed children to be updated * such that it properly includes or excludes this child. */ final void managedChildChanged() { requestLayout(); unmodifiableManagedChildren = null; } // implementation of Node.toFront function final void impl_toFront(Node node) { if (Utils.assertionEnabled()) { if (!childSet.contains(node)) { throw new java.lang.AssertionError( "specified node is not in the list of children"); } } if (children.get(children.size() - 1) != node) { childrenTriggerPermutation = true; try { children.remove(node); children.add(node); } finally { childrenTriggerPermutation = false; } } } // implementation of Node.toBack function final void impl_toBack(Node node) { if (Utils.assertionEnabled()) { if (!childSet.contains(node)) { throw new java.lang.AssertionError( "specified node is not in the list of children"); } } if (children.get(0) != node) { childrenTriggerPermutation = true; try { children.remove(node); children.add(0, node); } finally { childrenTriggerPermutation = false; } } } @Override void scenesChanged(final Scene newScene, final SubScene newSubScene, final Scene oldScene, final SubScene oldSubScene) { if (oldScene != null && newScene == null) { // RT-34863 - clean up CSS cache when Parent is removed from scene-graph StyleManager.getInstance().forget(this); } for (int i=0; i= 0; i--) { children.get(i).impl_pickNode(pickRay, result); if (result.isClosed()) { return; } } if (isPickOnBounds()) { result.offer(this, boundsDistance, PickResultChooser.computePoint(pickRay, boundsDistance)); } } } @Override boolean isConnected() { return super.isConnected() || sceneRoot; } @Override public Node lookup(String selector) { Node n = super.lookup(selector); if (n == null) { for (int i=0, max=children.size(); i lookupAll(Selector selector, List results) { results = super.lookupAll(selector, results); for (int i=0, max=children.size(); i impl_traversalEngine; /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ // SB-dependency: RT-21209 has been filed to track this @Deprecated public final void setImpl_traversalEngine(ParentTraversalEngine value) { impl_traversalEngineProperty().set(value); } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated public final ParentTraversalEngine getImpl_traversalEngine() { return impl_traversalEngine == null ? null : impl_traversalEngine.get(); } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated public final ObjectProperty impl_traversalEngineProperty() { if (impl_traversalEngine == null) { impl_traversalEngine = new SimpleObjectProperty<>( this, "impl_traversalEngine"); } return impl_traversalEngine; } /*********************************************************************** * Layout * * * * Functions and variables related to the layout scheme used by * * JavaFX. Includes both public and private API. * * * **********************************************************************/ /** * Indicates that this Node and its subnodes requires a layout pass on * the next pulse. */ private ReadOnlyBooleanWrapper needsLayout; LayoutFlags layoutFlag = LayoutFlags.CLEAN; protected final void setNeedsLayout(boolean value) { if (value) { markDirtyLayout(true); } else if (layoutFlag == LayoutFlags.NEEDS_LAYOUT) { boolean hasBranch = false; for (int i = 0, max = children.size(); i < max; i++) { final Node child = children.get(i); if (child instanceof Parent) { if (((Parent)child).layoutFlag != LayoutFlags.CLEAN) { hasBranch = true; break; } } } setLayoutFlag(hasBranch ? LayoutFlags.DIRTY_BRANCH : LayoutFlags.CLEAN); } } public final boolean isNeedsLayout() { return layoutFlag == LayoutFlags.NEEDS_LAYOUT; } public final ReadOnlyBooleanProperty needsLayoutProperty() { if (needsLayout == null) { needsLayout = new ReadOnlyBooleanWrapper(this, "needsLayout", layoutFlag == LayoutFlags.NEEDS_LAYOUT); } return needsLayout; } /** * This package levelis used only by Node. It is set to true while * the layout() function is processing and set to false on the conclusion. * It is used by the Node to decide whether to perform CSS updates * synchronously or asynchronously. */ boolean performingLayout = false; private boolean sizeCacheClear = true; private double prefWidthCache = -1; private double prefHeightCache = -1; private double minWidthCache = -1; private double minHeightCache = -1; void setLayoutFlag(LayoutFlags flag) { if (needsLayout != null) { needsLayout.set(flag == LayoutFlags.NEEDS_LAYOUT); } layoutFlag = flag; } private void markDirtyLayout(boolean local) { setLayoutFlag(LayoutFlags.NEEDS_LAYOUT); if (local || layoutRoot) { if (sceneRoot) { Toolkit.getToolkit().requestNextPulse(); if (getSubScene() != null) { getSubScene().setDirtyLayout(this); } } else { markDirtyLayoutBranch(); } } else { requestParentLayout(); } } /** * Requests a layout pass to be performed before the next scene is * rendered. This is batched up asynchronously to happen once per * "pulse", or frame of animation. *

* If this parent is either a layout root or unmanaged, then it will be * added directly to the scene's dirty layout list, otherwise requestParentLayout * will be invoked. * @since JavaFX 8.0 */ public void requestLayout() { clearSizeCache(); markDirtyLayout(false); } /** * Requests a layout pass of the parent to be performed before the next scene is * rendered. This is batched up asynchronously to happen once per * "pulse", or frame of animation. *

* This may be used when the current parent have changed it's min/max/preferred width/height, * but doesn't know yet if the change will lead to it's actual size change. This will be determined * when it's parent recomputes the layout with the new hints. */ protected final void requestParentLayout() { if (!layoutRoot) { final Parent parent = getParent(); if (parent != null && !parent.performingLayout) { parent.requestLayout(); } } } void clearSizeCache() { if (sizeCacheClear) { return; } sizeCacheClear = true; prefWidthCache = -1; prefHeightCache = -1; minWidthCache = -1; minHeightCache = -1; } @Override public double prefWidth(double height) { if (height == -1) { if (prefWidthCache == -1) { prefWidthCache = computePrefWidth(-1); if (Double.isNaN(prefWidthCache) || prefWidthCache < 0) prefWidthCache = 0; sizeCacheClear = false; } return prefWidthCache; } else { double result = computePrefWidth(height); return Double.isNaN(result) || result < 0 ? 0 : result; } } @Override public double prefHeight(double width) { if (width == -1) { if (prefHeightCache == -1) { prefHeightCache = computePrefHeight(-1); if (Double.isNaN(prefHeightCache) || prefHeightCache < 0) prefHeightCache = 0; sizeCacheClear = false; } return prefHeightCache; } else { double result = computePrefHeight(width); return Double.isNaN(result) || result < 0 ? 0 : result; } } @Override public double minWidth(double height) { if (height == -1) { if (minWidthCache == -1) { minWidthCache = computeMinWidth(-1); if (Double.isNaN(minWidthCache) || minWidthCache < 0) minWidthCache = 0; sizeCacheClear = false; } return minWidthCache; } else { double result = computeMinWidth(height); return Double.isNaN(result) || result < 0 ? 0 : result; } } @Override public double minHeight(double width) { if (width == -1) { if (minHeightCache == -1) { minHeightCache = computeMinHeight(-1); if (Double.isNaN(minHeightCache) || minHeightCache < 0) minHeightCache = 0; sizeCacheClear = false; } return minHeightCache; } else { double result = computeMinHeight(width); return Double.isNaN(result) || result < 0 ? 0 : result; } } // PENDING_DOC_REVIEW /** * Calculates the preferred width of this {@code Parent}. The default * implementation calculates this width as the width of the area occupied * by its managed children when they are positioned at their * current positions at their preferred widths. * * @param height the height that should be used if preferred width depends * on it * @return the calculated preferred width */ protected double computePrefWidth(double height) { double minX = 0; double maxX = 0; for (int i=0, max=children.size(); i * Subclasses should override this function to layout content as needed. */ protected void layoutChildren() { for (int i=0, max=children.size(); iCSS Reference * Guide. */ private final ObservableList stylesheets = new TrackableObservableList() { @Override protected void onChanged(Change c) { final Scene scene = getScene(); if (scene != null) { // Notify the StyleManager if stylesheets change. This Parent's // styleManager will get recreated in impl_processCSS. StyleManager.getInstance().stylesheetsChanged(Parent.this, c); // RT-9784 - if stylesheet is removed, reset styled properties to // their initial value. c.reset(); while(c.next()) { if (c.wasRemoved() == false) { continue; } break; // no point in resetting more than once... } impl_reapplyCSS(); } } }; /** * Gets an observable list of string URLs linking to the stylesheets to use * with this Parent's contents. See {@link Scene#getStylesheets()} for details. *

For additional information about using CSS * with the scene graph, see the CSS Reference * Guide.

* * @return the list of stylesheets to use with this Parent * @since JavaFX 2.1 */ public final ObservableList getStylesheets() { return stylesheets; } /** * This method recurses up the parent chain until parent is null. As the * stack unwinds, if the Parent has stylesheets, they are added to the * list. * * It is possible to override this method to stop the recursion. This allows * a Parent to have a set of stylesheets distinct from its Parent. * * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated // SB-dependency: RT-21247 has been filed to track this public /* Do not make this final! */ List impl_getAllParentStylesheets() { List list = null; final Parent myParent = getParent(); if (myParent != null) { // // recurse so that stylesheets of Parents closest to the root are // added to the list first. The ensures that declarations for // stylesheets further down the tree (closer to the leaf) have // a higer ordinal in the cascade. // list = myParent.impl_getAllParentStylesheets(); } if (stylesheets != null && stylesheets.isEmpty() == false) { if (list == null) { list = new ArrayList(stylesheets.size()); } for (int n=0,nMax=stylesheets.size(); n unused) { // Nothing to do... if (cssFlag == CssFlags.CLEAN) return; // RT-29254 - If DIRTY_BRANCH, pass control to Node#processCSS. This avoids calling impl_processCSS on // this node and all of its children when css doesn't need updated, recalculated, or reapplied. if (cssFlag == CssFlags.DIRTY_BRANCH) { super.processCSS(); return; } // Let the super implementation handle CSS for this node super.impl_processCSS(unused); // avoid the following call to children.toArray if there are no children if (children.isEmpty()) return; // // RT-33103 // // It is possible for a child to be removed from children in the middle of // the following loop. Iterating over the children may result in an IndexOutOfBoundsException. // So a copy is made and the copy is iterated over. // // Note that we don't want the fail-fast feature of an iterator, not to mention the general iterator overhead. // final Node[] childArray = children.toArray(new Node[children.size()]); // For each child, process CSS for (int i=0; i 0) { child.cssFlag = CssFlags.UPDATE; } child.impl_processCSS(unused); } } /*********************************************************************** * Misc * * * * Initialization and other functions * * * **********************************************************************/ /** * Constructs a new {@code Parent}. */ protected Parent() { layoutFlag = LayoutFlags.NEEDS_LAYOUT; setAccessibleRole(AccessibleRole.PARENT); } /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated @Override protected NGNode impl_createPeer() { return new NGGroup(); } @Override void nodeResolvedOrientationChanged() { for (int i = 0, max = children.size(); i < max; ++i) { children.get(i).parentResolvedOrientationInvalidated(); } } /*************************************************************************** * * * Bounds Computations * * * * This code originated in GroupBoundsHelper (part of javafx-sg-common) * * but has been ported here to the FX side since we cannot rely on the PG * * side for computing the bounds (due to the decoupling of the two * * scenegraphs for threading and other purposes). * * * * Unfortunately, we cannot simply reuse GroupBoundsHelper without some * * major (and hacky) modification due to the fact that GroupBoundsHelper * * relies on PG state and we need to do similar things here that rely on * * core scenegraph state. Unfortunately, that means we made a port. * * * **************************************************************************/ private BaseBounds tmp = new RectBounds(); /** * The cached bounds for the Group. If the cachedBounds are invalid * then we have no history of what the bounds are, or were. */ private BaseBounds cachedBounds = new RectBounds(); /** * Indicates that the cachedBounds is invalid (or old) and need to be recomputed. * If cachedBoundsInvalid is true and dirtyChildrenCount is non-zero, * then when we recompute the cachedBounds we can consider the * values in cachedBounds to represent the last valid bounds for the group. * This is useful for several fast paths. */ private boolean cachedBoundsInvalid; /** * The number of dirty children which bounds haven't been incorporated * into the cached bounds yet. Can be used even when dirtyChildren is null. */ private int dirtyChildrenCount; /** * This set is used to track all of the children of this group which are * dirty. It is only used in cases where the number of children is > some * value (currently 10). For very wide trees, this can provide a very * important speed boost. For the sake of memory consumption, this is * null unless the number of children ever crosses the threshold where * it will be activated. */ private ArrayList dirtyChildren; private Node top; private Node left; private Node bottom; private Node right; private Node near; private Node far; /** * @treatAsPrivate implementation detail * @deprecated This is an internal API that is not intended for use and will be removed in the next version */ @Deprecated @Override public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) { // If we have no children, our bounds are invalid if (children.isEmpty()) { return bounds.makeEmpty(); } if (tx.isTranslateOrIdentity()) { // this is a transform which is only doing translations, or nothing // at all (no scales, rotates, or shears) // so in this case we can easily use the cached bounds if (cachedBoundsInvalid) { recomputeBounds(); if (dirtyChildren != null) { dirtyChildren.clear(); } cachedBoundsInvalid = false; dirtyChildrenCount = 0; } if (!tx.isIdentity()) { bounds = bounds.deriveWithNewBounds((float)(cachedBounds.getMinX() + tx.getMxt()), (float)(cachedBounds.getMinY() + tx.getMyt()), (float)(cachedBounds.getMinZ() + tx.getMzt()), (float)(cachedBounds.getMaxX() + tx.getMxt()), (float)(cachedBounds.getMaxY() + tx.getMyt()), (float)(cachedBounds.getMaxZ() + tx.getMzt())); } else { bounds = bounds.deriveWithNewBounds(cachedBounds); } return bounds; } else { // there is a scale, shear, or rotation happening, so need to // do the full transform! double minX = Double.MAX_VALUE, minY = Double.MAX_VALUE, minZ = Double.MAX_VALUE; double maxX = Double.MIN_VALUE, maxY = Double.MIN_VALUE, maxZ = Double.MIN_VALUE; boolean first = true; for (int i=0, max=children.size(); i dirtyNodes, int remainingDirtyNodes) { // fast path for untransformed bounds calculation if (cachedBounds.isEmpty()) { createCachedBounds(dirtyNodes); return true; } int invalidEdges = 0; if ((left == null) || left.boundsChanged) { invalidEdges |= LEFT_INVALID; } if ((top == null) || top.boundsChanged) { invalidEdges |= TOP_INVALID; } if ((near == null) || near.boundsChanged) { invalidEdges |= NEAR_INVALID; } if ((right == null) || right.boundsChanged) { invalidEdges |= RIGHT_INVALID; } if ((bottom == null) || bottom.boundsChanged) { invalidEdges |= BOTTOM_INVALID; } if ((far == null) || far.boundsChanged) { invalidEdges |= FAR_INVALID; } // These indicate the bounds of the Group as computed by this // function float minX = cachedBounds.getMinX(); float minY = cachedBounds.getMinY(); float minZ = cachedBounds.getMinZ(); float maxX = cachedBounds.getMaxX(); float maxY = cachedBounds.getMaxY(); float maxZ = cachedBounds.getMaxZ(); // this checks the newly added nodes first, so if dirtyNodes is the // whole children list, we can end early for (int i = dirtyNodes.size() - 1; remainingDirtyNodes > 0; --i) { final Node node = dirtyNodes.get(i); if (node.boundsChanged) { // assert node.isVisible(); node.boundsChanged = false; --remainingDirtyNodes; tmp = getChildTransformedBounds(node, BaseTransform.IDENTITY_TRANSFORM, tmp); if (!tmp.isEmpty()) { float tmpx = tmp.getMinX(); float tmpy = tmp.getMinY(); float tmpz = tmp.getMinZ(); float tmpx2 = tmp.getMaxX(); float tmpy2 = tmp.getMaxY(); float tmpz2 = tmp.getMaxZ(); // If this node forms an edge, then we will set it to be the // node for this edge and update the min/max values if (tmpx <= minX) { minX = tmpx; left = node; invalidEdges &= ~LEFT_INVALID; } if (tmpy <= minY) { minY = tmpy; top = node; invalidEdges &= ~TOP_INVALID; } if (tmpz <= minZ) { minZ = tmpz; near = node; invalidEdges &= ~NEAR_INVALID; } if (tmpx2 >= maxX) { maxX = tmpx2; right = node; invalidEdges &= ~RIGHT_INVALID; } if (tmpy2 >= maxY) { maxY = tmpy2; bottom = node; invalidEdges &= ~BOTTOM_INVALID; } if (tmpz2 >= maxZ) { maxZ = tmpz2; far = node; invalidEdges &= ~FAR_INVALID; } } } } if (invalidEdges != 0) { // failed to validate some edges return false; } cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ, maxX, maxY, maxZ); return true; } private void createCachedBounds(final List fromNodes) { // These indicate the bounds of the Group as computed by this function float minX, minY, minZ; float maxX, maxY, maxZ; final int nodeCount = fromNodes.size(); int i; // handle first visible non-empty node for (i = 0; i < nodeCount; ++i) { final Node node = fromNodes.get(i); node.boundsChanged = false; if (node.isVisible()) { tmp = node.getTransformedBounds( tmp, BaseTransform.IDENTITY_TRANSFORM); if (!tmp.isEmpty()) { left = top = near = right = bottom = far = node; break; } } } if (i == nodeCount) { left = top = near = right = bottom = far = null; cachedBounds.makeEmpty(); return; } minX = tmp.getMinX(); minY = tmp.getMinY(); minZ = tmp.getMinZ(); maxX = tmp.getMaxX(); maxY = tmp.getMaxY(); maxZ = tmp.getMaxZ(); // handle remaining visible non-empty nodes for (++i; i < nodeCount; ++i) { final Node node = fromNodes.get(i); node.boundsChanged = false; if (node.isVisible()) { tmp = node.getTransformedBounds( tmp, BaseTransform.IDENTITY_TRANSFORM); if (!tmp.isEmpty()) { final float tmpx = tmp.getMinX(); final float tmpy = tmp.getMinY(); final float tmpz = tmp.getMinZ(); final float tmpx2 = tmp.getMaxX(); final float tmpy2 = tmp.getMaxY(); final float tmpz2 = tmp.getMaxZ(); if (tmpx < minX) { minX = tmpx; left = node; } if (tmpy < minY) { minY = tmpy; top = node; } if (tmpz < minZ) { minZ = tmpz; near = node; } if (tmpx2 > maxX) { maxX = tmpx2; right = node; } if (tmpy2 > maxY) { maxY = tmpy2; bottom = node; } if (tmpz2 > maxZ) { maxZ = tmpz2; far = node; } } } } cachedBounds = cachedBounds.deriveWithNewBounds(minX, minY, minZ, maxX, maxY, maxZ); } @Override protected void updateBounds() { for (int i=0, max=children.size(); i test_getRemoved() { return removed; } }