You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1769 lines
61 KiB
1769 lines
61 KiB
/*
|
|
* Copyright (c) 1998, 2013, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
|
|
package javax.swing.tree;
|
|
|
|
import javax.swing.event.TreeModelEvent;
|
|
import java.awt.Rectangle;
|
|
import java.util.Enumeration;
|
|
import java.util.Hashtable;
|
|
import java.util.NoSuchElementException;
|
|
import java.util.Stack;
|
|
import java.util.Vector;
|
|
|
|
import sun.swing.SwingUtilities2;
|
|
|
|
/**
|
|
* NOTE: This will become more open in a future release.
|
|
* <p>
|
|
* <strong>Warning:</strong>
|
|
* Serialized objects of this class will not be compatible with
|
|
* future Swing releases. The current serialization support is
|
|
* appropriate for short term storage or RMI between applications running
|
|
* the same version of Swing. As of 1.4, support for long term storage
|
|
* of all JavaBeans™
|
|
* has been added to the <code>java.beans</code> package.
|
|
* Please see {@link java.beans.XMLEncoder}.
|
|
*
|
|
* @author Rob Davis
|
|
* @author Ray Ryan
|
|
* @author Scott Violet
|
|
*/
|
|
|
|
public class VariableHeightLayoutCache extends AbstractLayoutCache {
|
|
/**
|
|
* The array of nodes that are currently visible, in the order they
|
|
* are displayed.
|
|
*/
|
|
private Vector<Object> visibleNodes;
|
|
|
|
/**
|
|
* This is set to true if one of the entries has an invalid size.
|
|
*/
|
|
private boolean updateNodeSizes;
|
|
|
|
/**
|
|
* The root node of the internal cache of nodes that have been shown.
|
|
* If the treeModel is vending a network rather than a true tree,
|
|
* there may be one cached node for each path to a modeled node.
|
|
*/
|
|
private TreeStateNode root;
|
|
|
|
/**
|
|
* Used in getting sizes for nodes to avoid creating a new Rectangle
|
|
* every time a size is needed.
|
|
*/
|
|
private Rectangle boundsBuffer;
|
|
|
|
/**
|
|
* Maps from <code>TreePath</code> to a <code>TreeStateNode</code>.
|
|
*/
|
|
private Hashtable<TreePath, TreeStateNode> treePathMapping;
|
|
|
|
/**
|
|
* A stack of stacks.
|
|
*/
|
|
private Stack<Stack<TreePath>> tempStacks;
|
|
|
|
|
|
public VariableHeightLayoutCache() {
|
|
super();
|
|
tempStacks = new Stack<Stack<TreePath>>();
|
|
visibleNodes = new Vector<Object>();
|
|
boundsBuffer = new Rectangle();
|
|
treePathMapping = new Hashtable<TreePath, TreeStateNode>();
|
|
}
|
|
|
|
/**
|
|
* Sets the <code>TreeModel</code> that will provide the data.
|
|
*
|
|
* @param newModel the <code>TreeModel</code> that is to provide the data
|
|
* @beaninfo
|
|
* bound: true
|
|
* description: The TreeModel that will provide the data.
|
|
*/
|
|
public void setModel(TreeModel newModel) {
|
|
super.setModel(newModel);
|
|
rebuild(false);
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not the root node from
|
|
* the <code>TreeModel</code> is visible.
|
|
*
|
|
* @param rootVisible true if the root node of the tree is to be displayed
|
|
* @see #rootVisible
|
|
* @beaninfo
|
|
* bound: true
|
|
* description: Whether or not the root node
|
|
* from the TreeModel is visible.
|
|
*/
|
|
public void setRootVisible(boolean rootVisible) {
|
|
if(isRootVisible() != rootVisible && root != null) {
|
|
if(rootVisible) {
|
|
root.updatePreferredSize(0);
|
|
visibleNodes.insertElementAt(root, 0);
|
|
}
|
|
else if(visibleNodes.size() > 0) {
|
|
visibleNodes.removeElementAt(0);
|
|
if(treeSelectionModel != null)
|
|
treeSelectionModel.removeSelectionPath
|
|
(root.getTreePath());
|
|
}
|
|
if(treeSelectionModel != null)
|
|
treeSelectionModel.resetRowSelection();
|
|
if(getRowCount() > 0)
|
|
getNode(0).setYOrigin(0);
|
|
updateYLocationsFrom(0);
|
|
visibleNodesChanged();
|
|
}
|
|
super.setRootVisible(rootVisible);
|
|
}
|
|
|
|
/**
|
|
* Sets the height of each cell. If the specified value
|
|
* is less than or equal to zero the current cell renderer is
|
|
* queried for each row's height.
|
|
*
|
|
* @param rowHeight the height of each cell, in pixels
|
|
* @beaninfo
|
|
* bound: true
|
|
* description: The height of each cell.
|
|
*/
|
|
public void setRowHeight(int rowHeight) {
|
|
if(rowHeight != getRowHeight()) {
|
|
super.setRowHeight(rowHeight);
|
|
invalidateSizes();
|
|
this.visibleNodesChanged();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the renderer that is responsible for drawing nodes in the tree.
|
|
* @param nd the renderer
|
|
*/
|
|
public void setNodeDimensions(NodeDimensions nd) {
|
|
super.setNodeDimensions(nd);
|
|
invalidateSizes();
|
|
visibleNodesChanged();
|
|
}
|
|
|
|
/**
|
|
* Marks the path <code>path</code> expanded state to
|
|
* <code>isExpanded</code>.
|
|
* @param path the <code>TreePath</code> of interest
|
|
* @param isExpanded true if the path should be expanded, otherwise false
|
|
*/
|
|
public void setExpandedState(TreePath path, boolean isExpanded) {
|
|
if(path != null) {
|
|
if(isExpanded)
|
|
ensurePathIsExpanded(path, true);
|
|
else {
|
|
TreeStateNode node = getNodeForPath(path, false, true);
|
|
|
|
if(node != null) {
|
|
node.makeVisible();
|
|
node.collapse();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the path is expanded, and visible.
|
|
* @return true if the path is expanded and visible, otherwise false
|
|
*/
|
|
public boolean getExpandedState(TreePath path) {
|
|
TreeStateNode node = getNodeForPath(path, true, false);
|
|
|
|
return (node != null) ? (node.isVisible() && node.isExpanded()) :
|
|
false;
|
|
}
|
|
|
|
/**
|
|
* Returns the <code>Rectangle</code> enclosing the label portion
|
|
* into which the item identified by <code>path</code> will be drawn.
|
|
*
|
|
* @param path the path to be drawn
|
|
* @param placeIn the bounds of the enclosing rectangle
|
|
* @return the bounds of the enclosing rectangle or <code>null</code>
|
|
* if the node could not be ascertained
|
|
*/
|
|
public Rectangle getBounds(TreePath path, Rectangle placeIn) {
|
|
TreeStateNode node = getNodeForPath(path, true, false);
|
|
|
|
if(node != null) {
|
|
if(updateNodeSizes)
|
|
updateNodeSizes(false);
|
|
return node.getNodeBounds(placeIn);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the path for <code>row</code>. If <code>row</code>
|
|
* is not visible, <code>null</code> is returned.
|
|
*
|
|
* @param row the location of interest
|
|
* @return the path for <code>row</code>, or <code>null</code>
|
|
* if <code>row</code> is not visible
|
|
*/
|
|
public TreePath getPathForRow(int row) {
|
|
if(row >= 0 && row < getRowCount()) {
|
|
return getNode(row).getTreePath();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the row where the last item identified in path is visible.
|
|
* Will return -1 if any of the elements in path are not
|
|
* currently visible.
|
|
*
|
|
* @param path the <code>TreePath</code> of interest
|
|
* @return the row where the last item in path is visible
|
|
*/
|
|
public int getRowForPath(TreePath path) {
|
|
if(path == null)
|
|
return -1;
|
|
|
|
TreeStateNode visNode = getNodeForPath(path, true, false);
|
|
|
|
if(visNode != null)
|
|
return visNode.getRow();
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of visible rows.
|
|
* @return the number of visible rows
|
|
*/
|
|
public int getRowCount() {
|
|
return visibleNodes.size();
|
|
}
|
|
|
|
/**
|
|
* Instructs the <code>LayoutCache</code> that the bounds for
|
|
* <code>path</code> are invalid, and need to be updated.
|
|
*
|
|
* @param path the <code>TreePath</code> which is now invalid
|
|
*/
|
|
public void invalidatePathBounds(TreePath path) {
|
|
TreeStateNode node = getNodeForPath(path, true, false);
|
|
|
|
if(node != null) {
|
|
node.markSizeInvalid();
|
|
if(node.isVisible())
|
|
updateYLocationsFrom(node.getRow());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred height.
|
|
* @return the preferred height
|
|
*/
|
|
public int getPreferredHeight() {
|
|
// Get the height
|
|
int rowCount = getRowCount();
|
|
|
|
if(rowCount > 0) {
|
|
TreeStateNode node = getNode(rowCount - 1);
|
|
|
|
return node.getYOrigin() + node.getPreferredHeight();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred width and height for the region in
|
|
* <code>visibleRegion</code>.
|
|
*
|
|
* @param bounds the region being queried
|
|
*/
|
|
public int getPreferredWidth(Rectangle bounds) {
|
|
if(updateNodeSizes)
|
|
updateNodeSizes(false);
|
|
|
|
return getMaxNodeWidth();
|
|
}
|
|
|
|
/**
|
|
* Returns the path to the node that is closest to x,y. If
|
|
* there is nothing currently visible this will return <code>null</code>,
|
|
* otherwise it will always return a valid path.
|
|
* If you need to test if the
|
|
* returned object is exactly at x, y you should get the bounds for
|
|
* the returned path and test x, y against that.
|
|
*
|
|
* @param x the x-coordinate
|
|
* @param y the y-coordinate
|
|
* @return the path to the node that is closest to x, y
|
|
*/
|
|
public TreePath getPathClosestTo(int x, int y) {
|
|
if(getRowCount() == 0)
|
|
return null;
|
|
|
|
if(updateNodeSizes)
|
|
updateNodeSizes(false);
|
|
|
|
int row = getRowContainingYLocation(y);
|
|
|
|
return getNode(row).getTreePath();
|
|
}
|
|
|
|
/**
|
|
* Returns an <code>Enumerator</code> that increments over the visible paths
|
|
* starting at the passed in location. The ordering of the enumeration
|
|
* is based on how the paths are displayed.
|
|
*
|
|
* @param path the location in the <code>TreePath</code> to start
|
|
* @return an <code>Enumerator</code> that increments over the visible
|
|
* paths
|
|
*/
|
|
public Enumeration<TreePath> getVisiblePathsFrom(TreePath path) {
|
|
TreeStateNode node = getNodeForPath(path, true, false);
|
|
|
|
if(node != null) {
|
|
return new VisibleTreeStateNodeEnumeration(node);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of visible children for <code>path</code>.
|
|
* @return the number of visible children for <code>path</code>
|
|
*/
|
|
public int getVisibleChildCount(TreePath path) {
|
|
TreeStateNode node = getNodeForPath(path, true, false);
|
|
|
|
return (node != null) ? node.getVisibleChildCount() : 0;
|
|
}
|
|
|
|
/**
|
|
* Informs the <code>TreeState</code> that it needs to recalculate
|
|
* all the sizes it is referencing.
|
|
*/
|
|
public void invalidateSizes() {
|
|
if(root != null)
|
|
root.deepMarkSizeInvalid();
|
|
if(!isFixedRowHeight() && visibleNodes.size() > 0) {
|
|
updateNodeSizes(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the value identified by <code>path</code> is
|
|
* currently expanded.
|
|
* @return true if the value identified by <code>path</code> is
|
|
* currently expanded
|
|
*/
|
|
public boolean isExpanded(TreePath path) {
|
|
if(path != null) {
|
|
TreeStateNode lastNode = getNodeForPath(path, true, false);
|
|
|
|
return (lastNode != null && lastNode.isExpanded());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// TreeModelListener methods
|
|
//
|
|
|
|
/**
|
|
* Invoked after a node (or a set of siblings) has changed in some
|
|
* way. The node(s) have not changed locations in the tree or
|
|
* altered their children arrays, but other attributes have
|
|
* changed and may affect presentation. Example: the name of a
|
|
* file has changed, but it is in the same location in the file
|
|
* system.
|
|
*
|
|
* <p><code>e.path</code> returns the path the parent of the
|
|
* changed node(s).
|
|
*
|
|
* <p><code>e.childIndices</code> returns the index(es) of the
|
|
* changed node(s).
|
|
*
|
|
* @param e the <code>TreeModelEvent</code> of interest
|
|
*/
|
|
public void treeNodesChanged(TreeModelEvent e) {
|
|
if(e != null) {
|
|
int changedIndexs[];
|
|
TreeStateNode changedNode;
|
|
|
|
changedIndexs = e.getChildIndices();
|
|
changedNode = getNodeForPath(SwingUtilities2.getTreePath(e, getModel()), false, false);
|
|
if(changedNode != null) {
|
|
Object changedValue = changedNode.getValue();
|
|
|
|
/* Update the size of the changed node, as well as all the
|
|
child indexs that are passed in. */
|
|
changedNode.updatePreferredSize();
|
|
if(changedNode.hasBeenExpanded() && changedIndexs != null) {
|
|
int counter;
|
|
TreeStateNode changedChildNode;
|
|
|
|
for(counter = 0; counter < changedIndexs.length;
|
|
counter++) {
|
|
changedChildNode = (TreeStateNode)changedNode
|
|
.getChildAt(changedIndexs[counter]);
|
|
/* Reset the user object. */
|
|
changedChildNode.setUserObject
|
|
(treeModel.getChild(changedValue,
|
|
changedIndexs[counter]));
|
|
changedChildNode.updatePreferredSize();
|
|
}
|
|
}
|
|
else if (changedNode == root) {
|
|
// Null indicies for root indicates it changed.
|
|
changedNode.updatePreferredSize();
|
|
}
|
|
if(!isFixedRowHeight()) {
|
|
int aRow = changedNode.getRow();
|
|
|
|
if(aRow != -1)
|
|
this.updateYLocationsFrom(aRow);
|
|
}
|
|
this.visibleNodesChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Invoked after nodes have been inserted into the tree.
|
|
*
|
|
* <p><code>e.path</code> returns the parent of the new nodes.
|
|
* <p><code>e.childIndices</code> returns the indices of the new nodes in
|
|
* ascending order.
|
|
*
|
|
* @param e the <code>TreeModelEvent</code> of interest
|
|
*/
|
|
public void treeNodesInserted(TreeModelEvent e) {
|
|
if(e != null) {
|
|
int changedIndexs[];
|
|
TreeStateNode changedParentNode;
|
|
|
|
changedIndexs = e.getChildIndices();
|
|
changedParentNode = getNodeForPath(SwingUtilities2.getTreePath(e, getModel()), false, false);
|
|
/* Only need to update the children if the node has been
|
|
expanded once. */
|
|
// PENDING(scott): make sure childIndexs is sorted!
|
|
if(changedParentNode != null && changedIndexs != null &&
|
|
changedIndexs.length > 0) {
|
|
if(changedParentNode.hasBeenExpanded()) {
|
|
boolean makeVisible;
|
|
int counter;
|
|
Object changedParent;
|
|
TreeStateNode newNode;
|
|
int oldChildCount = changedParentNode.
|
|
getChildCount();
|
|
|
|
changedParent = changedParentNode.getValue();
|
|
makeVisible = ((changedParentNode == root &&
|
|
!rootVisible) ||
|
|
(changedParentNode.getRow() != -1 &&
|
|
changedParentNode.isExpanded()));
|
|
for(counter = 0;counter < changedIndexs.length;counter++)
|
|
{
|
|
newNode = this.createNodeAt(changedParentNode,
|
|
changedIndexs[counter]);
|
|
}
|
|
if(oldChildCount == 0) {
|
|
// Update the size of the parent.
|
|
changedParentNode.updatePreferredSize();
|
|
}
|
|
if(treeSelectionModel != null)
|
|
treeSelectionModel.resetRowSelection();
|
|
/* Update the y origins from the index of the parent
|
|
to the end of the visible rows. */
|
|
if(!isFixedRowHeight() && (makeVisible ||
|
|
(oldChildCount == 0 &&
|
|
changedParentNode.isVisible()))) {
|
|
if(changedParentNode == root)
|
|
this.updateYLocationsFrom(0);
|
|
else
|
|
this.updateYLocationsFrom(changedParentNode.
|
|
getRow());
|
|
this.visibleNodesChanged();
|
|
}
|
|
else if(makeVisible)
|
|
this.visibleNodesChanged();
|
|
}
|
|
else if(treeModel.getChildCount(changedParentNode.getValue())
|
|
- changedIndexs.length == 0) {
|
|
changedParentNode.updatePreferredSize();
|
|
if(!isFixedRowHeight() && changedParentNode.isVisible())
|
|
updateYLocationsFrom(changedParentNode.getRow());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked after nodes have been removed from the tree. Note that
|
|
* if a subtree is removed from the tree, this method may only be
|
|
* invoked once for the root of the removed subtree, not once for
|
|
* each individual set of siblings removed.
|
|
*
|
|
* <p><code>e.path</code> returns the former parent of the deleted nodes.
|
|
*
|
|
* <p><code>e.childIndices</code> returns the indices the nodes had
|
|
* before they were deleted in ascending order.
|
|
*
|
|
* @param e the <code>TreeModelEvent</code> of interest
|
|
*/
|
|
public void treeNodesRemoved(TreeModelEvent e) {
|
|
if(e != null) {
|
|
int changedIndexs[];
|
|
TreeStateNode changedParentNode;
|
|
|
|
changedIndexs = e.getChildIndices();
|
|
changedParentNode = getNodeForPath(SwingUtilities2.getTreePath(e, getModel()), false, false);
|
|
// PENDING(scott): make sure that changedIndexs are sorted in
|
|
// ascending order.
|
|
if(changedParentNode != null && changedIndexs != null &&
|
|
changedIndexs.length > 0) {
|
|
if(changedParentNode.hasBeenExpanded()) {
|
|
boolean makeInvisible;
|
|
int counter;
|
|
int removedRow;
|
|
TreeStateNode removedNode;
|
|
|
|
makeInvisible = ((changedParentNode == root &&
|
|
!rootVisible) ||
|
|
(changedParentNode.getRow() != -1 &&
|
|
changedParentNode.isExpanded()));
|
|
for(counter = changedIndexs.length - 1;counter >= 0;
|
|
counter--) {
|
|
removedNode = (TreeStateNode)changedParentNode.
|
|
getChildAt(changedIndexs[counter]);
|
|
if(removedNode.isExpanded()) {
|
|
removedNode.collapse(false);
|
|
}
|
|
|
|
/* Let the selection model now. */
|
|
if(makeInvisible) {
|
|
removedRow = removedNode.getRow();
|
|
if(removedRow != -1) {
|
|
visibleNodes.removeElementAt(removedRow);
|
|
}
|
|
}
|
|
changedParentNode.remove(changedIndexs[counter]);
|
|
}
|
|
if(changedParentNode.getChildCount() == 0) {
|
|
// Update the size of the parent.
|
|
changedParentNode.updatePreferredSize();
|
|
if (changedParentNode.isExpanded() &&
|
|
changedParentNode.isLeaf()) {
|
|
// Node has become a leaf, collapse it.
|
|
changedParentNode.collapse(false);
|
|
}
|
|
}
|
|
if(treeSelectionModel != null)
|
|
treeSelectionModel.resetRowSelection();
|
|
/* Update the y origins from the index of the parent
|
|
to the end of the visible rows. */
|
|
if(!isFixedRowHeight() && (makeInvisible ||
|
|
(changedParentNode.getChildCount() == 0 &&
|
|
changedParentNode.isVisible()))) {
|
|
if(changedParentNode == root) {
|
|
/* It is possible for first row to have been
|
|
removed if the root isn't visible, in which
|
|
case ylocations will be off! */
|
|
if(getRowCount() > 0)
|
|
getNode(0).setYOrigin(0);
|
|
updateYLocationsFrom(0);
|
|
}
|
|
else
|
|
updateYLocationsFrom(changedParentNode.getRow());
|
|
this.visibleNodesChanged();
|
|
}
|
|
else if(makeInvisible)
|
|
this.visibleNodesChanged();
|
|
}
|
|
else if(treeModel.getChildCount(changedParentNode.getValue())
|
|
== 0) {
|
|
changedParentNode.updatePreferredSize();
|
|
if(!isFixedRowHeight() && changedParentNode.isVisible())
|
|
this.updateYLocationsFrom(changedParentNode.getRow());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked after the tree has drastically changed structure from a
|
|
* given node down. If the path returned by <code>e.getPath</code>
|
|
* is of length one and the first element does not identify the
|
|
* current root node the first element should become the new root
|
|
* of the tree.
|
|
*
|
|
* <p><code>e.path</code> holds the path to the node.
|
|
* <p><code>e.childIndices</code> returns <code>null</code>.
|
|
*
|
|
* @param e the <code>TreeModelEvent</code> of interest
|
|
*/
|
|
public void treeStructureChanged(TreeModelEvent e) {
|
|
if(e != null)
|
|
{
|
|
TreePath changedPath = SwingUtilities2.getTreePath(e, getModel());
|
|
TreeStateNode changedNode;
|
|
|
|
changedNode = getNodeForPath(changedPath, false, false);
|
|
|
|
// Check if root has changed, either to a null root, or
|
|
// to an entirely new root.
|
|
if(changedNode == root ||
|
|
(changedNode == null &&
|
|
((changedPath == null && treeModel != null &&
|
|
treeModel.getRoot() == null) ||
|
|
(changedPath != null && changedPath.getPathCount() == 1)))) {
|
|
rebuild(true);
|
|
}
|
|
else if(changedNode != null) {
|
|
int nodeIndex, oldRow;
|
|
TreeStateNode newNode, parent;
|
|
boolean wasExpanded, wasVisible;
|
|
int newIndex;
|
|
|
|
wasExpanded = changedNode.isExpanded();
|
|
wasVisible = (changedNode.getRow() != -1);
|
|
/* Remove the current node and recreate a new one. */
|
|
parent = (TreeStateNode)changedNode.getParent();
|
|
nodeIndex = parent.getIndex(changedNode);
|
|
if(wasVisible && wasExpanded) {
|
|
changedNode.collapse(false);
|
|
}
|
|
if(wasVisible)
|
|
visibleNodes.removeElement(changedNode);
|
|
changedNode.removeFromParent();
|
|
createNodeAt(parent, nodeIndex);
|
|
newNode = (TreeStateNode)parent.getChildAt(nodeIndex);
|
|
if(wasVisible && wasExpanded)
|
|
newNode.expand(false);
|
|
newIndex = newNode.getRow();
|
|
if(!isFixedRowHeight() && wasVisible) {
|
|
if(newIndex == 0)
|
|
updateYLocationsFrom(newIndex);
|
|
else
|
|
updateYLocationsFrom(newIndex - 1);
|
|
this.visibleNodesChanged();
|
|
}
|
|
else if(wasVisible)
|
|
this.visibleNodesChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// Local methods
|
|
//
|
|
|
|
private void visibleNodesChanged() {
|
|
}
|
|
|
|
/**
|
|
* Adds a mapping for node.
|
|
*/
|
|
private void addMapping(TreeStateNode node) {
|
|
treePathMapping.put(node.getTreePath(), node);
|
|
}
|
|
|
|
/**
|
|
* Removes the mapping for a previously added node.
|
|
*/
|
|
private void removeMapping(TreeStateNode node) {
|
|
treePathMapping.remove(node.getTreePath());
|
|
}
|
|
|
|
/**
|
|
* Returns the node previously added for <code>path</code>. This may
|
|
* return null, if you to create a node use getNodeForPath.
|
|
*/
|
|
private TreeStateNode getMapping(TreePath path) {
|
|
return treePathMapping.get(path);
|
|
}
|
|
|
|
/**
|
|
* Retursn the bounds for row, <code>row</code> by reference in
|
|
* <code>placeIn</code>. If <code>placeIn</code> is null a new
|
|
* Rectangle will be created and returned.
|
|
*/
|
|
private Rectangle getBounds(int row, Rectangle placeIn) {
|
|
if(updateNodeSizes)
|
|
updateNodeSizes(false);
|
|
|
|
if(row >= 0 && row < getRowCount()) {
|
|
return getNode(row).getNodeBounds(placeIn);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Completely rebuild the tree, all expanded state, and node caches are
|
|
* removed. All nodes are collapsed, except the root.
|
|
*/
|
|
private void rebuild(boolean clearSelection) {
|
|
Object rootObject;
|
|
|
|
treePathMapping.clear();
|
|
if(treeModel != null && (rootObject = treeModel.getRoot()) != null) {
|
|
root = createNodeForValue(rootObject);
|
|
root.path = new TreePath(rootObject);
|
|
addMapping(root);
|
|
root.updatePreferredSize(0);
|
|
visibleNodes.removeAllElements();
|
|
if (isRootVisible())
|
|
visibleNodes.addElement(root);
|
|
if(!root.isExpanded())
|
|
root.expand();
|
|
else {
|
|
Enumeration cursor = root.children();
|
|
while(cursor.hasMoreElements()) {
|
|
visibleNodes.addElement(cursor.nextElement());
|
|
}
|
|
if(!isFixedRowHeight())
|
|
updateYLocationsFrom(0);
|
|
}
|
|
}
|
|
else {
|
|
visibleNodes.removeAllElements();
|
|
root = null;
|
|
}
|
|
if(clearSelection && treeSelectionModel != null) {
|
|
treeSelectionModel.clearSelection();
|
|
}
|
|
this.visibleNodesChanged();
|
|
}
|
|
|
|
/**
|
|
* Creates a new node to represent the node at <I>childIndex</I> in
|
|
* <I>parent</I>s children. This should be called if the node doesn't
|
|
* already exist and <I>parent</I> has been expanded at least once.
|
|
* The newly created node will be made visible if <I>parent</I> is
|
|
* currently expanded. This does not update the position of any
|
|
* cells, nor update the selection if it needs to be. If succesful
|
|
* in creating the new TreeStateNode, it is returned, otherwise
|
|
* null is returned.
|
|
*/
|
|
private TreeStateNode createNodeAt(TreeStateNode parent,
|
|
int childIndex) {
|
|
boolean isParentRoot;
|
|
Object newValue;
|
|
TreeStateNode newChildNode;
|
|
|
|
newValue = treeModel.getChild(parent.getValue(), childIndex);
|
|
newChildNode = createNodeForValue(newValue);
|
|
parent.insert(newChildNode, childIndex);
|
|
newChildNode.updatePreferredSize(-1);
|
|
isParentRoot = (parent == root);
|
|
if(newChildNode != null && parent.isExpanded() &&
|
|
(parent.getRow() != -1 || isParentRoot)) {
|
|
int newRow;
|
|
|
|
/* Find the new row to insert this newly visible node at. */
|
|
if(childIndex == 0) {
|
|
if(isParentRoot && !isRootVisible())
|
|
newRow = 0;
|
|
else
|
|
newRow = parent.getRow() + 1;
|
|
}
|
|
else if(childIndex == parent.getChildCount())
|
|
newRow = parent.getLastVisibleNode().getRow() + 1;
|
|
else {
|
|
TreeStateNode previousNode;
|
|
|
|
previousNode = (TreeStateNode)parent.
|
|
getChildAt(childIndex - 1);
|
|
newRow = previousNode.getLastVisibleNode().getRow() + 1;
|
|
}
|
|
visibleNodes.insertElementAt(newChildNode, newRow);
|
|
}
|
|
return newChildNode;
|
|
}
|
|
|
|
/**
|
|
* Returns the TreeStateNode identified by path. This mirrors
|
|
* the behavior of getNodeForPath, but tries to take advantage of
|
|
* path if it is an instance of AbstractTreePath.
|
|
*/
|
|
private TreeStateNode getNodeForPath(TreePath path,
|
|
boolean onlyIfVisible,
|
|
boolean shouldCreate) {
|
|
if(path != null) {
|
|
TreeStateNode node;
|
|
|
|
node = getMapping(path);
|
|
if(node != null) {
|
|
if(onlyIfVisible && !node.isVisible())
|
|
return null;
|
|
return node;
|
|
}
|
|
|
|
// Check all the parent paths, until a match is found.
|
|
Stack<TreePath> paths;
|
|
|
|
if(tempStacks.size() == 0) {
|
|
paths = new Stack<TreePath>();
|
|
}
|
|
else {
|
|
paths = tempStacks.pop();
|
|
}
|
|
|
|
try {
|
|
paths.push(path);
|
|
path = path.getParentPath();
|
|
node = null;
|
|
while(path != null) {
|
|
node = getMapping(path);
|
|
if(node != null) {
|
|
// Found a match, create entries for all paths in
|
|
// paths.
|
|
while(node != null && paths.size() > 0) {
|
|
path = paths.pop();
|
|
node.getLoadedChildren(shouldCreate);
|
|
|
|
int childIndex = treeModel.
|
|
getIndexOfChild(node.getUserObject(),
|
|
path.getLastPathComponent());
|
|
|
|
if(childIndex == -1 ||
|
|
childIndex >= node.getChildCount() ||
|
|
(onlyIfVisible && !node.isVisible())) {
|
|
node = null;
|
|
}
|
|
else
|
|
node = (TreeStateNode)node.getChildAt
|
|
(childIndex);
|
|
}
|
|
return node;
|
|
}
|
|
paths.push(path);
|
|
path = path.getParentPath();
|
|
}
|
|
}
|
|
finally {
|
|
paths.removeAllElements();
|
|
tempStacks.push(paths);
|
|
}
|
|
// If we get here it means they share a different root!
|
|
// We could throw an exception...
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Updates the y locations of all of the visible nodes after
|
|
* location.
|
|
*/
|
|
private void updateYLocationsFrom(int location) {
|
|
if(location >= 0 && location < getRowCount()) {
|
|
int counter, maxCounter, newYOrigin;
|
|
TreeStateNode aNode;
|
|
|
|
aNode = getNode(location);
|
|
newYOrigin = aNode.getYOrigin() + aNode.getPreferredHeight();
|
|
for(counter = location + 1, maxCounter = visibleNodes.size();
|
|
counter < maxCounter;counter++) {
|
|
aNode = (TreeStateNode)visibleNodes.
|
|
elementAt(counter);
|
|
aNode.setYOrigin(newYOrigin);
|
|
newYOrigin += aNode.getPreferredHeight();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets the y origin of all the visible nodes as well as messaging
|
|
* all the visible nodes to updatePreferredSize(). You should not
|
|
* normally have to call this. Expanding and contracting the nodes
|
|
* automaticly adjusts the locations.
|
|
* updateAll determines if updatePreferredSize() is call on all nodes
|
|
* or just those that don't have a valid size.
|
|
*/
|
|
private void updateNodeSizes(boolean updateAll) {
|
|
int aY, counter, maxCounter;
|
|
TreeStateNode node;
|
|
|
|
updateNodeSizes = false;
|
|
for(aY = counter = 0, maxCounter = visibleNodes.size();
|
|
counter < maxCounter; counter++) {
|
|
node = (TreeStateNode)visibleNodes.elementAt(counter);
|
|
node.setYOrigin(aY);
|
|
if(updateAll || !node.hasValidSize())
|
|
node.updatePreferredSize(counter);
|
|
aY += node.getPreferredHeight();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the row containing location. If there
|
|
* are no rows, -1 is returned. If location is beyond the last
|
|
* row index, the last row index is returned.
|
|
*/
|
|
private int getRowContainingYLocation(int location) {
|
|
if(isFixedRowHeight()) {
|
|
if(getRowCount() == 0)
|
|
return -1;
|
|
return Math.max(0, Math.min(getRowCount() - 1,
|
|
location / getRowHeight()));
|
|
}
|
|
|
|
int max, maxY, mid, min, minY;
|
|
TreeStateNode node;
|
|
|
|
if((max = getRowCount()) <= 0)
|
|
return -1;
|
|
mid = min = 0;
|
|
while(min < max) {
|
|
mid = (max - min) / 2 + min;
|
|
node = (TreeStateNode)visibleNodes.elementAt(mid);
|
|
minY = node.getYOrigin();
|
|
maxY = minY + node.getPreferredHeight();
|
|
if(location < minY) {
|
|
max = mid - 1;
|
|
}
|
|
else if(location >= maxY) {
|
|
min = mid + 1;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
if(min == max) {
|
|
mid = min;
|
|
if(mid >= getRowCount())
|
|
mid = getRowCount() - 1;
|
|
}
|
|
return mid;
|
|
}
|
|
|
|
/**
|
|
* Ensures that all the path components in path are expanded, accept
|
|
* for the last component which will only be expanded if expandLast
|
|
* is true.
|
|
* Returns true if succesful in finding the path.
|
|
*/
|
|
private void ensurePathIsExpanded(TreePath aPath, boolean expandLast) {
|
|
if(aPath != null) {
|
|
// Make sure the last entry isn't a leaf.
|
|
if(treeModel.isLeaf(aPath.getLastPathComponent())) {
|
|
aPath = aPath.getParentPath();
|
|
expandLast = true;
|
|
}
|
|
if(aPath != null) {
|
|
TreeStateNode lastNode = getNodeForPath(aPath, false,
|
|
true);
|
|
|
|
if(lastNode != null) {
|
|
lastNode.makeVisible();
|
|
if(expandLast)
|
|
lastNode.expand();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the AbstractTreeUI.VisibleNode displayed at the given row
|
|
*/
|
|
private TreeStateNode getNode(int row) {
|
|
return (TreeStateNode)visibleNodes.elementAt(row);
|
|
}
|
|
|
|
/**
|
|
* Returns the maximum node width.
|
|
*/
|
|
private int getMaxNodeWidth() {
|
|
int maxWidth = 0;
|
|
int nodeWidth;
|
|
int counter;
|
|
TreeStateNode node;
|
|
|
|
for(counter = getRowCount() - 1;counter >= 0;counter--) {
|
|
node = this.getNode(counter);
|
|
nodeWidth = node.getPreferredWidth() + node.getXOrigin();
|
|
if(nodeWidth > maxWidth)
|
|
maxWidth = nodeWidth;
|
|
}
|
|
return maxWidth;
|
|
}
|
|
|
|
/**
|
|
* Responsible for creating a TreeStateNode that will be used
|
|
* to track display information about value.
|
|
*/
|
|
private TreeStateNode createNodeForValue(Object value) {
|
|
return new TreeStateNode(value);
|
|
}
|
|
|
|
|
|
/**
|
|
* TreeStateNode is used to keep track of each of
|
|
* the nodes that have been expanded. This will also cache the preferred
|
|
* size of the value it represents.
|
|
*/
|
|
private class TreeStateNode extends DefaultMutableTreeNode {
|
|
/** Preferred size needed to draw the user object. */
|
|
protected int preferredWidth;
|
|
protected int preferredHeight;
|
|
|
|
/** X location that the user object will be drawn at. */
|
|
protected int xOrigin;
|
|
|
|
/** Y location that the user object will be drawn at. */
|
|
protected int yOrigin;
|
|
|
|
/** Is this node currently expanded? */
|
|
protected boolean expanded;
|
|
|
|
/** Has this node been expanded at least once? */
|
|
protected boolean hasBeenExpanded;
|
|
|
|
/** Path of this node. */
|
|
protected TreePath path;
|
|
|
|
|
|
public TreeStateNode(Object value) {
|
|
super(value);
|
|
}
|
|
|
|
//
|
|
// Overriden DefaultMutableTreeNode methods
|
|
//
|
|
|
|
/**
|
|
* Messaged when this node is added somewhere, resets the path
|
|
* and adds a mapping from path to this node.
|
|
*/
|
|
public void setParent(MutableTreeNode parent) {
|
|
super.setParent(parent);
|
|
if(parent != null) {
|
|
path = ((TreeStateNode)parent).getTreePath().
|
|
pathByAddingChild(getUserObject());
|
|
addMapping(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Messaged when this node is removed from its parent, this messages
|
|
* <code>removedFromMapping</code> to remove all the children.
|
|
*/
|
|
public void remove(int childIndex) {
|
|
TreeStateNode node = (TreeStateNode)getChildAt(childIndex);
|
|
|
|
node.removeFromMapping();
|
|
super.remove(childIndex);
|
|
}
|
|
|
|
/**
|
|
* Messaged to set the user object. This resets the path.
|
|
*/
|
|
public void setUserObject(Object o) {
|
|
super.setUserObject(o);
|
|
if(path != null) {
|
|
TreeStateNode parent = (TreeStateNode)getParent();
|
|
|
|
if(parent != null)
|
|
resetChildrenPaths(parent.getTreePath());
|
|
else
|
|
resetChildrenPaths(null);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the children of the receiver.
|
|
* If the receiver is not currently expanded, this will return an
|
|
* empty enumeration.
|
|
*/
|
|
public Enumeration children() {
|
|
if (!this.isExpanded()) {
|
|
return DefaultMutableTreeNode.EMPTY_ENUMERATION;
|
|
} else {
|
|
return super.children();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the receiver is a leaf.
|
|
*/
|
|
public boolean isLeaf() {
|
|
return getModel().isLeaf(this.getValue());
|
|
}
|
|
|
|
//
|
|
// VariableHeightLayoutCache
|
|
//
|
|
|
|
/**
|
|
* Returns the location and size of this node.
|
|
*/
|
|
public Rectangle getNodeBounds(Rectangle placeIn) {
|
|
if(placeIn == null)
|
|
placeIn = new Rectangle(getXOrigin(), getYOrigin(),
|
|
getPreferredWidth(),
|
|
getPreferredHeight());
|
|
else {
|
|
placeIn.x = getXOrigin();
|
|
placeIn.y = getYOrigin();
|
|
placeIn.width = getPreferredWidth();
|
|
placeIn.height = getPreferredHeight();
|
|
}
|
|
return placeIn;
|
|
}
|
|
|
|
/**
|
|
* @return x location to draw node at.
|
|
*/
|
|
public int getXOrigin() {
|
|
if(!hasValidSize())
|
|
updatePreferredSize(getRow());
|
|
return xOrigin;
|
|
}
|
|
|
|
/**
|
|
* Returns the y origin the user object will be drawn at.
|
|
*/
|
|
public int getYOrigin() {
|
|
if(isFixedRowHeight()) {
|
|
int aRow = getRow();
|
|
|
|
if(aRow == -1)
|
|
return -1;
|
|
return getRowHeight() * aRow;
|
|
}
|
|
return yOrigin;
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred height of the receiver.
|
|
*/
|
|
public int getPreferredHeight() {
|
|
if(isFixedRowHeight())
|
|
return getRowHeight();
|
|
else if(!hasValidSize())
|
|
updatePreferredSize(getRow());
|
|
return preferredHeight;
|
|
}
|
|
|
|
/**
|
|
* Returns the preferred width of the receiver.
|
|
*/
|
|
public int getPreferredWidth() {
|
|
if(!hasValidSize())
|
|
updatePreferredSize(getRow());
|
|
return preferredWidth;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this node has a valid size.
|
|
*/
|
|
public boolean hasValidSize() {
|
|
return (preferredHeight != 0);
|
|
}
|
|
|
|
/**
|
|
* Returns the row of the receiver.
|
|
*/
|
|
public int getRow() {
|
|
return visibleNodes.indexOf(this);
|
|
}
|
|
|
|
/**
|
|
* Returns true if this node has been expanded at least once.
|
|
*/
|
|
public boolean hasBeenExpanded() {
|
|
return hasBeenExpanded;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the receiver has been expanded.
|
|
*/
|
|
public boolean isExpanded() {
|
|
return expanded;
|
|
}
|
|
|
|
/**
|
|
* Returns the last visible node that is a child of this
|
|
* instance.
|
|
*/
|
|
public TreeStateNode getLastVisibleNode() {
|
|
TreeStateNode node = this;
|
|
|
|
while(node.isExpanded() && node.getChildCount() > 0)
|
|
node = (TreeStateNode)node.getLastChild();
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the receiver is currently visible.
|
|
*/
|
|
public boolean isVisible() {
|
|
if(this == root)
|
|
return true;
|
|
|
|
TreeStateNode parent = (TreeStateNode)getParent();
|
|
|
|
return (parent != null && parent.isExpanded() &&
|
|
parent.isVisible());
|
|
}
|
|
|
|
/**
|
|
* Returns the number of children this will have. If the children
|
|
* have not yet been loaded, this messages the model.
|
|
*/
|
|
public int getModelChildCount() {
|
|
if(hasBeenExpanded)
|
|
return super.getChildCount();
|
|
return getModel().getChildCount(getValue());
|
|
}
|
|
|
|
/**
|
|
* Returns the number of visible children, that is the number of
|
|
* children that are expanded, or leafs.
|
|
*/
|
|
public int getVisibleChildCount() {
|
|
int childCount = 0;
|
|
|
|
if(isExpanded()) {
|
|
int maxCounter = getChildCount();
|
|
|
|
childCount += maxCounter;
|
|
for(int counter = 0; counter < maxCounter; counter++)
|
|
childCount += ((TreeStateNode)getChildAt(counter)).
|
|
getVisibleChildCount();
|
|
}
|
|
return childCount;
|
|
}
|
|
|
|
/**
|
|
* Toggles the receiver between expanded and collapsed.
|
|
*/
|
|
public void toggleExpanded() {
|
|
if (isExpanded()) {
|
|
collapse();
|
|
} else {
|
|
expand();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Makes the receiver visible, but invoking
|
|
* <code>expandParentAndReceiver</code> on the superclass.
|
|
*/
|
|
public void makeVisible() {
|
|
TreeStateNode parent = (TreeStateNode)getParent();
|
|
|
|
if(parent != null)
|
|
parent.expandParentAndReceiver();
|
|
}
|
|
|
|
/**
|
|
* Expands the receiver.
|
|
*/
|
|
public void expand() {
|
|
expand(true);
|
|
}
|
|
|
|
/**
|
|
* Collapses the receiver.
|
|
*/
|
|
public void collapse() {
|
|
collapse(true);
|
|
}
|
|
|
|
/**
|
|
* Returns the value the receiver is representing. This is a cover
|
|
* for getUserObject.
|
|
*/
|
|
public Object getValue() {
|
|
return getUserObject();
|
|
}
|
|
|
|
/**
|
|
* Returns a TreePath instance for this node.
|
|
*/
|
|
public TreePath getTreePath() {
|
|
return path;
|
|
}
|
|
|
|
//
|
|
// Local methods
|
|
//
|
|
|
|
/**
|
|
* Recreates the receivers path, and all its children's paths.
|
|
*/
|
|
protected void resetChildrenPaths(TreePath parentPath) {
|
|
removeMapping(this);
|
|
if(parentPath == null)
|
|
path = new TreePath(getUserObject());
|
|
else
|
|
path = parentPath.pathByAddingChild(getUserObject());
|
|
addMapping(this);
|
|
for(int counter = getChildCount() - 1; counter >= 0; counter--)
|
|
((TreeStateNode)getChildAt(counter)).resetChildrenPaths(path);
|
|
}
|
|
|
|
/**
|
|
* Sets y origin the user object will be drawn at to
|
|
* <I>newYOrigin</I>.
|
|
*/
|
|
protected void setYOrigin(int newYOrigin) {
|
|
yOrigin = newYOrigin;
|
|
}
|
|
|
|
/**
|
|
* Shifts the y origin by <code>offset</code>.
|
|
*/
|
|
protected void shiftYOriginBy(int offset) {
|
|
yOrigin += offset;
|
|
}
|
|
|
|
/**
|
|
* Updates the receivers preferredSize by invoking
|
|
* <code>updatePreferredSize</code> with an argument of -1.
|
|
*/
|
|
protected void updatePreferredSize() {
|
|
updatePreferredSize(getRow());
|
|
}
|
|
|
|
/**
|
|
* Updates the preferred size by asking the current renderer
|
|
* for the Dimension needed to draw the user object this
|
|
* instance represents.
|
|
*/
|
|
protected void updatePreferredSize(int index) {
|
|
Rectangle bounds = getNodeDimensions(this.getUserObject(),
|
|
index, getLevel(),
|
|
isExpanded(),
|
|
boundsBuffer);
|
|
|
|
if(bounds == null) {
|
|
xOrigin = 0;
|
|
preferredWidth = preferredHeight = 0;
|
|
updateNodeSizes = true;
|
|
}
|
|
else if(bounds.height == 0) {
|
|
xOrigin = 0;
|
|
preferredWidth = preferredHeight = 0;
|
|
updateNodeSizes = true;
|
|
}
|
|
else {
|
|
xOrigin = bounds.x;
|
|
preferredWidth = bounds.width;
|
|
if(isFixedRowHeight())
|
|
preferredHeight = getRowHeight();
|
|
else
|
|
preferredHeight = bounds.height;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Marks the receivers size as invalid. Next time the size, location
|
|
* is asked for it will be obtained.
|
|
*/
|
|
protected void markSizeInvalid() {
|
|
preferredHeight = 0;
|
|
}
|
|
|
|
/**
|
|
* Marks the receivers size, and all its descendants sizes, as invalid.
|
|
*/
|
|
protected void deepMarkSizeInvalid() {
|
|
markSizeInvalid();
|
|
for(int counter = getChildCount() - 1; counter >= 0; counter--)
|
|
((TreeStateNode)getChildAt(counter)).deepMarkSizeInvalid();
|
|
}
|
|
|
|
/**
|
|
* Returns the children of the receiver. If the children haven't
|
|
* been loaded from the model and
|
|
* <code>createIfNeeded</code> is true, the children are first
|
|
* loaded.
|
|
*/
|
|
protected Enumeration getLoadedChildren(boolean createIfNeeded) {
|
|
if(!createIfNeeded || hasBeenExpanded)
|
|
return super.children();
|
|
|
|
TreeStateNode newNode;
|
|
Object realNode = getValue();
|
|
TreeModel treeModel = getModel();
|
|
int count = treeModel.getChildCount(realNode);
|
|
|
|
hasBeenExpanded = true;
|
|
|
|
int childRow = getRow();
|
|
|
|
if(childRow == -1) {
|
|
for (int i = 0; i < count; i++) {
|
|
newNode = createNodeForValue
|
|
(treeModel.getChild(realNode, i));
|
|
this.add(newNode);
|
|
newNode.updatePreferredSize(-1);
|
|
}
|
|
}
|
|
else {
|
|
childRow++;
|
|
for (int i = 0; i < count; i++) {
|
|
newNode = createNodeForValue
|
|
(treeModel.getChild(realNode, i));
|
|
this.add(newNode);
|
|
newNode.updatePreferredSize(childRow++);
|
|
}
|
|
}
|
|
return super.children();
|
|
}
|
|
|
|
/**
|
|
* Messaged from expand and collapse. This is meant for subclassers
|
|
* that may wish to do something interesting with this.
|
|
*/
|
|
protected void didAdjustTree() {
|
|
}
|
|
|
|
/**
|
|
* Invokes <code>expandParentAndReceiver</code> on the parent,
|
|
* and expands the receiver.
|
|
*/
|
|
protected void expandParentAndReceiver() {
|
|
TreeStateNode parent = (TreeStateNode)getParent();
|
|
|
|
if(parent != null)
|
|
parent.expandParentAndReceiver();
|
|
expand();
|
|
}
|
|
|
|
/**
|
|
* Expands this node in the tree. This will load the children
|
|
* from the treeModel if this node has not previously been
|
|
* expanded. If <I>adjustTree</I> is true the tree and selection
|
|
* are updated accordingly.
|
|
*/
|
|
protected void expand(boolean adjustTree) {
|
|
if (!isExpanded() && !isLeaf()) {
|
|
boolean isFixed = isFixedRowHeight();
|
|
int startHeight = getPreferredHeight();
|
|
int originalRow = getRow();
|
|
|
|
expanded = true;
|
|
updatePreferredSize(originalRow);
|
|
|
|
if (!hasBeenExpanded) {
|
|
TreeStateNode newNode;
|
|
Object realNode = getValue();
|
|
TreeModel treeModel = getModel();
|
|
int count = treeModel.getChildCount(realNode);
|
|
|
|
hasBeenExpanded = true;
|
|
if(originalRow == -1) {
|
|
for (int i = 0; i < count; i++) {
|
|
newNode = createNodeForValue(treeModel.getChild
|
|
(realNode, i));
|
|
this.add(newNode);
|
|
newNode.updatePreferredSize(-1);
|
|
}
|
|
}
|
|
else {
|
|
int offset = originalRow + 1;
|
|
for (int i = 0; i < count; i++) {
|
|
newNode = createNodeForValue(treeModel.getChild
|
|
(realNode, i));
|
|
this.add(newNode);
|
|
newNode.updatePreferredSize(offset);
|
|
}
|
|
}
|
|
}
|
|
|
|
int i = originalRow;
|
|
Enumeration cursor = preorderEnumeration();
|
|
cursor.nextElement(); // don't add me, I'm already in
|
|
|
|
int newYOrigin;
|
|
|
|
if(isFixed)
|
|
newYOrigin = 0;
|
|
else if(this == root && !isRootVisible())
|
|
newYOrigin = 0;
|
|
else
|
|
newYOrigin = getYOrigin() + this.getPreferredHeight();
|
|
TreeStateNode aNode;
|
|
if(!isFixed) {
|
|
while (cursor.hasMoreElements()) {
|
|
aNode = (TreeStateNode)cursor.nextElement();
|
|
if(!updateNodeSizes && !aNode.hasValidSize())
|
|
aNode.updatePreferredSize(i + 1);
|
|
aNode.setYOrigin(newYOrigin);
|
|
newYOrigin += aNode.getPreferredHeight();
|
|
visibleNodes.insertElementAt(aNode, ++i);
|
|
}
|
|
}
|
|
else {
|
|
while (cursor.hasMoreElements()) {
|
|
aNode = (TreeStateNode)cursor.nextElement();
|
|
visibleNodes.insertElementAt(aNode, ++i);
|
|
}
|
|
}
|
|
|
|
if(adjustTree && (originalRow != i ||
|
|
getPreferredHeight() != startHeight)) {
|
|
// Adjust the Y origin of any nodes following this row.
|
|
if(!isFixed && ++i < getRowCount()) {
|
|
int counter;
|
|
int heightDiff = newYOrigin -
|
|
(getYOrigin() + getPreferredHeight()) +
|
|
(getPreferredHeight() - startHeight);
|
|
|
|
for(counter = visibleNodes.size() - 1;counter >= i;
|
|
counter--)
|
|
((TreeStateNode)visibleNodes.elementAt(counter)).
|
|
shiftYOriginBy(heightDiff);
|
|
}
|
|
didAdjustTree();
|
|
visibleNodesChanged();
|
|
}
|
|
|
|
// Update the rows in the selection
|
|
if(treeSelectionModel != null) {
|
|
treeSelectionModel.resetRowSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collapses this node in the tree. If <I>adjustTree</I> is
|
|
* true the tree and selection are updated accordingly.
|
|
*/
|
|
protected void collapse(boolean adjustTree) {
|
|
if (isExpanded()) {
|
|
Enumeration cursor = preorderEnumeration();
|
|
cursor.nextElement(); // don't remove me, I'm still visible
|
|
int rowsDeleted = 0;
|
|
boolean isFixed = isFixedRowHeight();
|
|
int lastYEnd;
|
|
if(isFixed)
|
|
lastYEnd = 0;
|
|
else
|
|
lastYEnd = getPreferredHeight() + getYOrigin();
|
|
int startHeight = getPreferredHeight();
|
|
int startYEnd = lastYEnd;
|
|
int myRow = getRow();
|
|
|
|
if(!isFixed) {
|
|
while(cursor.hasMoreElements()) {
|
|
TreeStateNode node = (TreeStateNode)cursor.
|
|
nextElement();
|
|
if (node.isVisible()) {
|
|
rowsDeleted++;
|
|
//visibleNodes.removeElement(node);
|
|
lastYEnd = node.getYOrigin() +
|
|
node.getPreferredHeight();
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
while(cursor.hasMoreElements()) {
|
|
TreeStateNode node = (TreeStateNode)cursor.
|
|
nextElement();
|
|
if (node.isVisible()) {
|
|
rowsDeleted++;
|
|
//visibleNodes.removeElement(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up the visible nodes.
|
|
for (int counter = rowsDeleted + myRow; counter > myRow;
|
|
counter--) {
|
|
visibleNodes.removeElementAt(counter);
|
|
}
|
|
|
|
expanded = false;
|
|
|
|
if(myRow == -1)
|
|
markSizeInvalid();
|
|
else if (adjustTree)
|
|
updatePreferredSize(myRow);
|
|
|
|
if(myRow != -1 && adjustTree &&
|
|
(rowsDeleted > 0 || startHeight != getPreferredHeight())) {
|
|
// Adjust the Y origin of any rows following this one.
|
|
startYEnd += (getPreferredHeight() - startHeight);
|
|
if(!isFixed && (myRow + 1) < getRowCount() &&
|
|
startYEnd != lastYEnd) {
|
|
int counter, maxCounter, shiftAmount;
|
|
|
|
shiftAmount = startYEnd - lastYEnd;
|
|
for(counter = myRow + 1, maxCounter =
|
|
visibleNodes.size();
|
|
counter < maxCounter;counter++)
|
|
((TreeStateNode)visibleNodes.elementAt(counter))
|
|
.shiftYOriginBy(shiftAmount);
|
|
}
|
|
didAdjustTree();
|
|
visibleNodesChanged();
|
|
}
|
|
if(treeSelectionModel != null && rowsDeleted > 0 &&
|
|
myRow != -1) {
|
|
treeSelectionModel.resetRowSelection();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the receiver, and all its children, from the mapping
|
|
* table.
|
|
*/
|
|
protected void removeFromMapping() {
|
|
if(path != null) {
|
|
removeMapping(this);
|
|
for(int counter = getChildCount() - 1; counter >= 0; counter--)
|
|
((TreeStateNode)getChildAt(counter)).removeFromMapping();
|
|
}
|
|
}
|
|
} // End of VariableHeightLayoutCache.TreeStateNode
|
|
|
|
|
|
/**
|
|
* An enumerator to iterate through visible nodes.
|
|
*/
|
|
private class VisibleTreeStateNodeEnumeration implements
|
|
Enumeration<TreePath> {
|
|
/** Parent thats children are being enumerated. */
|
|
protected TreeStateNode parent;
|
|
/** Index of next child. An index of -1 signifies parent should be
|
|
* visibled next. */
|
|
protected int nextIndex;
|
|
/** Number of children in parent. */
|
|
protected int childCount;
|
|
|
|
protected VisibleTreeStateNodeEnumeration(TreeStateNode node) {
|
|
this(node, -1);
|
|
}
|
|
|
|
protected VisibleTreeStateNodeEnumeration(TreeStateNode parent,
|
|
int startIndex) {
|
|
this.parent = parent;
|
|
this.nextIndex = startIndex;
|
|
this.childCount = this.parent.getChildCount();
|
|
}
|
|
|
|
/**
|
|
* @return true if more visible nodes.
|
|
*/
|
|
public boolean hasMoreElements() {
|
|
return (parent != null);
|
|
}
|
|
|
|
/**
|
|
* @return next visible TreePath.
|
|
*/
|
|
public TreePath nextElement() {
|
|
if(!hasMoreElements())
|
|
throw new NoSuchElementException("No more visible paths");
|
|
|
|
TreePath retObject;
|
|
|
|
if(nextIndex == -1) {
|
|
retObject = parent.getTreePath();
|
|
}
|
|
else {
|
|
TreeStateNode node = (TreeStateNode)parent.
|
|
getChildAt(nextIndex);
|
|
|
|
retObject = node.getTreePath();
|
|
}
|
|
updateNextObject();
|
|
return retObject;
|
|
}
|
|
|
|
/**
|
|
* Determines the next object by invoking <code>updateNextIndex</code>
|
|
* and if not succesful <code>findNextValidParent</code>.
|
|
*/
|
|
protected void updateNextObject() {
|
|
if(!updateNextIndex()) {
|
|
findNextValidParent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the next valid parent, this should be called when nextIndex
|
|
* is beyond the number of children of the current parent.
|
|
*/
|
|
protected boolean findNextValidParent() {
|
|
if(parent == root) {
|
|
// mark as invalid!
|
|
parent = null;
|
|
return false;
|
|
}
|
|
while(parent != null) {
|
|
TreeStateNode newParent = (TreeStateNode)parent.
|
|
getParent();
|
|
|
|
if(newParent != null) {
|
|
nextIndex = newParent.getIndex(parent);
|
|
parent = newParent;
|
|
childCount = parent.getChildCount();
|
|
if(updateNextIndex())
|
|
return true;
|
|
}
|
|
else
|
|
parent = null;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Updates <code>nextIndex</code> returning false if it is beyond
|
|
* the number of children of parent.
|
|
*/
|
|
protected boolean updateNextIndex() {
|
|
// nextIndex == -1 identifies receiver, make sure is expanded
|
|
// before descend.
|
|
if(nextIndex == -1 && !parent.isExpanded())
|
|
return false;
|
|
|
|
// Check that it can have kids
|
|
if(childCount == 0)
|
|
return false;
|
|
// Make sure next index not beyond child count.
|
|
else if(++nextIndex >= childCount)
|
|
return false;
|
|
|
|
TreeStateNode child = (TreeStateNode)parent.
|
|
getChildAt(nextIndex);
|
|
|
|
if(child != null && child.isExpanded()) {
|
|
parent = child;
|
|
nextIndex = -1;
|
|
childCount = child.getChildCount();
|
|
}
|
|
return true;
|
|
}
|
|
} // VariableHeightLayoutCache.VisibleTreeStateNodeEnumeration
|
|
}
|