/* * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package 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 sun.swing.SwingUtilities2; /** * NOTE: This will become more open in a future release. *

* Warning: * 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 java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @author Scott Violet */ @SuppressWarnings("serial") // Same-version serialization only public class FixedHeightLayoutCache extends AbstractLayoutCache { /** Root node. */ private FHTreeStateNode root; /** Number of rows currently visible. */ private int rowCount; /** * Used in getting sizes for nodes to avoid creating a new Rectangle * every time a size is needed. */ private Rectangle boundsBuffer; /** * Maps from TreePath to a FHTreeStateNode. */ private Hashtable treePathMapping; /** * Used for getting path/row information. */ private SearchInfo info; private Stack> tempStacks; /** * Constructs a {@code FixedHeightLayoutCache}. */ public FixedHeightLayoutCache() { super(); tempStacks = new Stack>(); boundsBuffer = new Rectangle(); treePathMapping = new Hashtable(); info = new SearchInfo(); setRowHeight(1); } /** * Sets the TreeModel that will provide the data. * * @param newModel the TreeModel that is to provide the data */ public void setModel(TreeModel newModel) { super.setModel(newModel); rebuild(false); } /** * Determines whether or not the root node from * the TreeModel is visible. * * @param rootVisible true if the root node of the tree is to be displayed * @see #rootVisible */ public void setRootVisible(boolean rootVisible) { if(isRootVisible() != rootVisible) { super.setRootVisible(rootVisible); if(root != null) { if(rootVisible) { rowCount++; root.adjustRowBy(1); } else { rowCount--; root.adjustRowBy(-1); } visibleNodesChanged(); } } } /** * Sets the height of each cell. If rowHeight is less than or equal to * 0 this will throw an IllegalArgumentException. * * @param rowHeight the height of each cell, in pixels */ public void setRowHeight(int rowHeight) { if(rowHeight <= 0) throw new IllegalArgumentException("FixedHeightLayoutCache only supports row heights greater than 0"); if(getRowHeight() != rowHeight) { super.setRowHeight(rowHeight); visibleNodesChanged(); } } /** * Returns the number of visible rows. */ public int getRowCount() { return rowCount; } /** * Does nothing, FixedHeightLayoutCache doesn't cache width, and that * is all that could change. */ public void invalidatePathBounds(TreePath path) { } /** * Informs the TreeState that it needs to recalculate all the sizes * it is referencing. */ public void invalidateSizes() { // Nothing to do here, rowHeight still same, which is all // this is interested in, visible region may have changed though. visibleNodesChanged(); } /** * Returns true if the value identified by row is currently expanded. */ public boolean isExpanded(TreePath path) { if(path != null) { FHTreeStateNode lastNode = getNodeForPath(path, true, false); return (lastNode != null && lastNode.isExpanded()); } return false; } /** * Returns a rectangle giving the bounds needed to draw path. * * @param path a TreePath specifying a node * @param placeIn a Rectangle object giving the available space * @return a Rectangle object specifying the space to be used */ public Rectangle getBounds(TreePath path, Rectangle placeIn) { if(path == null) return null; FHTreeStateNode node = getNodeForPath(path, true, false); if(node != null) return getBounds(node, -1, placeIn); // node hasn't been created yet. TreePath parentPath = path.getParentPath(); node = getNodeForPath(parentPath, true, false); if (node != null && node.isExpanded()) { int childIndex = treeModel.getIndexOfChild (parentPath.getLastPathComponent(), path.getLastPathComponent()); if(childIndex != -1) return getBounds(node, childIndex, placeIn); } return null; } /** * Returns the path for passed in row. If row is not visible * null is returned. */ public TreePath getPathForRow(int row) { if(row >= 0 && row < getRowCount()) { if(root.getPathForRow(row, getRowCount(), info)) { return info.getPath(); } } return null; } /** * Returns the row that the last item identified in path is visible * at. Will return -1 if any of the elements in path are not * currently visible. */ public int getRowForPath(TreePath path) { if(path == null || root == null) return -1; FHTreeStateNode node = getNodeForPath(path, true, false); if(node != null) return node.getRow(); TreePath parentPath = path.getParentPath(); node = getNodeForPath(parentPath, true, false); if(node != null && node.isExpanded()) { return node.getRowToModelIndex(treeModel.getIndexOfChild (parentPath.getLastPathComponent(), path.getLastPathComponent())); } return -1; } /** * Returns the path to the node that is closest to x,y. If * there is nothing currently visible this will return null, otherwise * it'll 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. */ public TreePath getPathClosestTo(int x, int y) { if(getRowCount() == 0) return null; int row = getRowContainingYLocation(y); return getPathForRow(row); } /** * Returns the number of visible children for row. */ public int getVisibleChildCount(TreePath path) { FHTreeStateNode node = getNodeForPath(path, true, false); if(node == null) return 0; return node.getTotalChildCount(); } /** * Returns an Enumerator 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. */ public Enumeration getVisiblePathsFrom(TreePath path) { if(path == null) return null; FHTreeStateNode node = getNodeForPath(path, true, false); if(node != null) { return new VisibleFHTreeStateNodeEnumeration(node); } TreePath parentPath = path.getParentPath(); node = getNodeForPath(parentPath, true, false); if(node != null && node.isExpanded()) { return new VisibleFHTreeStateNodeEnumeration(node, treeModel.getIndexOfChild(parentPath.getLastPathComponent(), path.getLastPathComponent())); } return null; } /** * Marks the path path expanded state to * isExpanded. */ public void setExpandedState(TreePath path, boolean isExpanded) { if(isExpanded) ensurePathIsExpanded(path, true); else if(path != null) { TreePath parentPath = path.getParentPath(); // YECK! Make the parent expanded. if(parentPath != null) { FHTreeStateNode parentNode = getNodeForPath(parentPath, false, true); if(parentNode != null) parentNode.makeVisible(); } // And collapse the child. FHTreeStateNode childNode = getNodeForPath(path, true, false); if(childNode != null) childNode.collapse(true); } } /** * Returns true if the path is expanded, and visible. */ public boolean getExpandedState(TreePath path) { FHTreeStateNode node = getNodeForPath(path, true, false); return (node != null) ? (node.isVisible() && node.isExpanded()) : 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.

