Java tutorial
/* * Copyright (c) 2010, 2017, 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.control; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanPropertyBase; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectPropertyBase; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.event.Event; import javafx.event.EventDispatchChain; import javafx.event.EventHandler; import javafx.event.EventTarget; import javafx.event.EventType; import javafx.scene.Node; import com.sun.javafx.event.EventHandlerManager; import java.util.Comparator; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import static javafx.scene.control.TreeSortMode.*; /** * The model for a single node supplying a hierarchy of values to a control such * as {@link TreeView}. The model may be implemented such that values may be loaded in * memory as they are needed. * <p> * The model allows registration of listeners which will be notified as the * number of items changes, their position or if the values themselves change. * Note however that a TreeItem is <b>not</b> a Node, and therefore no visual * events will be fired on the TreeItem. To get these events, it is necessary to * add relevant observers to the TreeCell instances (via a custom cell factory - * see the {@link Cell} class documentation for more details). * * <p>In the simplest case, TreeItem instances may be created in memory as such: * <pre><code> * TreeItem<String> root = new TreeItem<String>("Root Node"); * root.setExpanded(true); * root.getChildren().addAll( * new TreeItem<String>("Item 1"), * new TreeItem<String>("Item 2"), * new TreeItem<String>("Item 3") * ); * TreeView<String> treeView = new TreeView<String>(root); * </code></pre> * * This approach works well for simple tree structures, or when the data is not * excessive (so that it can easily fit in memory). In situations where the size * of the tree structure is unknown (and therefore potentially huge), there is * the option of creating TreeItem instances on-demand in a memory-efficient way. * To demonstrate this, the code below creates a file system browser: * * <pre><code> * private TreeView buildFileSystemBrowser() { * TreeItem<File> root = createNode(new File("/")); * return new TreeView<File>(root); * } * * // This method creates a TreeItem to represent the given File. It does this * // by overriding the TreeItem.getChildren() and TreeItem.isLeaf() methods * // anonymously, but this could be better abstracted by creating a * // 'FileTreeItem' subclass of TreeItem. However, this is left as an exercise * // for the reader. * private TreeItem<File> createNode(final File f) { * return new TreeItem<File>(f) { * // We cache whether the File is a leaf or not. A File is a leaf if * // it is not a directory and does not have any files contained within * // it. We cache this as isLeaf() is called often, and doing the * // actual check on File is expensive. * private boolean isLeaf; * * // We do the children and leaf testing only once, and then set these * // booleans to false so that we do not check again during this * // run. A more complete implementation may need to handle more * // dynamic file system situations (such as where a folder has files * // added after the TreeView is shown). Again, this is left as an * // exercise for the reader. * private boolean isFirstTimeChildren = true; * private boolean isFirstTimeLeaf = true; * * @Override public ObservableList<TreeItem<File>> getChildren() { * if (isFirstTimeChildren) { * isFirstTimeChildren = false; * * // First getChildren() call, so we actually go off and * // determine the children of the File contained in this TreeItem. * super.getChildren().setAll(buildChildren(this)); * } * return super.getChildren(); * } * * @Override public boolean isLeaf() { * if (isFirstTimeLeaf) { * isFirstTimeLeaf = false; * File f = (File) getValue(); * isLeaf = f.isFile(); * } * * return isLeaf; * } * * private ObservableList<TreeItem<File>> buildChildren(TreeItem<File> TreeItem) { * File f = TreeItem.getValue(); * if (f != null && f.isDirectory()) { * File[] files = f.listFiles(); * if (files != null) { * ObservableList<TreeItem<File>> children = FXCollections.observableArrayList(); * * for (File childFile : files) { * children.add(createNode(childFile)); * } * * return children; * } * } * * return FXCollections.emptyObservableList(); * } * }; * }</code></pre> * * <strong>TreeItem Events</strong> * <p>TreeItem supports the same event bubbling concept as elsewhere in the * scenegraph. This means that it is not necessary to listen for events on all * TreeItems (and this is certainly not encouraged!). A better, and far more low * cost solution is to instead attach event listeners to the TreeView * {@link TreeView#rootProperty() root} item. As long as there is a path between * where the event occurs and the root TreeItem, the event will be bubbled to the * root item. * * <p>It is important to note however that a TreeItem is <strong>not</strong> a * Node, which means that only the event types defined in TreeItem will be * delivered. To listen to general events (for example mouse interactions), it is * necessary to add the necessary listeners to the {@link Cell cells} contained * within the TreeView (by providing a {@link TreeView#cellFactoryProperty() * cell factory}). * * <p>The TreeItem class defines a number of events, with a defined hierarchy. These * are shown below (follow the links to learn more about each event type): * * <ul> * <li>{@link TreeItem#treeNotificationEvent() TreeItem.treeNotificationEvent()} * <ul> * <li>{@link TreeItem#valueChangedEvent() TreeItem.valueChangedEvent()}</li> * <li>{@link TreeItem#graphicChangedEvent() TreeItem.graphicChangedEvent()}</li> * <li>{@link TreeItem#expandedItemCountChangeEvent() TreeItem.expandedItemCountChangeEvent()} * <ul> * <li>{@link TreeItem#branchExpandedEvent() TreeItem.branchExpandedEvent()}</li> * <li>{@link TreeItem#branchCollapsedEvent() TreeItem.branchCollapsedEvent()}</li> * <li>{@link TreeItem#childrenModificationEvent() TreeItem.childrenModificationEvent()}</li> * </ul> * </li> * </ul> * </li> * </ul> * * <p>The indentation shown above signifies the relationship between event types. * For example, all TreeItem event types have * {@link TreeItem#treeNotificationEvent() treeNotificationEvent()} as their * parent event type, and the branch * {@link TreeItem#branchExpandedEvent() expand} / * {@link TreeItem#branchCollapsedEvent() collapse} event types are both * {@link TreeItem#treeNotificationEvent() treeNotificationEvent()}. For * performance reasons, it is encouraged to listen * to only the events you need to listen to. This means that it is encouraged * that it is better to listen to, for example, * {@link TreeItem#valueChangedEvent() TreeItem.valueChangedEvent()}, * rather than {@link TreeItem#treeNotificationEvent() TreeItem.treeNotificationEvent()}. * * @param <T> The type of the {@link #getValue() value} property within TreeItem. * @since JavaFX 2.0 * @see TreeView */ public class TreeItem<T> implements EventTarget { //, Comparable<TreeItem<T>> { /*************************************************************************** * * * Static properties and methods * * * **************************************************************************/ /** * The base EventType used to indicate that an event has occurred within a * TreeItem. When an event occurs in a TreeItem, the event is fired to any * listeners on the TreeItem that the event occurs, before it 'bubbles' up the * TreeItem chain by following the TreeItem parent property. This repeats * until a TreeItem whose parent TreeItem is null is reached At this point * the event stops 'bubbling' and goes no further. This means that events * that occur on a TreeItem can be relatively cheap, as a listener needs only * be installed on the TreeView root node to be alerted of events happening * at any point in the tree. * * @param <T> The type of the value contained within the TreeItem. * @return the base EventType when an event has occurred within a TreeItem */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> treeNotificationEvent() { return (EventType<TreeModificationEvent<T>>) TREE_NOTIFICATION_EVENT; } private static final EventType<?> TREE_NOTIFICATION_EVENT = new EventType<>(Event.ANY, "TreeNotificationEvent"); /** * The general EventType used when the TreeItem receives a modification that * results in the number of children being visible changes. * This is normally achieved via one of the sub-types of this * EventType (see {@link #branchExpandedEvent()}, * {@link #branchCollapsedEvent()} and {@link #childrenModificationEvent()} * for the three sub-types). * * @param <T> The type of the value contained within the TreeItem. * @return The general EventType when the TreeItem receives a modification * @since JavaFX 8.0 */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> expandedItemCountChangeEvent() { return (EventType<TreeModificationEvent<T>>) EXPANDED_ITEM_COUNT_CHANGE_EVENT; } private static final EventType<?> EXPANDED_ITEM_COUNT_CHANGE_EVENT = new EventType<>(treeNotificationEvent(), "ExpandedItemCountChangeEvent"); /** * An EventType used when the TreeItem receives a modification to its * expanded property, such that the TreeItem is now in the expanded state. * * @param <T> The type of the value contained within the TreeItem. * @return The EventType used when the TreeItem receives a modification */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> branchExpandedEvent() { return (EventType<TreeModificationEvent<T>>) BRANCH_EXPANDED_EVENT; } private static final EventType<?> BRANCH_EXPANDED_EVENT = new EventType<>(expandedItemCountChangeEvent(), "BranchExpandedEvent"); /** * An EventType used when the TreeItem receives a modification to its * expanded property, such that the TreeItem is now in the collapsed state. * * @param <T> The type of the value contained within the TreeItem. * @return The EventType when the TreeItem receives a modification */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> branchCollapsedEvent() { return (EventType<TreeModificationEvent<T>>) BRANCH_COLLAPSED_EVENT; } private static final EventType<?> BRANCH_COLLAPSED_EVENT = new EventType<>(expandedItemCountChangeEvent(), "BranchCollapsedEvent"); /** * An EventType used when the TreeItem receives a direct modification to its * children list. * * @param <T> The type of the value contained within the TreeItem. * @return The EventType when the TreeItem receives a direct modification to * its children list */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> childrenModificationEvent() { return (EventType<TreeModificationEvent<T>>) CHILDREN_MODIFICATION_EVENT; } private static final EventType<?> CHILDREN_MODIFICATION_EVENT = new EventType<>(expandedItemCountChangeEvent(), "ChildrenModificationEvent"); /** * An EventType used when the TreeItem receives a modification to its * value property. * * @param <T> The type of the value contained within the TreeItem. * @return The EventType when the TreeItem receives a modification to its * value property */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> valueChangedEvent() { return (EventType<TreeModificationEvent<T>>) VALUE_CHANGED_EVENT; } private static final EventType<?> VALUE_CHANGED_EVENT = new EventType<>(treeNotificationEvent(), "ValueChangedEvent"); /** * An EventType used when the TreeItem receives a modification to its * graphic property. * * @param <T> The type of the value contained within the TreeItem. * @return The EventType when the TreeItem receives a modification to its * graphic property */ @SuppressWarnings("unchecked") public static <T> EventType<TreeModificationEvent<T>> graphicChangedEvent() { return (EventType<TreeModificationEvent<T>>) GRAPHIC_CHANGED_EVENT; } private static final EventType<?> GRAPHIC_CHANGED_EVENT = new EventType<>(treeNotificationEvent(), "GraphicChangedEvent"); /*************************************************************************** * * * Constructors * * * **************************************************************************/ /** * Creates an empty TreeItem. */ public TreeItem() { this(null); } /** * Creates a TreeItem with the value property set to the provided object. * * @param value The object to be stored as the value of this TreeItem. */ public TreeItem(final T value) { this(value, (Node) null); } /** * Creates a TreeItem with the value property set to the provided object, and * the graphic set to the provided Node. * * @param value The object to be stored as the value of this TreeItem. * @param graphic The Node to show in the TreeView next to this TreeItem. */ public TreeItem(final T value, final Node graphic) { setValue(value); setGraphic(graphic); addEventHandler(TreeItem.<Object>expandedItemCountChangeEvent(), itemListener); } private final EventHandler<TreeModificationEvent<Object>> itemListener = new EventHandler<TreeModificationEvent<Object>>() { @Override public void handle(TreeModificationEvent<Object> event) { expandedDescendentCountDirty = true; } }; /*************************************************************************** * * * Instance Variables * * * **************************************************************************/ private boolean ignoreSortUpdate = false; private boolean expandedDescendentCountDirty = true; // The ObservableList containing all children belonging to this TreeItem. // It is important that interactions with this list go directly into the // children property, rather than via getChildren(), as this may be // a very expensive call. ObservableList<TreeItem<T>> children; // Made static based on findings of RT-18344 - EventHandlerManager is an // expensive class and should be reused amongst classes if at all possible. private final EventHandlerManager eventHandlerManager = new EventHandlerManager(this); // Rather than have the TreeView need to (pretty well) constantly determine // the expanded descendent count of a TreeItem, we instead cache it locally // based on tree item modification events. private int expandedDescendentCount = 1; // we record the previous value also, so that we can easily determine how // many items just disappeared on a TreeItem collapse event. Note that the // actual number of items that disappeared is one less than this value, // because we obviously are also counting this node, which hasn't disappeared // when all children are collapsed. int previousExpandedDescendentCount = 1; Comparator<TreeItem<T>> lastComparator = null; TreeSortMode lastSortMode = null; // Refer to the TreeItem.updateChildrenParent method below for more context // and a description of this field private int parentLinkCount = 0; /*************************************************************************** * * * Callbacks and events * * * **************************************************************************/ // called whenever the contents of the children sequence changes private ListChangeListener<TreeItem<T>> childrenListener = c -> { expandedDescendentCountDirty = true; updateChildren(c); }; /*************************************************************************** * * * Properties * * * **************************************************************************/ // --- Value private ObjectProperty<T> value; /** * Sets the application-specific data represented by this TreeItem. * @param value the application-specific data */ public final void setValue(T value) { valueProperty().setValue(value); } /** * Returns the application-specific data represented by this TreeItem. * @return the data represented by this TreeItem */ public final T getValue() { return value == null ? null : value.getValue(); } /** * A property representing the application-specific data contained within * this TreeItem. * @return the property representing the application-specific data contained * within this TreeItem */ public final ObjectProperty<T> valueProperty() { if (value == null) { value = new ObjectPropertyBase<T>() { @Override protected void invalidated() { fireEvent(new TreeModificationEvent<T>(VALUE_CHANGED_EVENT, TreeItem.this, get())); } @Override public Object getBean() { return TreeItem.this; } @Override public String getName() { return "value"; } }; } return value; } // --- Graphic private ObjectProperty<Node> graphic; /** * Sets the node that is generally shown to the left of the value property. * For best effect, this tends to be a 16x16 image. * * @param value The graphic node that will be displayed to the user. */ public final void setGraphic(Node value) { graphicProperty().setValue(value); } /** * Returns the node that is generally shown to the left of the value property. * For best effect, this tends to be a 16x16 image. * * @return The graphic node that will be displayed to the user. */ public final Node getGraphic() { return graphic == null ? null : graphic.getValue(); } /** * The node that is generally shown to the left of the value property. For * best effect, this tends to be a 16x16 image. * @return The node that is generally shown to the left of the value property */ public final ObjectProperty<Node> graphicProperty() { if (graphic == null) { graphic = new ObjectPropertyBase<Node>() { @Override protected void invalidated() { fireEvent(new TreeModificationEvent<T>(GRAPHIC_CHANGED_EVENT, TreeItem.this)); } @Override public Object getBean() { return TreeItem.this; } @Override public String getName() { return "graphic"; } }; } return graphic; } // --- Expanded private BooleanProperty expanded; /** * Sets the expanded state of this TreeItem. This has no effect on a TreeItem * with no children. On a TreeItem with children however, the result of * toggling this property is that visually the children will either become * visible or hidden, based on whether expanded is set to true or false. * * @param value If this TreeItem has children, calling setExpanded with * <code>true</code> will result in the children becoming visible. * Calling setExpanded with <code>false</code> will hide any children * belonging to the TreeItem. */ public final void setExpanded(boolean value) { if (!value && expanded == null) return; expandedProperty().setValue(value); } /** * Returns the expanded state of this TreeItem. * * @return Returns the expanded state of this TreeItem. */ public final boolean isExpanded() { return expanded == null ? false : expanded.getValue(); } /** * The expanded state of this TreeItem. * @return The expanded state property of this TreeItem */ public final BooleanProperty expandedProperty() { if (expanded == null) { expanded = new BooleanPropertyBase() { @Override protected void invalidated() { // We don't fire expanded events for leaf nodes (RT-32620) if (isLeaf()) return; EventType<?> evtType = isExpanded() ? BRANCH_EXPANDED_EVENT : BRANCH_COLLAPSED_EVENT; fireEvent(new TreeModificationEvent<T>(evtType, TreeItem.this, isExpanded())); } @Override public Object getBean() { return TreeItem.this; } @Override public String getName() { return "expanded"; } }; } return expanded; } // --- Leaf private ReadOnlyBooleanWrapper leaf; private void setLeaf(boolean value) { if (value && leaf == null) { return; } else if (leaf == null) { leaf = new ReadOnlyBooleanWrapper(this, "leaf", true); } leaf.setValue(value); } /** * A TreeItem is a leaf if it has no children. The isLeaf method may of * course be overridden by subclasses to support alternate means of defining * how a TreeItem may be a leaf, but the general premise is the same: a * leaf can not be expanded by the user, and as such will not show a * disclosure node or respond to expansion requests. * @return true if this TreeItem has no children */ public boolean isLeaf() { return leaf == null ? true : leaf.getValue(); } /** * Represents the TreeItem leaf property, which is true if the TreeItem has no children. * @return the TreeItem leaf property */ public final ReadOnlyBooleanProperty leafProperty() { if (leaf == null) { leaf = new ReadOnlyBooleanWrapper(this, "leaf", true); } return leaf.getReadOnlyProperty(); } // --- Parent private ReadOnlyObjectWrapper<TreeItem<T>> parent = new ReadOnlyObjectWrapper<TreeItem<T>>(this, "parent"); private void setParent(TreeItem<T> value) { parent.setValue(value); } /** * The parent of this TreeItem. Each TreeItem can have no more than one * parent. If a TreeItem has no parent, it represents a root in the tree model. * * @return The parent of this TreeItem, or null if the TreeItem has no parent. */ public final TreeItem<T> getParent() { return parent == null ? null : parent.getValue(); } /** * A property that represents the parent of this TreeItem. * @return the parent property of this TreeItem */ public final ReadOnlyObjectProperty<TreeItem<T>> parentProperty() { return parent.getReadOnlyProperty(); } /*********************************************************************** * * * TreeItem API * * * **********************************************************************/ /** * The children of this TreeItem. This method is called frequently, and * it is therefore recommended that the returned list be cached by * any TreeItem implementations. * * @return a list that contains the child TreeItems belonging to the TreeItem. */ public ObservableList<TreeItem<T>> getChildren() { if (children == null) { children = FXCollections.observableArrayList(); children.addListener(childrenListener); } // we need to check if this TreeItem needs to have its children sorted. // There are two different ways that this could be possible. if (children.isEmpty()) return children; // checkSortState should in almost all instances be called, but there // are situations where checking the sort state will result in // unwanted permutation events being fired (if a sort is applied). To // avoid this (which resolves RT-37593), we set the ignoreSortUpdate // to true (and of course, we're careful to set it back to false again) if (!ignoreSortUpdate) { checkSortState(); } return children; } /*************************************************************************** * * * Public API * * * **************************************************************************/ /** * Returns the previous sibling of the TreeItem. Ordering is based on the * position of the TreeItem relative to its siblings in the children * list belonging to the parent of the TreeItem. * * @return A TreeItem that is the previous sibling of the current TreeItem, * or null if no such sibling can be found. */ public TreeItem<T> previousSibling() { return previousSibling(this); } /** * Returns the previous sibling after the given node. Ordering is based on the * position of the given TreeItem relative to its siblings in the children * list belonging to the parent of the TreeItem. * * @param beforeNode The TreeItem for which the previous sibling is being * sought. * @return A TreeItem that is the previous sibling of the given TreeItem, * or null if no such sibling can be found. */ public TreeItem<T> previousSibling(final TreeItem<T> beforeNode) { if (getParent() == null || beforeNode == null) { return null; } List<TreeItem<T>> parentChildren = getParent().getChildren(); final int childCount = parentChildren.size(); int pos = -1; for (int i = 0; i < childCount; i++) { if (beforeNode.equals(parentChildren.get(i))) { pos = i - 1; return pos < 0 ? null : parentChildren.get(pos); } } return null; } /** * Returns the next sibling of the TreeItem. Ordering is based on the * position of the TreeItem relative to its siblings in the children * list belonging to the parent of the TreeItem. * * @return A TreeItem that is the next sibling of the current TreeItem, * or null if no such sibling can be found. */ public TreeItem<T> nextSibling() { return nextSibling(this); } /** * Returns the next sibling after the given node. Ordering is based on the * position of the given TreeItem relative to its siblings in the children * list belonging to the parent of the TreeItem. * * @param afterNode The TreeItem for which the next sibling is being * sought. * @return A TreeItem that is the next sibling of the given TreeItem, * or null if no such sibling can be found. */ public TreeItem<T> nextSibling(final TreeItem<T> afterNode) { if (getParent() == null || afterNode == null) { return null; } List<TreeItem<T>> parentChildren = getParent().getChildren(); final int childCount = parentChildren.size(); int pos = -1; for (int i = 0; i < childCount; i++) { if (afterNode.equals(parentChildren.get(i))) { pos = i + 1; return pos >= childCount ? null : parentChildren.get(pos); } } return null; } /** * Returns a string representation of this {@code TreeItem} object. * @return a string representation of this {@code TreeItem} object. */ @Override public String toString() { return "TreeItem [ value: " + getValue() + " ]"; } private void fireEvent(TreeModificationEvent<T> evt) { Event.fireEvent(this, evt); } /*************************************************************************** * * * Event Target Implementation / API * * * **************************************************************************/ /** {@inheritDoc} */ @Override public EventDispatchChain buildEventDispatchChain(EventDispatchChain tail) { // To allow for a TreeView (and its skin) to be notified of changes in the // tree, this method recursively calls up to the root node, at which point // it fires a ROOT_NOTIFICATION_EVENT, which the TreeView may be watching for. if (getParent() != null) { getParent().buildEventDispatchChain(tail); } return tail.append(eventHandlerManager); } /** * Registers an event handler to this TreeItem. The TreeItem class allows * registration of listeners which will be notified as the * number of items changes, their position or if the values themselves change. * Note however that a TreeItem is <b>not</b> a Node, and therefore no visual * events will be fired on the TreeItem. To get these events, it is necessary to * add relevant observers to the TreeCell instances (via a custom cell factory - * see the {@link Cell} class documentation for more details). * * @param <E> The event * @param eventType the type of the events to receive by the handler * @param eventHandler the handler to register * @throws NullPointerException if the event type or handler is null */ public <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) { eventHandlerManager.addEventHandler(eventType, eventHandler); } /** * Unregisters a previously registered event handler from this TreeItem. One * handler might have been registered for different event types, so the * caller needs to specify the particular event type from which to * unregister the handler. * * @param <E> The event * @param eventType the event type from which to unregister * @param eventHandler the handler to unregister * @throws NullPointerException if the event type or handler is null */ public <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<E> eventHandler) { eventHandlerManager.removeEventHandler(eventType, eventHandler); } /*************************************************************************** * * * private methods * * * **************************************************************************/ void sort() { sort(children, lastComparator, lastSortMode); } private void sort(final ObservableList<TreeItem<T>> children, final Comparator<TreeItem<T>> comparator, final TreeSortMode sortMode) { if (comparator == null) return; runSort(children, comparator, sortMode); // if we're at the root node, we'll fire an event so that the control // can update its display if (getParent() == null) { TreeModificationEvent<T> e = new TreeModificationEvent<T>(TreeItem.childrenModificationEvent(), this); e.wasPermutated = true; fireEvent(e); } } private void checkSortState() { TreeItem<T> rootNode = getRoot(); TreeSortMode sortMode = rootNode.lastSortMode; Comparator<TreeItem<T>> comparator = rootNode.lastComparator; if (comparator != null && comparator != lastComparator) { lastComparator = comparator; runSort(children, comparator, sortMode); } } private void runSort(ObservableList<TreeItem<T>> children, Comparator<TreeItem<T>> comparator, TreeSortMode sortMode) { if (sortMode == ALL_DESCENDANTS) { doSort(children, comparator); } else if (sortMode == ONLY_FIRST_LEVEL) { // if we are here we presume that the current node is the root node // (but we can test to see if getParent() returns null to be sure). // We also know that ONLY_FIRST_LEVEL only applies to the children // of the root, so we return straight after we sort these children. if (getParent() == null) { doSort(children, comparator); } // } else if (sortMode == ONLY_LEAVES) { // if (isLeaf()) { // // sort the parent once // } // } else if (sortMode == ALL_BUT_LEAVES) { // } else { // Unknown sort mode } } private TreeItem<T> getRoot() { TreeItem<T> parent = getParent(); if (parent == null) return this; while (true) { TreeItem<T> newParent = parent.getParent(); if (newParent == null) return parent; parent = newParent; } } private void doSort(ObservableList<TreeItem<T>> children, final Comparator<TreeItem<T>> comparator) { if (!isLeaf() && isExpanded()) { FXCollections.sort(children, comparator); } } // This value is package accessible so that it may be retrieved from TreeView. int getExpandedDescendentCount(boolean reset) { if (reset || expandedDescendentCountDirty) { updateExpandedDescendentCount(reset); expandedDescendentCountDirty = false; } return expandedDescendentCount; } private void updateExpandedDescendentCount(boolean reset) { previousExpandedDescendentCount = expandedDescendentCount; expandedDescendentCount = 1; ignoreSortUpdate = true; if (!isLeaf() && isExpanded()) { for (TreeItem<T> child : getChildren()) { if (child == null) continue; expandedDescendentCount += child.isExpanded() ? child.getExpandedDescendentCount(reset) : 1; } } ignoreSortUpdate = false; } private void updateChildren(ListChangeListener.Change<? extends TreeItem<T>> c) { setLeaf(children.isEmpty()); final List<TreeItem<T>> added = new ArrayList<>(); final List<TreeItem<T>> removed = new ArrayList<>(); while (c.next()) { added.addAll(c.getAddedSubList()); removed.addAll(c.getRemoved()); } // update the relationships such that all added children point to // this node as the parent (and all removed children point to null) updateChildrenParent(removed, null); updateChildrenParent(added, this); c.reset(); // fire an event up the parent hierarchy such that any listening // TreeViews (which only listen to their root node) can redraw fireEvent(new TreeModificationEvent<T>(CHILDREN_MODIFICATION_EVENT, this, added, removed, c)); } // Convenience method to set the parent of all children in the given list to // the given parent TreeItem private static <T> void updateChildrenParent(List<? extends TreeItem<T>> treeItems, final TreeItem<T> newParent) { if (treeItems == null) return; for (final TreeItem<T> treeItem : treeItems) { if (treeItem == null) continue; TreeItem<T> currentParent = treeItem.getParent(); // We only replace the parent if the parentLinkCount of the given // TreeItem is zero (which indicates that this TreeItem has not been // 'linked' to its parent multiple times). This can happen in // situations such as what is shown in RT-28668 (and tested for in // TreeViewTest.test_rt28556()). Specifically, when a sort is applied // to the children of a TreeItem, it is possible for them to be // sorted in such a way that the element is considered to be // added in multiple places in the child list and then removed from // one of those places subsequently. In doing this final removal, // the parent of that TreeItem is set to null when it should in fact // remain with the parent that it belongs to. if (treeItem.parentLinkCount == 0) { treeItem.setParent(newParent); } boolean parentMatch = currentParent != null && currentParent.equals(newParent); if (parentMatch) { if (newParent == null) { treeItem.parentLinkCount--; } else { treeItem.parentLinkCount++; } } } } /** * An {@link Event} that contains relevant information for all forms of * TreeItem modifications. * @param <T> The TreeModificationEvent * @since JavaFX 2.0 */ public static class TreeModificationEvent<T> extends Event { private static final long serialVersionUID = 4741889985221719579L; /** * Common supertype for all tree modification event types. * @since JavaFX 8.0 */ public static final EventType<?> ANY = TREE_NOTIFICATION_EVENT; private transient final TreeItem<T> treeItem; private final T newValue; private final List<? extends TreeItem<T>> added; private final List<? extends TreeItem<T>> removed; private final ListChangeListener.Change<? extends TreeItem<T>> change; private final boolean wasExpanded; private final boolean wasCollapsed; private boolean wasPermutated; /** * Constructs a basic TreeModificationEvent - this is useful in situations * where the tree item has not received a new value, has not changed * between expanded/collapsed states, and whose children has not changed. * An example of when this constructor is used is when the TreeItem * graphic property changes. * * @param eventType The type of the event that has occurred. * @param treeItem The TreeItem on which this event occurred. */ public TreeModificationEvent(EventType<? extends Event> eventType, TreeItem<T> treeItem) { this(eventType, treeItem, null); } /** * Constructs a TreeModificationEvent for when the TreeItem has had its * {@link TreeItem#valueProperty()} changed. * * @param eventType The type of the event that has occurred. * @param treeItem The TreeItem on which this event occurred. * @param newValue The new value that has been put into the * {@link TreeItem#valueProperty()}. */ public TreeModificationEvent(EventType<? extends Event> eventType, TreeItem<T> treeItem, T newValue) { super(eventType); this.treeItem = treeItem; this.newValue = newValue; this.added = null; this.removed = null; this.change = null; this.wasExpanded = false; this.wasCollapsed = false; } /** * Constructs a TreeModificationEvent for when the TreeItem has had its * {@link TreeItem#expandedProperty()} changed. * * @param eventType The type of the event that has occurred. * @param treeItem The TreeItem on which this event occurred. * @param expanded A boolean to represent the current expanded * state of the TreeItem. */ public TreeModificationEvent(EventType<? extends Event> eventType, TreeItem<T> treeItem, boolean expanded) { super(eventType); this.treeItem = treeItem; this.newValue = null; this.added = null; this.removed = null; this.change = null; this.wasExpanded = expanded; this.wasCollapsed = !expanded; } /** * Constructs a TreeModificationEvent for when the TreeItem has had its * children list changed. * * @param eventType The type of the event that has occurred. * @param treeItem The TreeItem on which this event occurred. * @param added A list of the items added to the children list of the * given TreeItem. * @param removed A list of the items removed from the children list of * the given TreeItem. */ public TreeModificationEvent(EventType<? extends Event> eventType, TreeItem<T> treeItem, List<? extends TreeItem<T>> added, List<? extends TreeItem<T>> removed) { this(eventType, treeItem, added, removed, null); } /** * Constructs a TreeModificationEvent for when the TreeItem has had its * children list changed, including the * {@link javafx.collections.ListChangeListener.Change} that has taken place. * * @param eventType The type of the event that has occurred. * @param treeItem The TreeItem on which this event occurred. * @param added A list of the items added to the children list of the * given TreeItem. * @param removed A list of the items removed from the children list of * the given TreeItem. * @param change The actual change that has taken place on the children list. */ private TreeModificationEvent(EventType<? extends Event> eventType, TreeItem<T> treeItem, List<? extends TreeItem<T>> added, List<? extends TreeItem<T>> removed, ListChangeListener.Change<? extends TreeItem<T>> change) { super(eventType); this.treeItem = treeItem; this.newValue = null; this.added = added; this.removed = removed; this.change = change; this.wasExpanded = false; this.wasCollapsed = false; this.wasPermutated = added != null && removed != null && added.size() == removed.size() && added.containsAll(removed); } /** * Returns the TreeItem upon which this event occurred. * @since JavaFX 2.1 */ @Override public TreeItem<T> getSource() { return this.treeItem; } /** * Returns the TreeItem that this event occurred upon. * @return The TreeItem that this event occurred upon. */ public TreeItem<T> getTreeItem() { return treeItem; } /** * If the value of the TreeItem changed, this method will return the new * value. If it did not change, this method will return null. * @return The new value of the TreeItem if it changed, null otherwise. */ public T getNewValue() { return newValue; } /** * Returns the children added to the TreeItem in this event, or an empty * list if no children were added. * @return The newly added children, or an empty list if no children * were added. */ public List<? extends TreeItem<T>> getAddedChildren() { return added == null ? Collections.<TreeItem<T>>emptyList() : added; } /** * Returns the children removed from the TreeItem in this event, or an * empty list if no children were added. * @return The removed children, or an empty list if no children * were removed. */ public List<? extends TreeItem<T>> getRemovedChildren() { return removed == null ? Collections.<TreeItem<T>>emptyList() : removed; } /** * Returns the number of children items that were removed in this event, * or zero if no children were removed. * @return The number of removed children items, or zero if no children * were removed. */ public int getRemovedSize() { return getRemovedChildren().size(); } /** * Returns the number of children items that were added in this event, * or zero if no children were added. * @return The number of added children items, or zero if no children * were added. */ public int getAddedSize() { return getAddedChildren().size(); } /** * Returns true if this event represents a TreeItem expansion event, * and false if the TreeItem was not expanded. * @return true if this event represents a TreeItem expansion event, * and false if the TreeItem was not expanded */ public boolean wasExpanded() { return wasExpanded; } /** * Returns true if this event represents a TreeItem collapse event, * and false if the TreeItem was not collapsed. * @return true if this event represents a TreeItem collapse event, * and false if the TreeItem was not collapsed */ public boolean wasCollapsed() { return wasCollapsed; } /** * Returns true if this event represents a TreeItem event where children * TreeItems were added. * @return true if this event represents a TreeItem event where children * TreeItems were added */ public boolean wasAdded() { return getAddedSize() > 0; } /** * Returns true if this event represents a TreeItem event where children * TreeItems were removed. * @return true if this event represents a TreeItem event where children * TreeItems were removed */ public boolean wasRemoved() { return getRemovedSize() > 0; } /** * Returns true if the order of the TreeItem children list has changed, * but that there have been no additions or removals. * @return true if the order of the TreeItem children list has changed, * but that there have been no additions or removals */ public boolean wasPermutated() { return wasPermutated; } int getFrom() { return change == null ? -1 : change.getFrom(); } int getTo() { return change == null ? -1 : change.getTo(); } ListChangeListener.Change<? extends TreeItem<T>> getChange() { return change; } } }