blob: b970642511bf7962c5b6cf04b010c7f04ae0e1f0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pivot.wtk;
import java.io.Serializable;
import java.util.Comparator;
import org.apache.pivot.beans.DefaultProperty;
import org.apache.pivot.collections.ArrayList;
import org.apache.pivot.collections.List;
import org.apache.pivot.collections.ListListener;
import org.apache.pivot.collections.Sequence;
import org.apache.pivot.collections.Sequence.Tree.ImmutablePath;
import org.apache.pivot.collections.Sequence.Tree.Path;
import org.apache.pivot.collections.immutable.ImmutableList;
import org.apache.pivot.util.Filter;
import org.apache.pivot.util.ListenerList;
import org.apache.pivot.util.Vote;
import org.apache.pivot.wtk.content.TreeViewNodeRenderer;
/**
* Class that displays a hierarchical data structure, allowing a user to select
* one or more paths.
*/
@DefaultProperty("treeData")
public class TreeView extends Component {
/**
* Enumeration defining supported selection modes. <tt>TreeView</tt>
* defaults to single select mode.
*/
public enum SelectMode {
/**
* Selection is disabled.
*/
NONE,
/**
* A single path may be selected at a time.
*/
SINGLE,
/**
* Multiple paths may be concurrently selected.
*/
MULTI
}
/**
* Enumeration defining node check states. Note that <tt>TreeView</tt> does
* not involve itself in the propagation of checkmarks (either up or down
* the tree). Developers who wish to propagate checkmarks may do so by
* registering a {@link TreeViewNodeStateListener} and setting the desired
* checkmark states manually.
*/
public enum NodeCheckState {
/**
* The node is checked.
*/
CHECKED,
/**
* The node is unchecked. If <tt>showMixedCheckmarkState</tt> is true,
* this implies that all of the node's descendants are unchecked as
* well.
*/
UNCHECKED,
/**
* The node's check state is mixed, meaning that it is not checked, but
* at least one of its descendants is checked. This state will only be
* reported if <tt>showMixedCheckmarkState</tt> is true. Otherwise, the
* node will be reported as {@link #UNCHECKED}.
*/
MIXED
}
/**
* {@link Renderer} interface to customize the appearance of items in a
* TreeView.
*/
public interface NodeRenderer extends Renderer {
/**
* Prepares the renderer for layout or paint.
*
* @param node The node value to render, or <tt>null</tt> if called to
* calculate preferred height for skins that assume a fixed renderer
* height.
* @param path The path to the node being rendered, or <tt>null</tt> if
* <tt>node</tt> is <tt>null</tt>.
* @param rowIndex The row index of the node being rendered, as seen in
* the current visible nodes list, or <tt>-1</tt> if <tt>node</tt> is
* <tt>null</tt>.
* @param treeView The host component.
* @param expanded <tt>true</tt> if the node is expanded; <tt>false</tt>
* otherwise.
* @param selected <tt>true</tt> if the node is selected; <tt>false</tt>
* otherwise.
* @param checkState The node's {@linkplain NodeCheckState check state}.
* @param highlighted <tt>true</tt> if the node is highlighted;
* <tt>false</tt> otherwise.
* @param disabled <tt>true</tt> if the node is disabled; <tt>false</tt>
* otherwise.
*/
public void render(Object node, Path path, int rowIndex, TreeView treeView,
boolean expanded, boolean selected, NodeCheckState checkState, boolean highlighted,
boolean disabled);
/**
* Converts a tree node to a string representation.
*
* @param node The actual tree node data object.
* @return The node's string representation, or <tt>null</tt> if the node
* does not have a string representation. <p> Note that this method may
* be called often during keyboard navigation, so implementations should
* avoid unnecessary string allocations.
*/
public String toString(Object node);
}
/**
* Tree view node editor interface.
*/
public interface NodeEditor {
/**
* Called to begin editing a tree node.
*
* @param treeView The source of this event.
* @param path The path to the node being edited.
*/
public void beginEdit(TreeView treeView, Path path);
/**
* Terminates an edit operation.
*
* @param result <tt>true</tt> to perform the edit; <tt>false</tt> to
* cancel it.
*/
public void endEdit(boolean result);
/**
* @return Whether an edit is currently in progress.
*/
public boolean isEditing();
}
/**
* Tree view skin interface. Tree view skins must implement this.
*/
public interface Skin {
/**
* Gets the path to the node found at the specified y-coordinate
* (relative to the tree view).
*
* @param y The y-coordinate in pixels.
* @return The path to the node, or <tt>null</tt> if there is no node
* being painted at the specified y-coordinate.
*/
public Path getNodeAt(int y);
/**
* Gets the bounds of the node at the specified path relative to the
* tree view. Note that all nodes are left aligned with the tree; to get
* the pixel value of a node's indent, use {@link #getNodeIndent(int)}.
*
* @param path The path to the node.
* @return The bounds, or <tt>null</tt> if the node is not currently
* visible.
*/
public Bounds getNodeBounds(Path path);
/**
* Gets the pixel indent of nodes at the specified depth. Depth is
* measured in generations away from the tree view's "root" node, which
* is represented by the {@link #getTreeData() tree data}.
*
* @param depth The depth, where the first child of the root has depth
* 1, the child of that branch has depth 2, etc.
* @return The indent in pixels to the node's content.
*/
public int getNodeIndent(int depth);
/**
* Gets the row index of the node, as seen in the current visible nodes
* list. Note that as branches are expanded and collapsed, the row index
* of any given node in the tree will change.
*
* @param path The path to the node.
* @return The row index of the node, or <tt>-1</tt> if the node is not
* currently visible.
*/
public int getRowIndex(Path path);
}
/**
* Tree view listener list.
*/
private static class TreeViewListenerList extends WTKListenerList<TreeViewListener> implements
TreeViewListener {
@Override
public void treeDataChanged(TreeView treeView, List<?> previousTreeData) {
for (TreeViewListener listener : this) {
listener.treeDataChanged(treeView, previousTreeData);
}
}
@Override
public void nodeRendererChanged(TreeView treeView, NodeRenderer previousNodeRenderer) {
for (TreeViewListener listener : this) {
listener.nodeRendererChanged(treeView, previousNodeRenderer);
}
}
@Override
public void nodeEditorChanged(TreeView treeView, TreeView.NodeEditor previousNodeEditor) {
for (TreeViewListener listener : this) {
listener.nodeEditorChanged(treeView, previousNodeEditor);
}
}
@Override
public void selectModeChanged(TreeView treeView, SelectMode previousSelectMode) {
for (TreeViewListener listener : this) {
listener.selectModeChanged(treeView, previousSelectMode);
}
}
@Override
public void checkmarksEnabledChanged(TreeView treeView) {
for (TreeViewListener listener : this) {
listener.checkmarksEnabledChanged(treeView);
}
}
@Override
public void showMixedCheckmarkStateChanged(TreeView treeView) {
for (TreeViewListener listener : this) {
listener.showMixedCheckmarkStateChanged(treeView);
}
}
@Override
public void disabledNodeFilterChanged(TreeView treeView,
Filter<?> previousDisabledNodeFilter) {
for (TreeViewListener listener : this) {
listener.disabledNodeFilterChanged(treeView, previousDisabledNodeFilter);
}
}
@Override
public void disabledCheckmarkFilterChanged(TreeView treeView,
Filter<?> previousDisabledCheckmarkFilter) {
for (TreeViewListener listener : this) {
listener.disabledCheckmarkFilterChanged(treeView, previousDisabledCheckmarkFilter);
}
}
}
/**
* Tree view branch listener list.
*/
private static class TreeViewBranchListenerList extends WTKListenerList<TreeViewBranchListener>
implements TreeViewBranchListener {
@Override
public void branchExpanded(TreeView treeView, Path path) {
for (TreeViewBranchListener listener : this) {
listener.branchExpanded(treeView, path);
}
}
@Override
public void branchCollapsed(TreeView treeView, Path path) {
for (TreeViewBranchListener listener : this) {
listener.branchCollapsed(treeView, path);
}
}
@Override
public Vote previewBranchExpandedChange(TreeView treeView, Path path) {
Vote vote = Vote.APPROVE;
for (TreeViewBranchListener listener : this) {
vote = vote.tally(listener.previewBranchExpandedChange(treeView, path));
}
return vote;
}
@Override
public void branchExpandedChangeVetoed(TreeView treeView, Path path, Vote reason) {
for (TreeViewBranchListener listener : this) {
listener.branchExpandedChangeVetoed(treeView, path, reason);
}
}
}
/**
* Tree view node listener list.
*/
private static class TreeViewNodeListenerList extends WTKListenerList<TreeViewNodeListener>
implements TreeViewNodeListener {
@Override
public void nodeInserted(TreeView treeView, Path path, int index) {
for (TreeViewNodeListener listener : this) {
listener.nodeInserted(treeView, path, index);
}
}
@Override
public void nodesRemoved(TreeView treeView, Path path, int index, int count) {
for (TreeViewNodeListener listener : this) {
listener.nodesRemoved(treeView, path, index, count);
}
}
@Override
public void nodeUpdated(TreeView treeView, Path path, int index) {
for (TreeViewNodeListener listener : this) {
listener.nodeUpdated(treeView, path, index);
}
}
@Override
public void nodesCleared(TreeView treeView, Path path) {
for (TreeViewNodeListener listener : this) {
listener.nodesCleared(treeView, path);
}
}
@Override
public void nodesSorted(TreeView treeView, Path path) {
for (TreeViewNodeListener listener : this) {
listener.nodesSorted(treeView, path);
}
}
}
/**
* Tree view node state listener list.
*/
private static class TreeViewNodeStateListenerList extends
WTKListenerList<TreeViewNodeStateListener> implements TreeViewNodeStateListener {
@Override
public void nodeCheckStateChanged(TreeView treeView, Path path,
TreeView.NodeCheckState previousCheckState) {
for (TreeViewNodeStateListener listener : this) {
listener.nodeCheckStateChanged(treeView, path, previousCheckState);
}
}
}
/**
* Tree view selection listener list.
*/
private static class TreeViewSelectionListenerList extends
WTKListenerList<TreeViewSelectionListener> implements TreeViewSelectionListener {
@Override
public void selectedPathAdded(TreeView treeView, Path path) {
for (TreeViewSelectionListener listener : this) {
listener.selectedPathAdded(treeView, path);
}
}
@Override
public void selectedPathRemoved(TreeView treeView, Path path) {
for (TreeViewSelectionListener listener : this) {
listener.selectedPathRemoved(treeView, path);
}
}
@Override
public void selectedPathsChanged(TreeView treeView, Sequence<Path> previousSelectedPaths) {
for (TreeViewSelectionListener listener : this) {
listener.selectedPathsChanged(treeView, previousSelectedPaths);
}
}
@Override
public void selectedNodeChanged(TreeView treeView, Object previousSelectedNode) {
for (TreeViewSelectionListener listener : this) {
listener.selectedNodeChanged(treeView, previousSelectedNode);
}
}
}
/**
* A comparator that sorts paths by the order in which they would visually
* appear in a fully expanded tree, otherwise known as their "row order".
*/
public static final class PathComparator implements Comparator<Path>, Serializable {
private static final long serialVersionUID = 1L;
@Override
public int compare(Path path1, Path path2) {
int path1Length = path1.getLength();
int path2Length = path2.getLength();
for (int i = 0, n = Math.min(path1Length, path2Length); i < n; i++) {
int pathElement1 = path1.get(i);
int pathElement2 = path2.get(i);
if (pathElement1 != pathElement2) {
return pathElement1 - pathElement2;
}
}
return path1Length - path2Length;
}
}
/**
* Notifies the tree of nested <tt>ListListener</tt> events that occur on
* the tree data.
*/
private class BranchHandler extends ArrayList<BranchHandler> implements ListListener<Object> {
private static final long serialVersionUID = -6132480635507615071L;
// Reference to its parent allows for the construction of its path
private BranchHandler parent;
// The backing data structure
private List<?> branchData;
/**
* Creates a new <tt>BranchHandler</tt> tied to the specified parent and
* listening to events from the specified branch data.
*
* @param parent The branch handler for our parent node.
* @param branchData The nodes for this branch that must be handled.
*/
@SuppressWarnings("unchecked")
public BranchHandler(BranchHandler parent, List<?> branchData) {
super(branchData.getLength());
this.parent = parent;
this.branchData = branchData;
((List<Object>) branchData).getListListeners().add(this);
// Create placeholder child entries, to be loaded lazily
for (int i = 0, n = branchData.getLength(); i < n; i++) {
add(null);
}
}
/**
* @return The branch data that this handler is monitoring.
*/
public List<?> getBranchData() {
return branchData;
}
/**
* Unregisters this branch handler's interest in ListListener events.
* This must be done to release references from the tree data to our
* internal BranchHandler data structures. Failure to do so would mean
* that our BranchHandler objects would remain in scope as long as the
* tree data remained in scope, even if we were no longer using the
* BranchHandler objects.
*/
@SuppressWarnings("unchecked")
public void release() {
((List<Object>) branchData).getListListeners().remove(this);
// Recursively have all child branches unregister interest
for (int i = 0, n = getLength(); i < n; i++) {
BranchHandler branchHandler = get(i);
if (branchHandler != null) {
branchHandler.release();
}
}
}
/**
* @return The path that leads from the root of the tree data to this
* branch. Note: <tt>rootBranchHandler.getPath()</tt> will return an
* empty sequence.
*/
@SuppressWarnings("unchecked")
private Path getPath() {
Path path = new Path();
BranchHandler handler = this;
while (handler.parent != null) {
int index = ((List<Object>) handler.parent.branchData).indexOf(handler.branchData);
path.insert(index, 0);
handler = handler.parent;
}
return path;
}
@Override
public void itemInserted(List<Object> list, int index) {
Path path = getPath();
// Insert child handler placeholder (lazily loaded)
insert(null, index);
// Update our data structures
incrementPaths(expandedPaths, path, index);
int updated = incrementPaths(selectedPaths, path, index);
incrementPaths(checkedPaths, path, index);
// Notify listeners
treeViewNodeListeners.nodeInserted(TreeView.this, path, index);
if (updated > 0) {
treeViewSelectionListeners.selectedPathsChanged(TreeView.this, getSelectedPaths());
}
}
@Override
public void itemsRemoved(List<Object> list, int index, Sequence<Object> items) {
Path path = getPath();
Path previousSelectedPath;
if (selectMode == SelectMode.SINGLE && selectedPaths.getLength() > 0) {
previousSelectedPath = selectedPaths.get(0);
} else {
previousSelectedPath = null;
}
// Remove child handlers
int count = items.getLength();
Sequence<BranchHandler> removed = remove(index, count);
// Release each child handler that was removed
for (int i = 0, n = removed.getLength(); i < n; i++) {
BranchHandler handler = removed.get(i);
if (handler != null) {
handler.release();
}
}
// Update our data structures
clearAndDecrementPaths(expandedPaths, path, index, count);
int updated = clearAndDecrementPaths(selectedPaths, path, index, count);
clearAndDecrementPaths(checkedPaths, path, index, count);
// Notify listeners
treeViewNodeListeners.nodesRemoved(TreeView.this, path, index, count);
if (updated > 0) {
treeViewSelectionListeners.selectedPathsChanged(TreeView.this, getSelectedPaths());
if (selectMode == SelectMode.SINGLE
&& !getSelectedPath().equals(previousSelectedPath)) {
treeViewSelectionListeners.selectedNodeChanged(TreeView.this, null);
}
}
}
@Override
public void itemUpdated(List<Object> list, int index, Object previousItem) {
Path path = getPath();
if (list.get(index) != previousItem) {
// Release child handler
BranchHandler handler = update(index, null);
if (handler != null) {
handler.release();
}
// Update our data structures
clearPaths(expandedPaths, path, index);
clearPaths(selectedPaths, path, index);
clearPaths(checkedPaths, path, index);
}
// Notify listeners
treeViewNodeListeners.nodeUpdated(TreeView.this, path, index);
}
@Override
public void listCleared(List<Object> list) {
Path path = getPath();
// Release each child handler
for (int i = 0, n = getLength(); i < n; i++) {
BranchHandler handler = get(i);
if (handler != null) {
handler.release();
}
}
// Remove child handlers
super.clear();
// Update our data structures
clearPaths(expandedPaths, path);
int cleared = clearPaths(selectedPaths, path);
clearPaths(checkedPaths, path);
// Notify listeners
treeViewNodeListeners.nodesCleared(TreeView.this, path);
if (cleared > 0) {
treeViewSelectionListeners.selectedPathsChanged(TreeView.this, getSelectedPaths());
if (selectMode == SelectMode.SINGLE) {
treeViewSelectionListeners.selectedNodeChanged(TreeView.this, null);
}
}
}
@Override
public void comparatorChanged(List<Object> list, Comparator<Object> previousComparator) {
if (list.getComparator() != null) {
Path path = getPath();
// Release all child handlers. This is safe because of the
// calls to clearPaths(). Failure to do this would result in
// the indices of our child handlers not matching those of the
// backing data structures, which would yield very hard to find
// bugs
for (int i = 0, n = getLength(); i < n; i++) {
BranchHandler handler = update(i, null);
if (handler != null) {
handler.release();
}
}
// Update our data structures
clearPaths(expandedPaths, path);
int cleared = clearPaths(selectedPaths, path);
clearPaths(checkedPaths, path);
// Notify listeners
treeViewNodeListeners.nodesSorted(TreeView.this, path);
if (cleared > 0) {
treeViewSelectionListeners.selectedPathsChanged(TreeView.this,
getSelectedPaths());
if (selectMode == SelectMode.SINGLE) {
treeViewSelectionListeners.selectedNodeChanged(TreeView.this, null);
}
}
}
}
/**
* Updates the paths within the specified sequence in response to a tree
* data path insertion. For instance, if <tt>paths</tt> is <tt>[[3, 0],
* [5, 0]]</tt>, <tt>basePath</tt> is <tt>[]</tt>, and <tt>index</tt> is
* <tt>4</tt>, then <tt>paths</tt> will be updated to <tt>[[3, 0], [6,
* 0]]</tt>. No events are fired.
*
* @param paths Sequence of paths guaranteed to be sorted by
* "row order".
* @param basePath The path to the parent of the inserted item.
* @param index The index of the inserted item within its parent.
* @return The number of path elements that were updated.
*/
private int incrementPaths(ArrayList<Path> paths, Path basePath, int index) {
// Calculate the child's path
Path childPath = new Path(basePath);
childPath.add(index);
// Find the child path's place in our sorted paths sequence
int i = ArrayList.binarySearch(paths, childPath, PATH_COMPARATOR);
if (i < 0) {
i = -(i + 1);
}
// Temporarily clear the comparator while we update the list
paths.setComparator(null);
int n = paths.getLength();
try {
// Update all affected paths by incrementing the appropriate
// path element
for (int depth = basePath.getLength(); i < n; i++) {
Path affectedPath = paths.get(i);
if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
// All paths from here forward are guaranteed to be
// unaffected
break;
}
Integer[] elements = affectedPath.toArray();
elements[depth]++;
paths.update(i, new ImmutablePath(elements));
}
} finally {
// Restore the comparator
paths.setComparator(PATH_COMPARATOR);
}
return (n - i);
}
/**
* Updates the paths within the specified sequence in response to items
* having been removed from the base path. For instance, if
* <tt>paths</tt> is <tt>[[3, 0], [3, 1], [6, 0]]</tt>,
* <tt>basePath</tt> is <tt>[]</tt>, <tt>index</tt> is <tt>3</tt>, and
* <tt>count</tt> is <tt>2</tt>, then <tt>paths</tt> will be updated to
* <tt>[[4, 0]]</tt>. No events are fired.
*
* @param paths Sequence of paths guaranteed to be sorted by
* "row order".
* @param basePath The path to the parent of the removed items.
* @param index The index of the first removed item within the base.
* @param count The number of items removed.
* @return The number of path elements that were updated.
*/
private int clearAndDecrementPaths(ArrayList<Path> paths, Path basePath, int index,
int count) {
int depth = basePath.getLength();
// Find the index of the first path to clear (inclusive)
Path testPath = new Path(basePath);
testPath.add(index);
int start = ArrayList.binarySearch(paths, testPath, PATH_COMPARATOR);
if (start < 0) {
start = -(start + 1);
}
// Find the index of the last path to clear (exclusive)
testPath.update(depth, index + count);
int end = ArrayList.binarySearch(paths, testPath, PATH_COMPARATOR);
if (end < 0) {
end = -(end + 1);
}
// Clear affected paths
if (end > start) {
paths.remove(start, end - start);
}
// Decrement paths as necessary
int n = paths.getLength();
for (int i = start; i < n; i++) {
Path affectedPath = paths.get(i);
if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
// All paths from here forward are guaranteed to be
// unaffected
break;
}
Integer[] elements = affectedPath.toArray();
elements[depth] -= count;
paths.update(i, new ImmutablePath(elements));
}
return (n - start);
}
/**
* Removes affected paths from within the specified sequence in response
* to an item having been updated in the base path. For instance, if
* <tt>paths</tt> is <tt>[[3], [3, 0], [3, 1], [5, 0]]</tt>,
* <tt>basePath</tt> is <tt>[3]</tt>, and <tt>index</tt> is <tt>0</tt>,
* then <tt>paths</tt> will be updated to <tt>[[3], [3, 1], [5,
* 0]]</tt>. No events are fired.
*
* @param paths Sequence of paths guaranteed to be sorted by
* "row order".
* @param basePath The path to the parent of the updated item.
* @param index The index of the updated item within its parent.
*/
private void clearPaths(ArrayList<Path> paths, Path basePath, int index) {
// Calculate the child's path
Path childPath = new Path(basePath);
childPath.add(index);
// Find the child path's place in our sorted paths sequence
int clearIndex = ArrayList.binarySearch(paths, childPath, PATH_COMPARATOR);
if (clearIndex < 0) {
clearIndex = -(clearIndex + 1);
}
// Remove the child and all descendants from the paths list
for (int i = clearIndex, n = paths.getLength(); i < n; i++) {
Path affectedPath = paths.get(clearIndex);
if (!Sequence.Tree.isDescendant(childPath, affectedPath)) {
break;
}
paths.remove(clearIndex, 1);
}
}
/**
* Removes affected paths from within the specified sequence in response
* to a base path having been sorted. For instance, if <tt>paths</tt> is
* <tt>[[3], [3, 0], [3, 1], [5, 0]]</tt> and <tt>basePath</tt> is
* <tt>[3]</tt>, then <tt>paths</tt> will be updated to <tt>[[3], [5,
* 0]]</tt>. No events are fired.
*
* @param paths Sequence of paths guaranteed to be sorted by
* "row order".
* @param basePath The path whose children were sorted.
* @return The number of path elements that were updated.
*/
private int clearPaths(ArrayList<Path> paths, Path basePath) {
// Find first descendant in paths list, if it exists
int index = ArrayList.binarySearch(paths, basePath, PATH_COMPARATOR);
index = (index < 0 ? -(index + 1) : index + 1);
// Remove all descendants from the paths list
int n = paths.getLength();
for (int i = index; i < n; i++) {
Path affectedPath = paths.get(index);
if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
break;
}
paths.remove(index, 1);
}
return (n - index);
}
}
// Core data model
private List<?> treeData = null;
// Ancillary data models
private ArrayList<Path> expandedPaths = new ArrayList<>(PATH_COMPARATOR);
private ArrayList<Path> selectedPaths = new ArrayList<>(PATH_COMPARATOR);
private ArrayList<Path> checkedPaths = new ArrayList<>(PATH_COMPARATOR);
// Properties
private SelectMode selectMode = SelectMode.SINGLE;
private boolean checkmarksEnabled = false;
private boolean showMixedCheckmarkState = false;
// Filters
private Filter<?> disabledNodeFilter = null;
private Filter<?> disabledCheckmarkFilter = null;
// Handlers
private BranchHandler rootBranchHandler;
// Renderer & editor
private NodeRenderer nodeRenderer = DEFAULT_NODE_RENDERER;
private NodeEditor nodeEditor = null;
// Listener lists
private TreeViewListenerList treeViewListeners = new TreeViewListenerList();
private TreeViewBranchListenerList treeViewBranchListeners = new TreeViewBranchListenerList();
private TreeViewNodeListenerList treeViewNodeListeners = new TreeViewNodeListenerList();
private TreeViewNodeStateListenerList treeViewNodeStateListeners = new TreeViewNodeStateListenerList();
private TreeViewSelectionListenerList treeViewSelectionListeners = new TreeViewSelectionListenerList();
// other properties
private String treeDataKey = null;
private static final NodeRenderer DEFAULT_NODE_RENDERER = new TreeViewNodeRenderer();
private static final Comparator<Path> PATH_COMPARATOR = new PathComparator();
/**
* Creates a new <tt>TreeView</tt> with empty tree data.
*/
public TreeView() {
this(new ArrayList<>());
}
/**
* Creates a new <tt>TreeView</tt> with the specified tree data.
*
* @param treeData Default data set to be used with the tree. This list
* represents the root set of items displayed by the tree and will never
* itself be painted. Sub-items that also implement the <tt>List</tt>
* interface are considered branches; other items are considered leaves.
* @see #setTreeData(List)
*/
public TreeView(List<?> treeData) {
setTreeData(treeData);
installSkin(TreeView.class);
}
/**
* Sets the skin, replacing any previous skin. This ensures that the skin
* being set implements the {@link TreeView.Skin} interface.
*
* @param skin The new skin.
*/
@Override
protected void setSkin(org.apache.pivot.wtk.Skin skin) {
if (!(skin instanceof TreeView.Skin)) {
throw new IllegalArgumentException("Skin class must implement "
+ TreeView.Skin.class.getName());
}
super.setSkin(skin);
}
/**
* Returns the tree view's data model. This list represents the root set of
* items displayed by the tree and will never itself be painted. Sub-items
* that also implement the <tt>List</tt> interface are considered branches;
* other items are considered leaves. <p> For instance, a tree view that
* displays a single root branch would be backed by list with one child
* (also a list).
*
* @return The tree view's data model.
*/
public List<?> getTreeData() {
return treeData;
}
/**
* Sets the tree data. Note that it is the responsibility of the caller to
* ensure that the current tree node renderer is capable of displaying the
* contents of the tree structure. By default, an instance of
* {@link TreeViewNodeRenderer} is used. <p> When the tree data is changed,
* the state of all nodes (expansion, selection, and checked) will be
* cleared since the nodes themselves are being replaced. Note that
* corresponding events will <b>not</b> be fired, since these actions are
* implied by the {@link TreeViewListener#treeDataChanged(TreeView,List)
* treeDataChanged} event.
*
* @param treeData The data to be presented by the tree.
*/
public void setTreeData(List<?> treeData) {
if (treeData == null) {
throw new IllegalArgumentException("treeData is null.");
}
List<?> previousTreeData = this.treeData;
if (previousTreeData != treeData) {
int cleared;
if (previousTreeData != null) {
// Reset our data models
expandedPaths.clear();
cleared = selectedPaths.getLength();
selectedPaths.clear();
checkedPaths.clear();
// Release our existing branch handlers
rootBranchHandler.release();
} else {
cleared = 0;
}
// Update our root branch handler
rootBranchHandler = new BranchHandler(null, treeData);
// Update the tree data
this.treeData = treeData;
// Notify listeners
treeViewListeners.treeDataChanged(this, previousTreeData);
if (cleared > 0) {
treeViewSelectionListeners.selectedPathsChanged(TreeView.this, getSelectedPaths());
if (selectMode == SelectMode.SINGLE) {
treeViewSelectionListeners.selectedNodeChanged(TreeView.this, null);
}
}
}
}
/**
* Gets the tree view's node renderer, which is responsible for the
* appearance of the node data. As such, note that there is an implied
* coordination between the node renderer and the data model. The default
* node renderer used is an instance of <tt>TreeViewNodeRenderer</tt>.
*
* @return The current node renderer.
* @see TreeViewNodeRenderer
*/
public NodeRenderer getNodeRenderer() {
return nodeRenderer;
}
/**
* Sets the tree view's node renderer, which is responsible for the
* appearance of the node data.
*
* @param nodeRenderer The new node renderer.
*/
public void setNodeRenderer(NodeRenderer nodeRenderer) {
if (nodeRenderer == null) {
throw new IllegalArgumentException("nodeRenderer is null.");
}
NodeRenderer previousNodeRenderer = this.nodeRenderer;
if (previousNodeRenderer != nodeRenderer) {
this.nodeRenderer = nodeRenderer;
treeViewListeners.nodeRendererChanged(this, previousNodeRenderer);
}
}
/**
* Returns the editor used to edit nodes in this tree.
*
* @return The node editor, or <tt>null</tt> if no editor is installed.
*/
public NodeEditor getNodeEditor() {
return nodeEditor;
}
/**
* Sets the editor used to edit nodes in this tree.
*
* @param nodeEditor The node editor for the tree.
*/
public void setNodeEditor(NodeEditor nodeEditor) {
NodeEditor previousNodeEditor = this.nodeEditor;
if (previousNodeEditor != nodeEditor) {
this.nodeEditor = nodeEditor;
treeViewListeners.nodeEditorChanged(this, previousNodeEditor);
}
}
/**
* Returns the current selection mode.
*
* @return The current selection mode.
*/
public SelectMode getSelectMode() {
return selectMode;
}
/**
* Sets the selection mode. Clears the selection if the mode has changed.
* Note that if the selection is cleared, selection listeners will not be
* notified, as the clearing of the selection is implied by the
* {@link TreeViewListener#selectModeChanged(TreeView,TreeView.SelectMode)
* selectModeChanged} event.
*
* @param selectMode The new selection mode.
* @see TreeViewListener
* @see TreeViewSelectionListener
*/
public void setSelectMode(SelectMode selectMode) {
if (selectMode == null) {
throw new IllegalArgumentException("selectMode is null");
}
SelectMode previousSelectMode = this.selectMode;
if (selectMode != previousSelectMode) {
// Clear any current selection
selectedPaths.clear();
// Update the selection mode
this.selectMode = selectMode;
// Fire select mode change event
treeViewListeners.selectModeChanged(this, previousSelectMode);
}
}
/**
* Returns the currently selected paths.
*
* @return An immutable list containing the currently selected paths. Note
* that the returned list is a wrapper around the actual selection, not a
* copy. Any changes made to the selection state will be reflected in the
* list, but events will not be fired.
*/
public ImmutableList<Path> getSelectedPaths() {
return new ImmutableList<>(selectedPaths);
}
/**
* Set the new selected nodes in the tree.
*
* @param selectedPaths The new set of paths to the selected nodes.
* @return The new set of selected paths (with duplicates eliminated).
* @throws IllegalStateException If selection has been disabled (select mode
* <tt>NONE</tt>).
*/
public Sequence<Path> setSelectedPaths(Sequence<Path> selectedPaths) {
if (selectedPaths == null) {
throw new IllegalArgumentException("selectedPaths is null.");
}
if (selectMode == SelectMode.NONE) {
throw new IllegalStateException("Selection is not enabled.");
}
if (selectMode == SelectMode.SINGLE && selectedPaths.getLength() > 1) {
throw new IllegalArgumentException("Selection length is greater than 1.");
}
Sequence<Path> previousSelectedPaths = this.selectedPaths;
Object previousSelectedNode = (selectMode == SelectMode.SINGLE) ? getSelectedNode() : null;
// TODO Only add and monitor non-duplicates
if (selectedPaths != previousSelectedPaths) {
this.selectedPaths = new ArrayList<>(PATH_COMPARATOR);
for (int i = 0, n = selectedPaths.getLength(); i < n; i++) {
Path path = selectedPaths.get(i);
// Monitor the path's parent
monitorBranch(new Path(path, path.getLength() - 1));
// Update the selection
this.selectedPaths.add(new ImmutablePath(path));
}
// Notify listeners
treeViewSelectionListeners.selectedPathsChanged(this, previousSelectedPaths);
if (selectMode == SelectMode.SINGLE) {
treeViewSelectionListeners.selectedNodeChanged(TreeView.this, previousSelectedNode);
}
}
return getSelectedPaths();
}
/**
* Returns the first selected path, as it would appear in a fully expanded
* tree.
*
* @return The first selected path, or <tt>null</tt> if nothing is selected.
*/
public Path getFirstSelectedPath() {
return (selectedPaths.getLength() > 0 ? selectedPaths.get(0) : null);
}
/**
* Returns the last selected path, as it would appear in a fully expanded
* tree.
*
* @return The last selected path, or <tt>null</tt> if nothing is selected.
*/
public Path getLastSelectedPath() {
return (selectedPaths.getLength() > 0 ? selectedPaths.get(selectedPaths.getLength() - 1)
: null);
}
/**
* Returns the currently selected index, even when in multi-select mode.
*
* @return The selected path, or <tt>null</tt> if nothing is selected.
*/
public Path getSelectedPath() {
return getFirstSelectedPath();
}
/**
* Set the single selected path.
*
* @param path The new path to select.
*/
public void setSelectedPath(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
setSelectedPaths(new ArrayList<Path>(path));
}
/**
* @return The selected object, or <tt>null</tt> if nothing is selected.
* Note that technically, the selected path could be backed by a
* <tt>null</tt> data value. If the caller wishes to distinguish between
* these cases, they can use <tt>getSelectedPath()</tt> instead.
*/
public Object getSelectedNode() {
Path path = getSelectedPath();
Object node = null;
if (path != null) {
node = Sequence.Tree.get(treeData, path);
}
return node;
}
/**
* Adds a path to the selection.
*
* @param path The path to the node to be added to the selection.
* @return <tt>true</tt> if the path was added to the selection;
* <tt>false</tt>, otherwise.
* @throws IllegalStateException If multi-select is not enabled.
*/
public boolean addSelectedPath(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
if (selectMode != SelectMode.MULTI) {
throw new IllegalStateException("Tree view is not in multi-select mode.");
}
int index = selectedPaths.indexOf(path);
if (index < 0) {
// Monitor the path's parent
monitorBranch(new Path(path, path.getLength() - 1));
// Update the selection
selectedPaths.add(new ImmutablePath(path));
// Notify listeners
treeViewSelectionListeners.selectedPathAdded(this, path);
treeViewSelectionListeners.selectedPathsChanged(this, null);
}
return (index < 0);
}
/**
* Removes a path from the selection.
*
* @param path Path to the node to be removed from the selection.
* @return <tt>true</tt> if the path was removed from the selection;
* <tt>false</tt>, otherwise.
* @throws IllegalStateException If multi-select is not enabled.
*/
public boolean removeSelectedPath(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
if (selectMode != SelectMode.MULTI) {
throw new IllegalStateException("Tree view is not in multi-select mode.");
}
int index = selectedPaths.indexOf(path);
if (index >= 0) {
// Update the selection
selectedPaths.remove(index, 1);
// Notify listeners
treeViewSelectionListeners.selectedPathRemoved(this, path);
treeViewSelectionListeners.selectedPathsChanged(this, null);
}
return (index >= 0);
}
public String getTreeDataKey() {
return treeDataKey;
}
public void setTreeDataKey(String treeDataKey) {
String previousTreeDataKey = this.treeDataKey;
if (previousTreeDataKey != treeDataKey) {
this.treeDataKey = treeDataKey;
// treeViewBindingListeners.treeDataKeyChanged(this,
// previousTreeDataKey); // future use
}
}
@Override
public void clear() {
if (treeDataKey != null) {
setTreeData(new ArrayList<>());
}
clearSelection();
}
/**
* Clears the selection.
*/
public void clearSelection() {
if (selectedPaths.getLength() > 0) {
Sequence<Path> previousSelectedPaths = selectedPaths;
// Update the selection
selectedPaths = new ArrayList<>(PATH_COMPARATOR);
// Notify listeners
treeViewSelectionListeners.selectedPathsChanged(this, previousSelectedPaths);
}
}
/**
* @return Whether or not the node at the given path is part of the
* current selection.
* @param path Path to the node to check.
*/
public boolean isNodeSelected(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
return (selectedPaths.indexOf(path) >= 0);
}
/**
* Returns the disabled state of a given node.
*
* @param path The path to the node whose disabled state is to be tested
* @return <tt>true</tt> if the node is disabled; <tt>false</tt>, otherwise
*/
@SuppressWarnings("unchecked")
public boolean isNodeDisabled(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
boolean disabled = false;
if (disabledNodeFilter != null) {
Object node = Sequence.Tree.get(treeData, path);
disabled = ((Filter<Object>) disabledNodeFilter).include(node);
}
return disabled;
}
/**
* Returns the disabled node filter, which determines the disabled state of
* all nodes. Disabled nodes are not interactive to the user. Note, however,
* that disabled nodes may still be expanded, selected, and checked
* <i>programatically</i>. A disabled node may have enabled children. <p> If
* the disabled node filter is set to <tt>null</tt>, all nodes are enabled.
*
* @return The disabled node filter, or <tt>null</tt> if no disabled node
* filter is set
*/
public Filter<?> getDisabledNodeFilter() {
return disabledNodeFilter;
}
/**
* Sets the disabled node filter, which determines the disabled state of all
* nodes. Disabled nodes are not interactive to the user. Note, however,
* that disabled nodes may still be expanded, selected, and checked
* <i>programatically</i>. A disabled node may have enabled children. <p> If
* the disabled node filter is set to <tt>null</tt>, all nodes are enabled.
*
* @param disabledNodeFilter The disabled node filter, or <tt>null</tt> for
* no disabled node filter
*/
public void setDisabledNodeFilter(Filter<?> disabledNodeFilter) {
Filter<?> previousDisabledNodeFilter = this.disabledNodeFilter;
if (previousDisabledNodeFilter != disabledNodeFilter) {
this.disabledNodeFilter = disabledNodeFilter;
treeViewListeners.disabledNodeFilterChanged(this, previousDisabledNodeFilter);
}
}
/**
* @return Whether or not the checkmarks on each node are enabled.
*/
public boolean getCheckmarksEnabled() {
return checkmarksEnabled;
}
/**
* Enables or disables checkmarks. If checkmarks are being disabled, all
* checked nodes will be automatically unchecked. Note that the
* corresponding event will <b>not</b> be fired, since the clearing of
* existing checkmarks is implied by the
* {@link TreeViewListener#checkmarksEnabledChanged(TreeView)
* checkmarksEnabledChanged} event.
*
* @param checkmarksEnabled <tt>true</tt> to enable checkmarks;
* <tt>false</tt> to disable them.
*/
public void setCheckmarksEnabled(boolean checkmarksEnabled) {
if (this.checkmarksEnabled != checkmarksEnabled) {
// Clear any current check state
checkedPaths.clear();
// Update the checkmark mode
this.checkmarksEnabled = checkmarksEnabled;
// Fire checkmarks enabled change event
treeViewListeners.checkmarksEnabledChanged(this);
}
}
/**
* Tells whether or not the mixed check state will be reported by this tree
* view. This state is a derived state meaning "the node is not checked, but
* one or more of its descendants are." When this state is configured to not
* be shown, such nodes will simply be reported as unchecked.
*
* @return <tt>true</tt> if the tree view will report so-called mixed nodes
* as mixed; <tt>false</tt> if it will report them as unchecked.
* @see NodeCheckState#MIXED
*/
public boolean getShowMixedCheckmarkState() {
return showMixedCheckmarkState;
}
/**
* Sets whether or not the "mixed" check state will be reported by this tree
* view. This state is a derived state meaning "the node is not checked, but
* one or more of its descendants are." When this state is configured to not
* be shown, such nodes will simply be reported as unchecked. <p> Changing
* this flag may result in some nodes changing their reported check state.
* Note that the corresponding <tt>nodeCheckStateChanged</tt> events will
* <b>not</b> be fired, since the possibility of such a change in check
* state is implied by the
* {@link TreeViewListener#showMixedCheckmarkStateChanged(TreeView)
* showMixedCheckmarkStateChanged} event.
*
* @param showMixedCheckmarkState <tt>true</tt> to show the derived mixed
* state; <tt>false</tt> to report so-called "mixed" nodes as unchecked.
* @see NodeCheckState#MIXED
*/
public void setShowMixedCheckmarkState(boolean showMixedCheckmarkState) {
if (this.showMixedCheckmarkState != showMixedCheckmarkState) {
// Update the flag
this.showMixedCheckmarkState = showMixedCheckmarkState;
// Notify listeners
treeViewListeners.showMixedCheckmarkStateChanged(this);
}
}
/**
* Tells whether or not the node at the specified path is checked. If
* checkmarks are not enabled, this is guaranteed to be <tt>false</tt>. So
* called mixed nodes will always be reported as unchecked in this method.
*
* @param path The path to the node.
* @return <tt>true</tt> if the node is explicitly checked; <tt>false</tt>
* otherwise.
* @see #getCheckmarksEnabled()
*/
public boolean isNodeChecked(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
return (checkedPaths.indexOf(path) >= 0);
}
/**
* Returns the checkmark state of the node at the specified path. If
* checkmarks are not enabled, this is guaranteed to be <tt>UNCHECKED</tt>.
* <p> Note that the <tt>MIXED</tt> check state (meaning "the node is not
* checked, but one or more of its descendants are") is only reported when
* the tree view is configured as such. Otherwise, such nodes will be
* reported as <tt>UNCHECKED</tt>.
*
* @param path The path to the node.
* @return The checkmark state of the specified node.
* @see #getCheckmarksEnabled()
* @see #setShowMixedCheckmarkState(boolean)
*/
public NodeCheckState getNodeCheckState(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
NodeCheckState checkState = NodeCheckState.UNCHECKED;
if (checkmarksEnabled) {
int index = ArrayList.binarySearch(checkedPaths, path, PATH_COMPARATOR);
if (index >= 0) {
checkState = NodeCheckState.CHECKED;
} else if (showMixedCheckmarkState) {
// Translate to the insertion index
index = -(index + 1);
if (index < checkedPaths.getLength()) {
Path nextCheckedPath = checkedPaths.get(index);
if (Sequence.Tree.isDescendant(path, nextCheckedPath)) {
checkState = NodeCheckState.MIXED;
}
}
}
}
return checkState;
}
/**
* Sets the check state of the node at the specified path. If the node
* already has the specified check state, nothing happens. <p> Note that it
* is impossible to set the check state of a node to <tt>MIXED</tt>. This is
* because the mixed check state is a derived state meaning "the node is not
* checked, but one or more of its descendants are."
*
* @param path The path to the node.
* @param checked <tt>true</tt> to check the node; <tt>false</tt> to uncheck
* it.
* @throws IllegalStateException If checkmarks are not enabled (see
* {@link #getCheckmarksEnabled()}).
* @see NodeCheckState#MIXED
*/
public void setNodeChecked(Path path, boolean checked) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
if (!checkmarksEnabled) {
throw new IllegalStateException("Checkmarks are not enabled.");
}
int index = checkedPaths.indexOf(path);
if ((index < 0 && checked) || (index >= 0 && !checked)) {
NodeCheckState previousCheckState = getNodeCheckState(path);
Sequence<NodeCheckState> ancestorCheckStates = null;
if (showMixedCheckmarkState) {
// Record the check states of our ancestors before we change
// anything so we know which events to fire after we're done
ancestorCheckStates = new ArrayList<>(path.getLength() - 1);
Path ancestorPath = new Path(path, path.getLength() - 1);
for (int i = ancestorPath.getLength() - 1; i >= 0; i--) {
ancestorCheckStates.insert(getNodeCheckState(ancestorPath), 0);
ancestorPath.remove(i, 1);
}
}
if (checked) {
// Monitor the path's parent
monitorBranch(new Path(path, path.getLength() - 1));
// Update the checked paths
checkedPaths.add(new ImmutablePath(path));
} else {
// Update the checked paths
checkedPaths.remove(index, 1);
}
// Notify listeners
treeViewNodeStateListeners.nodeCheckStateChanged(this, path, previousCheckState);
if (showMixedCheckmarkState && ancestorCheckStates != null) {
// Notify listeners of any changes to our ancestors' check
// states
Path ancestorPath = new Path(path, path.getLength() - 1);
for (int i = ancestorPath.getLength() - 1; i >= 0; i--) {
NodeCheckState ancestorPreviousCheckState = ancestorCheckStates.get(i);
NodeCheckState ancestorCheckState = getNodeCheckState(ancestorPath);
if (ancestorCheckState != ancestorPreviousCheckState) {
treeViewNodeStateListeners.nodeCheckStateChanged(this, ancestorPath,
ancestorPreviousCheckState);
}
ancestorPath.remove(i, 1);
}
}
}
}
/**
* Gets the sequence of node paths that are checked. If checkmarks are not
* enabled (see {@link #getCheckmarksEnabled()}), this is guaranteed to
* return an empty sequence. <p> Note that if the tree view is configured to
* show mixed checkmark states (see {@link #getShowMixedCheckmarkState()}),
* this will still only return the nodes that are fully checked.
*
* @return The paths to the checked nodes in the tree, guaranteed to be
* non-<tt>null</tt>.
*/
public Sequence<Path> getCheckedPaths() {
return new ImmutableList<>(checkedPaths);
}
/**
* Returns the disabled checkmark filter, which determines which checkboxes
* are interactive and which are not. Note that this filter only affects
* user interaction; nodes may still be checked programatically despite
* their inclusion in this filter. If this filter is set to <tt>null</tt>,
* all checkboxes will be interactive. <p> <b>Note:</b> this filter is only
* relavent if {@link #setCheckmarksEnabled(boolean) checkmarksEnabled} is
* set to true.
*
* @return The disabled checkmark filter, or <tt>null</tt> if no disabled
* checkmark filter is set
*/
public Filter<?> getDisabledCheckmarkFilter() {
return disabledCheckmarkFilter;
}
/**
* Sets the disabled checkmark filter, which determines which checkboxes are
* interactive and which are not. Note that this filter only affects user
* interaction; nodes may still be checked programatically despite their
* inclusion in this filter. If this filter is set to <tt>null</tt>, all
* checkboxes will be interactive. <p> <b>Note:</b> this filter is only
* relavent if {@link #setCheckmarksEnabled(boolean) checkmarksEnabled} is
* set to true. enabled.
*
* @param disabledCheckmarkFilter The disabled checkmark filter, or
* <tt>null</tt> for no disabled checkmark filter
*/
public void setDisabledCheckmarkFilter(Filter<?> disabledCheckmarkFilter) {
Filter<?> previousDisabledCheckmarkFilter = this.disabledCheckmarkFilter;
if (previousDisabledCheckmarkFilter != disabledCheckmarkFilter) {
this.disabledCheckmarkFilter = disabledCheckmarkFilter;
treeViewListeners.disabledCheckmarkFilterChanged(this, previousDisabledCheckmarkFilter);
}
}
/**
* Sets the expansion state of the specified branch. If the branch already
* has the specified expansion state, nothing happens.
* <p> The listeners are polled first to give them a chance to veto the operation
* for any reason. If the vote passes, then the state is changed.
*
* @param path The path to the branch node.
* @param expanded <tt>true</tt> to expand the branch; <tt>false</tt> to
* collapse it.
*/
public void setBranchExpanded(Path path, boolean expanded) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
// Give listeners a chance to veto the operation
Vote vote = treeViewBranchListeners.previewBranchExpandedChange(this, path);
if (vote != Vote.APPROVE) {
treeViewBranchListeners.branchExpandedChangeVetoed(this, path, vote);
} else {
int index = expandedPaths.indexOf(path);
if (expanded && index < 0) {
// Monitor the branch
monitorBranch(path);
// Update the expanded paths
expandedPaths.add(new ImmutablePath(path));
// Notify listeners
treeViewBranchListeners.branchExpanded(this, path);
} else if (!expanded && index >= 0) {
// Update the expanded paths
expandedPaths.remove(index, 1);
// Notify listeners
treeViewBranchListeners.branchCollapsed(this, path);
}
}
}
/**
* Tells whether or not the specified branch is expanded.
*
* @param path The path to the branch node.
* @return <tt>true</tt> if the branch is expanded; <tt>false</tt> otherwise.
*/
public boolean isBranchExpanded(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
return (expandedPaths.indexOf(path) >= 0);
}
/**
* Expands the branch at the specified path. If the branch is already
* expanded, nothing happens.
*
* @param path The path to the branch node.
*/
public final void expandBranch(Path path) {
setBranchExpanded(path, true);
}
/**
* Expands all branches in the tree view.
*/
@SuppressWarnings("unchecked")
public final void expandAll() {
Sequence.Tree.ItemIterator<Object> itemIterator = Sequence.Tree.depthFirstIterator((List<Object>) treeData);
while (itemIterator.hasNext()) {
Object node = itemIterator.next();
if (node instanceof List<?>) {
Path path = itemIterator.getPath();
if (path.getLength() > 0) {
expandBranch(path);
}
}
}
}
/**
* Collapses the branch at the specified path. If the branch is already
* collapsed, nothing happens.
*
* @param path The path to the branch node.
*/
public final void collapseBranch(Path path) {
setBranchExpanded(path, false);
}
/**
* Collapses all branches in the tree view.
*/
@SuppressWarnings("unchecked")
public final void collapseAll() {
Sequence.Tree.ItemIterator<Object> itemIterator = Sequence.Tree.depthFirstIterator((List<Object>) treeData);
while (itemIterator.hasNext()) {
Object node = itemIterator.next();
if (node instanceof List<?>) {
Path path = itemIterator.getPath();
if (path.getLength() > 0) {
collapseBranch(path);
}
}
}
}
/**
* Ensures that this tree view is listening for list events on every branch
* node along the specified path.
*
* @param path A path leading to a nested branch node.
* @throws IndexOutOfBoundsException If a path element is out of bounds.
* @throws IllegalArgumentException If the path contains any leaf nodes.
*/
private void monitorBranch(Path path) {
BranchHandler parent = rootBranchHandler;
for (int i = 0, n = path.getLength(); i < n; i++) {
int index = path.get(i);
if (index < 0 || index >= parent.getLength()) {
throw new IndexOutOfBoundsException("Branch path out of bounds: " + path);
}
BranchHandler child = parent.get(index);
if (child == null) {
List<?> parentBranchData = parent.getBranchData();
Object childData = parentBranchData.get(index);
if (!(childData instanceof List<?>)) {
throw new IllegalArgumentException("Unexpected leaf in branch path: " + path);
}
child = new BranchHandler(parent, (List<?>) childData);
parent.update(index, child);
}
parent = child;
}
}
/**
* Gets the path to the node found at the specified y-coordinate (relative
* to the tree view).
*
* @param y The y-coordinate in pixels.
* @return The path to the node, or <tt>null</tt> if there is no node being
* painted at the specified y-coordinate.
*/
public Path getNodeAt(int y) {
TreeView.Skin treeViewSkin = (TreeView.Skin) getSkin();
return treeViewSkin.getNodeAt(y);
}
/**
* Gets the bounds of the node at the specified path relative to the tree
* view. Note that all nodes are left aligned with the tree; to get the
* pixel value of a node's indent, use {@link #getNodeIndent(int)}.
*
* @param path The path to the node.
* @return The bounds, or <tt>null</tt> if the node is not currently visible.
*/
public Bounds getNodeBounds(Path path) {
if (path == null) {
throw new IllegalArgumentException("path is null.");
}
if (path.getLength() == 0) {
throw new IllegalArgumentException("path is empty.");
}
TreeView.Skin treeViewSkin = (TreeView.Skin) getSkin();
return treeViewSkin.getNodeBounds(path);
}
/**
* Gets the pixel indent of nodes at the specified depth. Depth is measured
* in generations away from the tree view's "root" node, which is
* represented by the {@link #getTreeData() tree data}.
*
* @param depth The depth, where the first child of the root has depth 1,
* the child of that branch has depth 2, etc.
* @return The indent in pixels.
*/
public int getNodeIndent(int depth) {
TreeView.Skin treeViewSkin = (TreeView.Skin) getSkin();
return treeViewSkin.getNodeIndent(depth);
}
/**
* Gets the row index of the node, as seen in the current visible nodes
* list. Note that as branches are expanded and collapsed, the row index of
* any given node in the tree will change.
*
* @param path The path to the node.
* @return The row index of the node, or <tt>-1</tt> if the node is not
* currently visible.
*/
public int getRowIndex(Path path) {
TreeView.Skin treeViewSkin = (TreeView.Skin) getSkin();
return treeViewSkin.getRowIndex(path);
}
/**
* Gets the <tt>TreeViewListener</tt>s. Developers interested in these
* events can register for notification on these events by adding themselves
* to the listener list.
*
* @return The tree view listeners.
*/
public ListenerList<TreeViewListener> getTreeViewListeners() {
return treeViewListeners;
}
/**
* Gets the <tt>TreeViewBranchListener</tt>s. Developers interested in these
* events can register for notification on these events by adding themselves
* to the listener list.
*
* @return The tree view branch listeners.
*/
public ListenerList<TreeViewBranchListener> getTreeViewBranchListeners() {
return treeViewBranchListeners;
}
/**
* Gets the <tt>TreeViewNodeListener</tt>s. Developers interested in these
* events can register for notification on these events by adding themselves
* to the listener list.
*
* @return The tree view node listeners.
*/
public ListenerList<TreeViewNodeListener> getTreeViewNodeListeners() {
return treeViewNodeListeners;
}
/**
* Gets the <tt>TreeViewNodeStateListener</tt>s. Developers interested in
* these events can register for notification on these events by adding
* themselves to the listener list.
*
* @return The tree view node state listeners.
*/
public ListenerList<TreeViewNodeStateListener> getTreeViewNodeStateListeners() {
return treeViewNodeStateListeners;
}
/**
* Gets the <tt>TreeViewSelectionListener</tt>s. Developers interested in
* these events can register for notification on these events by adding
* themselves to the listener list.
*
* @return The tree view selection listeners.
*/
public ListenerList<TreeViewSelectionListener> getTreeViewSelectionListeners() {
return treeViewSelectionListeners;
}
}