* *

e.path() returns the path the parent of the changed node(s).

* *

e.childIndices() returns the index(es) of the changed node(s).

*/ public void treeNodesChanged(TreeModelEvent e) { if(e != null) { int changedIndexs[]; FHTreeStateNode changedParent = getNodeForPath (SwingUtilities2.getTreePath(e, getModel()), false, false); int maxCounter; changedIndexs = e.getChildIndices(); /* Only need to update the children if the node has been expanded once. */ // PENDING(scott): make sure childIndexs is sorted! if (changedParent != null) { if (changedIndexs != null && (maxCounter = changedIndexs.length) > 0) { Object parentValue = changedParent.getUserObject(); for(int counter = 0; counter < maxCounter; counter++) { FHTreeStateNode child = changedParent. getChildAtModelIndex(changedIndexs[counter]); if(child != null) { child.setUserObject(treeModel.getChild(parentValue, changedIndexs[counter])); } } if(changedParent.isVisible() && changedParent.isExpanded()) visibleNodesChanged(); } // Null for root indicates it changed. else if (changedParent == root && changedParent.isVisible() && changedParent.isExpanded()) { visibleNodesChanged(); } } } } /** *

Invoked after nodes have been inserted into the tree.

* *

e.path() returns the parent of the new nodes *

e.childIndices() returns the indices of the new nodes in * ascending order. */ public void treeNodesInserted(TreeModelEvent e) { if(e != null) { int changedIndexs[]; FHTreeStateNode changedParent = getNodeForPath (SwingUtilities2.getTreePath(e, getModel()), false, false); int maxCounter; changedIndexs = e.getChildIndices(); /* Only need to update the children if the node has been expanded once. */ // PENDING(scott): make sure childIndexs is sorted! if(changedParent != null && changedIndexs != null && (maxCounter = changedIndexs.length) > 0) { boolean isVisible = (changedParent.isVisible() && changedParent.isExpanded()); for(int counter = 0; counter < maxCounter; counter++) { changedParent.childInsertedAtModelIndex (changedIndexs[counter], isVisible); } if(isVisible && treeSelectionModel != null) treeSelectionModel.resetRowSelection(); if(changedParent.isVisible()) this.visibleNodesChanged(); } } } /** *

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.

* *

e.path() returns the former parent of the deleted nodes.

* *

e.childIndices() returns the indices the nodes had before they were deleted in ascending order.

*/ public void treeNodesRemoved(TreeModelEvent e) { if(e != null) { int changedIndexs[]; int maxCounter; TreePath parentPath = SwingUtilities2.getTreePath(e, getModel()); FHTreeStateNode changedParentNode = getNodeForPath (parentPath, false, false); changedIndexs = e.getChildIndices(); // PENDING(scott): make sure that changedIndexs are sorted in // ascending order. if(changedParentNode != null && changedIndexs != null && (maxCounter = changedIndexs.length) > 0) { Object[] children = e.getChildren(); boolean isVisible = (changedParentNode.isVisible() && changedParentNode.isExpanded()); for(int counter = maxCounter - 1; counter >= 0; counter--) { changedParentNode.removeChildAtModelIndex (changedIndexs[counter], isVisible); } if(isVisible) { if(treeSelectionModel != null) treeSelectionModel.resetRowSelection(); if (treeModel.getChildCount(changedParentNode. getUserObject()) == 0 && changedParentNode.isLeaf()) { // Node has become a leaf, collapse it. changedParentNode.collapse(false); } visibleNodesChanged(); } else if(changedParentNode.isVisible()) visibleNodesChanged(); } } } /** *

Invoked after the tree has drastically changed structure from a * given node down. If the path returned by e.getPath() 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. * *

e.path() holds the path to the node.

*

e.childIndices() returns null.

*/ public void treeStructureChanged(TreeModelEvent e) { if(e != null) { TreePath changedPath = SwingUtilities2.getTreePath(e, getModel()); FHTreeStateNode 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) { boolean wasExpanded, wasVisible; FHTreeStateNode parent = (FHTreeStateNode) changedNode.getParent(); wasExpanded = changedNode.isExpanded(); wasVisible = changedNode.isVisible(); int index = parent.getIndex(changedNode); changedNode.collapse(false); parent.remove(index); if(wasVisible && wasExpanded) { int row = changedNode.getRow(); parent.resetChildrenRowsFrom(row, index, changedNode.getChildIndex()); changedNode = getNodeForPath(changedPath, false, true); changedNode.expand(); } if(treeSelectionModel != null && wasVisible && wasExpanded) treeSelectionModel.resetRowSelection(); if(wasVisible) this.visibleNodesChanged(); } } } // // Local methods // private void visibleNodesChanged() { } /** * Returns the bounds for the given node. If childIndex * is -1, the bounds of parent are returned, otherwise * the bounds of the node at childIndex are returned. */ private Rectangle getBounds(FHTreeStateNode parent, int childIndex, Rectangle placeIn) { boolean expanded; int level; int row; Object value; if(childIndex == -1) { // Getting bounds for parent row = parent.getRow(); value = parent.getUserObject(); expanded = parent.isExpanded(); level = parent.getLevel(); } else { row = parent.getRowToModelIndex(childIndex); value = treeModel.getChild(parent.getUserObject(), childIndex); expanded = false; level = parent.getLevel() + 1; } Rectangle bounds = getNodeDimensions(value, row, level, expanded, boundsBuffer); // No node dimensions, bail. if(bounds == null) return null; if(placeIn == null) placeIn = new Rectangle(); placeIn.x = bounds.x; placeIn.height = getRowHeight(); placeIn.y = row * placeIn.height; placeIn.width = bounds.width; return placeIn; } /** * Adjust the large row count of the AbstractTreeUI the receiver was * created with. */ private void adjustRowCountBy(int changeAmount) { rowCount += changeAmount; } /** * Adds a mapping for node. */ private void addMapping(FHTreeStateNode node) { treePathMapping.put(node.getTreePath(), node); } /** * Removes the mapping for a previously added node. */ private void removeMapping(FHTreeStateNode node) { treePathMapping.remove(node.getTreePath()); } /** * Returns the node previously added for path. This may * return null, if you to create a node use getNodeForPath. */ private FHTreeStateNode getMapping(TreePath path) { return treePathMapping.get(path); } /** * Sent to completely rebuild the visible tree. All nodes are collapsed. */ private void rebuild(boolean clearSelection) { Object rootUO; treePathMapping.clear(); if(treeModel != null && (rootUO = treeModel.getRoot()) != null) { root = createNodeForValue(rootUO, 0); root.path = new TreePath(rootUO); addMapping(root); if(isRootVisible()) { rowCount = 1; root.row = 0; } else { rowCount = 0; root.row = -1; } root.expand(); } else { root = null; rowCount = 0; } if(clearSelection && treeSelectionModel != null) { treeSelectionModel.clearSelection(); } this.visibleNodesChanged(); } /** * 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(getRowCount() == 0) return -1; return Math.max(0, Math.min(getRowCount() - 1, location / getRowHeight())); } /** * 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 boolean 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) { FHTreeStateNode lastNode = getNodeForPath(aPath, false, true); if(lastNode != null) { lastNode.makeVisible(); if(expandLast) lastNode.expand(); return true; } } } return false; } /** * Creates and returns an instance of FHTreeStateNode. */ private FHTreeStateNode createNodeForValue(Object value,int childIndex) { return new FHTreeStateNode(value, childIndex, -1); } /** * Messages getTreeNodeForPage(path, onlyIfVisible, shouldCreate, * path.length) as long as path is non-null and the length is {@literal >} 0. * Otherwise returns null. */ private FHTreeStateNode getNodeForPath(TreePath path, boolean onlyIfVisible, boolean shouldCreate) { if(path != null) { FHTreeStateNode node; node = getMapping(path); if(node != null) { if(onlyIfVisible && !node.isVisible()) return null; return node; } if(onlyIfVisible) return null; // Check all the parent paths, until a match is found. Stack paths; if(tempStacks.size() == 0) { paths = new Stack(); } 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 = node.createChildFor(path. getLastPathComponent()); } 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! return null; } return null; } /** * FHTreeStateNode is used to track what has been expanded. * FHTreeStateNode differs from VariableHeightTreeState.TreeStateNode * in that it is highly model intensive. That is almost all queries to a * FHTreeStateNode result in the TreeModel being queried. And it * obviously does not support variable sized row heights. */ private class FHTreeStateNode extends DefaultMutableTreeNode { /** Is this node expanded? */ protected boolean isExpanded; /** Index of this node from the model. */ protected int childIndex; /** Child count of the receiver. */ protected int childCount; /** Row of the receiver. This is only valid if the row is expanded. */ protected int row; /** Path of this node. */ protected TreePath path; public FHTreeStateNode(Object userObject, int childIndex, int row) { super(userObject); this.childIndex = childIndex; this.row = row; } // // 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 = ((FHTreeStateNode)parent).getTreePath(). pathByAddingChild(getUserObject()); addMapping(this); } } /** * Messaged when this node is removed from its parent, this messages * removedFromMapping to remove all the children. */ public void remove(int childIndex) { FHTreeStateNode node = (FHTreeStateNode)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) { FHTreeStateNode parent = (FHTreeStateNode)getParent(); if(parent != null) resetChildrenPaths(parent.getTreePath()); else resetChildrenPaths(null); } } // // /** * Returns the index of the receiver in the model. */ public int getChildIndex() { return childIndex; } /** * Returns the TreePath of the receiver. */ public TreePath getTreePath() { return path; } /** * Returns the child for the passed in model index, this will * return null if the child for index * has not yet been created (expanded). */ public FHTreeStateNode getChildAtModelIndex(int index) { // PENDING: Make this a binary search! for(int counter = getChildCount() - 1; counter >= 0; counter--) if(((FHTreeStateNode)getChildAt(counter)).childIndex == index) return (FHTreeStateNode)getChildAt(counter); return null; } /** * Returns true if this node is visible. This is determined by * asking all the parents if they are expanded. */ public boolean isVisible() { FHTreeStateNode parent = (FHTreeStateNode)getParent(); if(parent == null) return true; return (parent.isExpanded() && parent.isVisible()); } /** * Returns the row of the receiver. */ public int getRow() { return row; } /** * Returns the row of the child with a model index of * index. */ public int getRowToModelIndex(int index) { FHTreeStateNode child; int lastRow = getRow() + 1; int retValue = lastRow; // This too could be a binary search! for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { child = (FHTreeStateNode)getChildAt(counter); if(child.childIndex >= index) { if(child.childIndex == index) return child.row; if(counter == 0) return getRow() + 1 + index; return child.row - (child.childIndex - index); } } // YECK! return getRow() + 1 + getTotalChildCount() - (childCount - index); } /** * Returns the number of children in the receiver by descending all * expanded nodes and messaging them with getTotalChildCount. */ public int getTotalChildCount() { if(isExpanded()) { FHTreeStateNode parent = (FHTreeStateNode)getParent(); int pIndex; if(parent != null && (pIndex = parent.getIndex(this)) + 1 < parent.getChildCount()) { // This node has a created sibling, to calc total // child count directly from that! FHTreeStateNode nextSibling = (FHTreeStateNode)parent. getChildAt(pIndex + 1); return nextSibling.row - row - (nextSibling.childIndex - childIndex); } else { int retCount = childCount; for(int counter = getChildCount() - 1; counter >= 0; counter--) { retCount += ((FHTreeStateNode)getChildAt(counter)) .getTotalChildCount(); } return retCount; } } return 0; } /** * Returns true if this node is expanded. */ public boolean isExpanded() { return isExpanded; } /** * The highest visible nodes have a depth of 0. */ public int getVisibleLevel() { if (isRootVisible()) { return getLevel(); } else { return getLevel() - 1; } } /** * 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--) ((FHTreeStateNode)getChildAt(counter)). resetChildrenPaths(path); } /** * 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--) ((FHTreeStateNode)getChildAt(counter)).removeFromMapping(); } } /** * Creates a new node to represent userObject. * This does NOT check to ensure there isn't already a child node * to manage userObject. */ protected FHTreeStateNode createChildFor(Object userObject) { int newChildIndex = treeModel.getIndexOfChild (getUserObject(), userObject); if(newChildIndex < 0) return null; FHTreeStateNode aNode; FHTreeStateNode child = createNodeForValue(userObject, newChildIndex); int childRow; if(isVisible()) { childRow = getRowToModelIndex(newChildIndex); } else { childRow = -1; } child.row = childRow; for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { aNode = (FHTreeStateNode)getChildAt(counter); if(aNode.childIndex > newChildIndex) { insert(child, counter); return child; } } add(child); return child; } /** * Adjusts the receiver, and all its children rows by * amount. */ protected void adjustRowBy(int amount) { row += amount; if(isExpanded) { for(int counter = getChildCount() - 1; counter >= 0; counter--) ((FHTreeStateNode)getChildAt(counter)).adjustRowBy(amount); } } /** * Adjusts this node, its child, and its parent starting at * an index of index index is the index of the child * to start adjusting from, which is not necessarily the model * index. */ protected void adjustRowBy(int amount, int startIndex) { // Could check isVisible, but probably isn't worth it. if(isExpanded) { // children following startIndex. for(int counter = getChildCount() - 1; counter >= startIndex; counter--) ((FHTreeStateNode)getChildAt(counter)).adjustRowBy(amount); } // Parent FHTreeStateNode parent = (FHTreeStateNode)getParent(); if(parent != null) { parent.adjustRowBy(amount, parent.getIndex(this) + 1); } } /** * Messaged when the node has expanded. This updates all of * the receivers children rows, as well as the total row count. */ protected void didExpand() { int nextRow = setRowAndChildren(row); FHTreeStateNode parent = (FHTreeStateNode)getParent(); int childRowCount = nextRow - row - 1; if(parent != null) { parent.adjustRowBy(childRowCount, parent.getIndex(this) + 1); } adjustRowCountBy(childRowCount); } /** * Sets the receivers row to nextRow and recursively * updates all the children of the receivers rows. The index the * next row is to be placed as is returned. */ protected int setRowAndChildren(int nextRow) { row = nextRow; if(!isExpanded()) return row + 1; int lastRow = row + 1; int lastModelIndex = 0; FHTreeStateNode child; int maxCounter = getChildCount(); for(int counter = 0; counter < maxCounter; counter++) { child = (FHTreeStateNode)getChildAt(counter); lastRow += (child.childIndex - lastModelIndex); lastModelIndex = child.childIndex + 1; if(child.isExpanded) { lastRow = child.setRowAndChildren(lastRow); } else { child.row = lastRow++; } } return lastRow + childCount - lastModelIndex; } /** * Resets the receivers children's rows. Starting with the child * at childIndex (and modelIndex) to * newRow. This uses setRowAndChildren * to recursively descend children, and uses * resetRowSelection to ascend parents. */ // This can be rather expensive, but is needed for the collapse // case this is resulting from a remove (although I could fix // that by having instances of FHTreeStateNode hold a ref to // the number of children). I prefer this though, making determing // the row of a particular node fast is very nice! protected void resetChildrenRowsFrom(int newRow, int childIndex, int modelIndex) { int lastRow = newRow; int lastModelIndex = modelIndex; FHTreeStateNode node; int maxCounter = getChildCount(); for(int counter = childIndex; counter < maxCounter; counter++) { node = (FHTreeStateNode)getChildAt(counter); lastRow += (node.childIndex - lastModelIndex); lastModelIndex = node.childIndex + 1; if(node.isExpanded) { lastRow = node.setRowAndChildren(lastRow); } else { node.row = lastRow++; } } lastRow += childCount - lastModelIndex; node = (FHTreeStateNode)getParent(); if(node != null) { node.resetChildrenRowsFrom(lastRow, node.getIndex(this) + 1, this.childIndex + 1); } else { // This is the root, reset total ROWCOUNT! rowCount = lastRow; } } /** * Makes the receiver visible, but invoking * expandParentAndReceiver on the superclass. */ protected void makeVisible() { FHTreeStateNode parent = (FHTreeStateNode)getParent(); if(parent != null) parent.expandParentAndReceiver(); } /** * Invokes expandParentAndReceiver on the parent, * and expands the receiver. */ protected void expandParentAndReceiver() { FHTreeStateNode parent = (FHTreeStateNode)getParent(); if(parent != null) parent.expandParentAndReceiver(); expand(); } /** * Expands the receiver. */ protected void expand() { if(!isExpanded && !isLeaf()) { boolean visible = isVisible(); isExpanded = true; childCount = treeModel.getChildCount(getUserObject()); if(visible) { didExpand(); } // Update the selection model. if(visible && treeSelectionModel != null) { treeSelectionModel.resetRowSelection(); } } } /** * Collapses the receiver. If adjustRows is true, * the rows of nodes after the receiver are adjusted. */ protected void collapse(boolean adjustRows) { if(isExpanded) { if(isVisible() && adjustRows) { int childCount = getTotalChildCount(); isExpanded = false; adjustRowCountBy(-childCount); // We can do this because adjustRowBy won't descend // the children. adjustRowBy(-childCount, 0); } else isExpanded = false; if(adjustRows && isVisible() && treeSelectionModel != null) treeSelectionModel.resetRowSelection(); } } /** * Returns true if the receiver is a leaf. */ public boolean isLeaf() { TreeModel model = getModel(); return (model != null) ? model.isLeaf(this.getUserObject()) : true; } /** * Adds newChild to this nodes children at the appropriate location. * The location is determined from the childIndex of newChild. */ protected void addNode(FHTreeStateNode newChild) { boolean added = false; int childIndex = newChild.getChildIndex(); for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { if(((FHTreeStateNode)getChildAt(counter)).getChildIndex() > childIndex) { added = true; insert(newChild, counter); counter = maxCounter; } } if(!added) add(newChild); } /** * Removes the child at modelIndex. * isChildVisible should be true if the receiver * is visible and expanded. */ protected void removeChildAtModelIndex(int modelIndex, boolean isChildVisible) { FHTreeStateNode childNode = getChildAtModelIndex(modelIndex); if(childNode != null) { int row = childNode.getRow(); int index = getIndex(childNode); childNode.collapse(false); remove(index); adjustChildIndexs(index, -1); childCount--; if(isChildVisible) { // Adjust the rows. resetChildrenRowsFrom(row, index, modelIndex); } } else { int maxCounter = getChildCount(); FHTreeStateNode aChild; for(int counter = 0; counter < maxCounter; counter++) { aChild = (FHTreeStateNode)getChildAt(counter); if(aChild.childIndex >= modelIndex) { if(isChildVisible) { adjustRowBy(-1, counter); adjustRowCountBy(-1); } // Since matched and children are always sorted by // index, no need to continue testing with the // above. for(; counter < maxCounter; counter++) ((FHTreeStateNode)getChildAt(counter)). childIndex--; childCount--; return; } } // No children to adjust, but it was a child, so we still need // to adjust nodes after this one. if(isChildVisible) { adjustRowBy(-1, maxCounter); adjustRowCountBy(-1); } childCount--; } } /** * Adjusts the child indexs of the receivers children by * amount, starting at index. */ protected void adjustChildIndexs(int index, int amount) { for(int counter = index, maxCounter = getChildCount(); counter < maxCounter; counter++) { ((FHTreeStateNode)getChildAt(counter)).childIndex += amount; } } /** * Messaged when a child has been inserted at index. For all the * children that have a childIndex ≥ index their index is incremented * by one. */ protected void childInsertedAtModelIndex(int index, boolean isExpandedAndVisible) { FHTreeStateNode aChild; int maxCounter = getChildCount(); for(int counter = 0; counter < maxCounter; counter++) { aChild = (FHTreeStateNode)getChildAt(counter); if(aChild.childIndex >= index) { if(isExpandedAndVisible) { adjustRowBy(1, counter); adjustRowCountBy(1); } /* Since matched and children are always sorted by index, no need to continue testing with the above. */ for(; counter < maxCounter; counter++) ((FHTreeStateNode)getChildAt(counter)).childIndex++; childCount++; return; } } // No children to adjust, but it was a child, so we still need // to adjust nodes after this one. if(isExpandedAndVisible) { adjustRowBy(1, maxCounter); adjustRowCountBy(1); } childCount++; } /** * Returns true if there is a row for row. * nextRow gives the bounds of the receiver. * Information about the found row is returned in info. * This should be invoked on root with nextRow set * to getRowCount(). */ protected boolean getPathForRow(int row, int nextRow, SearchInfo info) { if(this.row == row) { info.node = this; info.isNodeParentNode = false; info.childIndex = childIndex; return true; } FHTreeStateNode child; FHTreeStateNode lastChild = null; for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { child = (FHTreeStateNode)getChildAt(counter); if(child.row > row) { if(counter == 0) { // No node exists for it, and is first. info.node = this; info.isNodeParentNode = true; info.childIndex = row - this.row - 1; return true; } else { // May have been in last child's bounds. int lastChildEndRow = 1 + child.row - (child.childIndex - lastChild.childIndex); if(row < lastChildEndRow) { return lastChild.getPathForRow(row, lastChildEndRow, info); } // Between last child and child, but not in last child info.node = this; info.isNodeParentNode = true; info.childIndex = row - lastChildEndRow + lastChild.childIndex + 1; return true; } } lastChild = child; } // Not in children, but we should have it, offset from // nextRow. if(lastChild != null) { int lastChildEndRow = nextRow - (childCount - lastChild.childIndex) + 1; if(row < lastChildEndRow) { return lastChild.getPathForRow(row, lastChildEndRow, info); } // Between last child and child, but not in last child info.node = this; info.isNodeParentNode = true; info.childIndex = row - lastChildEndRow + lastChild.childIndex + 1; return true; } else { // No children. int retChildIndex = row - this.row - 1; if(retChildIndex >= childCount) { return false; } info.node = this; info.isNodeParentNode = true; info.childIndex = retChildIndex; return true; } } /** * Asks all the children of the receiver for their totalChildCount * and returns this value (plus stopIndex). */ protected int getCountTo(int stopIndex) { FHTreeStateNode aChild; int retCount = stopIndex + 1; for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { aChild = (FHTreeStateNode)getChildAt(counter); if(aChild.childIndex >= stopIndex) counter = maxCounter; else retCount += aChild.getTotalChildCount(); } if(parent != null) return retCount + ((FHTreeStateNode)getParent()) .getCountTo(childIndex); if(!isRootVisible()) return (retCount - 1); return retCount; } /** * Returns the number of children that are expanded to * stopIndex. This does not include the number * of children that the child at stopIndex might * have. */ protected int getNumExpandedChildrenTo(int stopIndex) { FHTreeStateNode aChild; int retCount = stopIndex; for(int counter = 0, maxCounter = getChildCount(); counter < maxCounter; counter++) { aChild = (FHTreeStateNode)getChildAt(counter); if(aChild.childIndex >= stopIndex) return retCount; else { retCount += aChild.getTotalChildCount(); } } return retCount; } /** * Messaged when this node either expands or collapses. */ protected void didAdjustTree() { } } // FixedHeightLayoutCache.FHTreeStateNode /** * Used as a placeholder when getting the path in FHTreeStateNodes. */ private class SearchInfo { protected FHTreeStateNode node; protected boolean isNodeParentNode; protected int childIndex; protected TreePath getPath() { if(node == null) return null; if(isNodeParentNode) return node.getTreePath().pathByAddingChild(treeModel.getChild (node.getUserObject(), childIndex)); return node.path; } } // FixedHeightLayoutCache.SearchInfo /** * An enumerator to iterate through visible nodes. */ // This is very similar to // VariableHeightTreeState.VisibleTreeStateNodeEnumeration private class VisibleFHTreeStateNodeEnumeration implements Enumeration { /** Parent thats children are being enumerated. */ protected FHTreeStateNode 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 VisibleFHTreeStateNodeEnumeration(FHTreeStateNode node) { this(node, -1); } protected VisibleFHTreeStateNodeEnumeration(FHTreeStateNode parent, int startIndex) { this.parent = parent; this.nextIndex = startIndex; this.childCount = treeModel.getChildCount(this.parent. getUserObject()); } /** * @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 { FHTreeStateNode node = parent.getChildAtModelIndex(nextIndex); if(node == null) retObject = parent.getTreePath().pathByAddingChild (treeModel.getChild(parent.getUserObject(), nextIndex)); else retObject = node.getTreePath(); } updateNextObject(); return retObject; } /** * Determines the next object by invoking updateNextIndex * and if not succesful findNextValidParent. */ 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) { FHTreeStateNode newParent = (FHTreeStateNode)parent. getParent(); if(newParent != null) { nextIndex = parent.childIndex; parent = newParent; childCount = treeModel.getChildCount (parent.getUserObject()); if(updateNextIndex()) return true; } else parent = null; } return false; } /** * Updates nextIndex 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; } FHTreeStateNode child = parent.getChildAtModelIndex(nextIndex); if(child != null && child.isExpanded()) { parent = child; nextIndex = -1; childCount = treeModel.getChildCount(child.getUserObject()); } return true; } } // FixedHeightLayoutCache.VisibleFHTreeStateNodeEnumeration }