| /* FixedHeightLayoutCache.java -- Fixed cell height tree layout cache |
| Copyright (C) 2002, 2004, 2006, Free Software Foundation, Inc. |
| |
| This file is part of GNU Classpath. |
| |
| GNU Classpath is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License as published by |
| the Free Software Foundation; either version 2, or (at your option) |
| any later version. |
| |
| GNU Classpath 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 for more details. |
| |
| You should have received a copy of the GNU General Public License |
| along with GNU Classpath; see the file COPYING. If not, write to the |
| Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
| 02110-1301 USA. |
| |
| Linking this library statically or dynamically with other modules is |
| making a combined work based on this library. Thus, the terms and |
| conditions of the GNU General Public License cover the whole |
| combination. |
| |
| As a special exception, the copyright holders of this library give you |
| permission to link this library with independent modules to produce an |
| executable, regardless of the license terms of these independent |
| modules, and to copy and distribute the resulting executable under |
| terms of your choice, provided that you also meet, for each linked |
| independent module, the terms and conditions of the license of that |
| module. An independent module is a module which is not derived from |
| or based on this library. If you modify this library, you may extend |
| this exception to your version of the library, but you are not |
| obligated to do so. If you do not wish to do so, delete this |
| exception statement from your version. */ |
| |
| package javax.swing.tree; |
| |
| import gnu.javax.swing.tree.GnuPath; |
| |
| import java.awt.Rectangle; |
| import java.util.Enumeration; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.LinkedList; |
| import java.util.Set; |
| import java.util.Vector; |
| |
| import javax.swing.event.TreeModelEvent; |
| |
| |
| /** |
| * The fixed height tree layout. This class assumes that all cells in the tree |
| * have the same fixed height. This may be not the case, for instance, if leaves |
| * and branches have different height, of if the tree rows may have arbitrary |
| * variable height. This class will also work if the NodeDimensions are not |
| * set. |
| * |
| * @author Audrius Meskauskas |
| * @author Andrew Selkirk |
| */ |
| public class FixedHeightLayoutCache |
| extends VariableHeightLayoutCache |
| { |
| /** |
| * The cached node record. |
| */ |
| class NodeRecord |
| { |
| NodeRecord(int aRow, int aDepth, Object aNode, Object aParent) |
| { |
| row = aRow; |
| depth = aDepth; |
| parent = aParent; |
| node = aNode; |
| |
| isExpanded = expanded.contains(aNode); |
| } |
| |
| /** |
| * The row, where the tree node is displayed. |
| */ |
| final int row; |
| |
| /** |
| * The nesting depth |
| */ |
| final int depth; |
| |
| /** |
| * The parent of the given node, null for the root node. |
| */ |
| final Object parent; |
| |
| /** |
| * This node. |
| */ |
| final Object node; |
| |
| /** |
| * True for the expanded nodes. The value is calculated in constructor. |
| * Using this field saves one hashtable access operation. |
| */ |
| final boolean isExpanded; |
| |
| /** |
| * The cached bounds of the tree row. |
| */ |
| Rectangle bounds; |
| |
| /** |
| * The path from the tree top to the given node (computed under first |
| * demand) |
| */ |
| private TreePath path; |
| |
| /** |
| * Get the path for this node. The derived class is returned, |
| * making check for the last child of some parent easier. |
| */ |
| TreePath getPath() |
| { |
| if (path == null) |
| { |
| boolean lastChild = false; |
| if (parent != null) |
| { |
| int nc = treeModel.getChildCount(parent); |
| if (nc > 0) |
| { |
| int n = treeModel.getIndexOfChild(parent, node); |
| if (n == nc - 1) |
| lastChild = true; |
| } |
| } |
| |
| LinkedList lpath = new LinkedList(); |
| NodeRecord rp = this; |
| while (rp != null) |
| { |
| lpath.addFirst(rp.node); |
| if (rp.parent != null) |
| { |
| Object parent = rp.parent; |
| rp = (NodeRecord) nodes.get(parent); |
| // Add the root node, even if it is not visible. |
| if (rp == null) |
| lpath.addFirst(parent); |
| } |
| else |
| rp = null; |
| } |
| path = new GnuPath(lpath.toArray(), lastChild); |
| } |
| return path; |
| } |
| |
| /** |
| * Get the rectangle bounds (compute, if required). |
| */ |
| Rectangle getBounds() |
| { |
| // This method may be called in the context when the tree rectangle is |
| // not known. To work around this, it is assumed near infinitely large. |
| if (bounds == null) |
| bounds = getNodeDimensions(node, row, depth, isExpanded, |
| new Rectangle()); |
| return bounds; |
| } |
| } |
| |
| /** |
| * The set of all expanded tree nodes. |
| */ |
| Set expanded = new HashSet(); |
| |
| /** |
| * Maps nodes to the row numbers. |
| */ |
| Hashtable nodes = new Hashtable(); |
| |
| /** |
| * Maps row numbers to nodes. |
| */ |
| Hashtable row2node = new Hashtable(); |
| |
| /** |
| * If true, the row map must be recomputed before using. |
| */ |
| boolean dirty; |
| |
| /** |
| * The cumulative height of all rows. |
| */ |
| int totalHeight; |
| |
| /** |
| * The maximal width. |
| */ |
| int maximalWidth; |
| |
| /** |
| * Creates the unitialised instance. Before using the class, the row height |
| * must be set with the {@link #setRowHeight(int)} and the model must be set |
| * with {@link #setModel(TreeModel)}. The node dimensions may not be set. |
| */ |
| public FixedHeightLayoutCache() |
| { |
| // Nothing to do here. |
| } |
| |
| /** |
| * Get the total number of rows in the tree. Every displayed node occupies the |
| * single row. The root node row is included if the root node is set as |
| * visible (false by default). |
| * |
| * @return int the number of the displayed rows. |
| */ |
| public int getRowCount() |
| { |
| if (dirty) update(); |
| return row2node.size(); |
| } |
| |
| /** |
| * Refresh the row map. |
| */ |
| private final void update() |
| { |
| nodes.clear(); |
| row2node.clear(); |
| |
| totalHeight = maximalWidth = 0; |
| |
| Object root = treeModel.getRoot(); |
| |
| if (rootVisible) |
| { |
| countRows(root, null, 0); |
| } |
| else |
| { |
| int sc = treeModel.getChildCount(root); |
| for (int i = 0; i < sc; i++) |
| { |
| Object child = treeModel.getChild(root, i); |
| countRows(child, root, 0); |
| } |
| } |
| dirty = false; |
| } |
| |
| /** |
| * Recursively counts all rows in the tree. |
| */ |
| private final void countRows(Object node, Object parent, int depth) |
| { |
| Integer n = new Integer(row2node.size()); |
| row2node.put(n, node); |
| |
| NodeRecord nr = new NodeRecord(n.intValue(), depth, node, parent); |
| nodes.put(node, nr); |
| |
| // For expanded nodes and for the root node. |
| if (expanded.contains(node)) |
| { |
| int sc = treeModel.getChildCount(node); |
| int deeper = depth + 1; |
| for (int i = 0; i < sc; i++) |
| { |
| Object child = treeModel.getChild(node, i); |
| countRows(child, node, deeper); |
| } |
| } |
| } |
| |
| /** |
| * Discard the bound information for the given path. |
| * |
| * @param path the path, for that the bound information must be recomputed. |
| */ |
| public void invalidatePathBounds(TreePath path) |
| { |
| NodeRecord r = (NodeRecord) nodes.get(path.getLastPathComponent()); |
| if (r != null) |
| r.bounds = null; |
| } |
| |
| /** |
| * Mark all cached information as invalid. |
| */ |
| public void invalidateSizes() |
| { |
| dirty = true; |
| } |
| |
| /** |
| * Set the expanded state of the given path. The expansion states must be |
| * always updated when expanding and colapsing the tree nodes. Otherwise |
| * other methods will not work correctly after the nodes are collapsed or |
| * expanded. |
| * |
| * @param path the tree path, for that the state is being set. |
| * @param isExpanded the expanded state of the given path. |
| */ |
| public void setExpandedState(TreePath path, boolean isExpanded) |
| { |
| if (isExpanded) |
| expanded.add(path.getLastPathComponent()); |
| else |
| expanded.remove(path.getLastPathComponent()); |
| |
| dirty = true; |
| } |
| |
| /** |
| * Get the expanded state for the given tree path. |
| * |
| * @return true if the given path is expanded, false otherwise. |
| */ |
| public boolean isExpanded(TreePath path) |
| { |
| return expanded.contains(path.getLastPathComponent()); |
| } |
| |
| /** |
| * Get bounds for the given tree path. |
| * |
| * @param path the tree path |
| * @param rect the rectangle that will be reused to return the result. |
| * @return Rectangle the bounds of the last line, defined by the given path. |
| */ |
| public Rectangle getBounds(TreePath path, Rectangle rect) |
| { |
| if (path == null) |
| return null; |
| if (dirty) |
| update(); |
| Object last = path.getLastPathComponent(); |
| NodeRecord r = (NodeRecord) nodes.get(last); |
| if (r == null) |
| // This node is not visible. |
| { |
| rect.x = rect.y = rect.width = rect.height = 0; |
| } |
| else |
| { |
| if (r.bounds == null) |
| { |
| Rectangle dim = getNodeDimensions(last, r.row, r.depth, |
| r.isExpanded, rect); |
| r.bounds = dim; |
| } |
| |
| rect.setRect(r.bounds); |
| } |
| return rect; |
| } |
| |
| /** |
| * Get the path, the last element of that is displayed in the given row. |
| * |
| * @param row the row |
| * @return TreePath the path |
| */ |
| public TreePath getPathForRow(int row) |
| { |
| if (dirty) |
| update(); |
| Object last = row2node.get(new Integer(row)); |
| if (last == null) |
| return null; |
| else |
| { |
| NodeRecord r = (NodeRecord) nodes.get(last); |
| return r.getPath(); |
| } |
| } |
| |
| /** |
| * Get the row, displaying the last node of the given path. |
| * |
| * @param path the path |
| * @return int the row number or -1 if the end of the path is not visible. |
| */ |
| public int getRowForPath(TreePath path) |
| { |
| if (path == null) |
| return -1; |
| |
| if (dirty) update(); |
| |
| NodeRecord r = (NodeRecord) nodes.get(path.getLastPathComponent()); |
| if (r == null) |
| return - 1; |
| else |
| return r.row; |
| } |
| |
| /** |
| * Get the path, closest to the given point. |
| * |
| * @param x the point x coordinate |
| * @param y the point y coordinate |
| * @return the tree path, closest to the the given point |
| */ |
| public TreePath getPathClosestTo(int x, int y) |
| { |
| if (dirty) |
| update(); |
| |
| // As the rows have arbitrary height, we need to iterate. |
| NodeRecord best = null; |
| NodeRecord r; |
| Enumeration en = nodes.elements(); |
| |
| int dist = Integer.MAX_VALUE; |
| |
| while (en.hasMoreElements() && dist > 0) |
| { |
| r = (NodeRecord) en.nextElement(); |
| if (best == null) |
| { |
| best = r; |
| dist = distance(r.getBounds(), x, y); |
| } |
| else |
| { |
| int rr = distance(r.getBounds(), x, y); |
| if (rr < dist) |
| { |
| best = r; |
| dist = rr; |
| } |
| } |
| } |
| |
| if (best == null) |
| return null; |
| else |
| return best.getPath(); |
| } |
| |
| /** |
| * Get the closest distance from this point till the given rectangle. Only |
| * vertical distance is taken into consideration. |
| */ |
| int distance(Rectangle r, int x, int y) |
| { |
| if (y < r.y) |
| return r.y - y; |
| else if (y > r.y + r.height) |
| return y - (r.y + r.height); |
| else |
| return 0; |
| } |
| |
| /** |
| * Get the number of the visible childs for the given tree path. If the node |
| * is not expanded, 0 is returned. Otherwise, the number of children is |
| * obtained from the model as the number of children for the last path |
| * component. |
| * |
| * @param path the tree path |
| * @return int the number of the visible childs (for row). |
| */ |
| public int getVisibleChildCount(TreePath path) |
| { |
| if (isExpanded(path)) |
| return 0; |
| else |
| return treeModel.getChildCount(path.getLastPathComponent()); |
| } |
| |
| /** |
| * Get the enumeration over all visible pathes that start from the given |
| * parent path. |
| * |
| * @param parentPath the parent path |
| * @return the enumeration over pathes |
| */ |
| public Enumeration getVisiblePathsFrom(TreePath parentPath) |
| { |
| if (dirty) |
| update(); |
| Vector p = new Vector(parentPath.getPathCount()); |
| Object node; |
| NodeRecord nr; |
| |
| for (int i = 0; i < parentPath.getPathCount(); i++) |
| { |
| node = parentPath.getPathComponent(i); |
| nr = (NodeRecord) nodes.get(node); |
| if (nr.row >= 0) |
| p.add(node); |
| } |
| return p.elements(); |
| } |
| |
| /** |
| * Return the expansion state of the given tree path. The expansion state |
| * must be previously set with the |
| * {@link #setExpandedState(TreePath, boolean)} |
| * |
| * @param path the path being checked |
| * @return true if the last node of the path is expanded, false otherwise. |
| */ |
| public boolean getExpandedState(TreePath path) |
| { |
| return expanded.contains(path.getLastPathComponent()); |
| } |
| |
| /** |
| * The listener method, called when the tree nodes are changed. |
| * |
| * @param event the change event |
| */ |
| public void treeNodesChanged(TreeModelEvent event) |
| { |
| dirty = true; |
| } |
| |
| /** |
| * The listener method, called when the tree nodes are inserted. |
| * |
| * @param event the change event |
| */ |
| public void treeNodesInserted(TreeModelEvent event) |
| { |
| dirty = true; |
| } |
| |
| /** |
| * The listener method, called when the tree nodes are removed. |
| * |
| * @param event the change event |
| */ |
| public void treeNodesRemoved(TreeModelEvent event) |
| { |
| dirty = true; |
| } |
| |
| /** |
| * Called when the tree structure has been changed. |
| * |
| * @param event the change event |
| */ |
| public void treeStructureChanged(TreeModelEvent event) |
| { |
| dirty = true; |
| } |
| |
| /** |
| * Set the tree model that will provide the data. |
| */ |
| public void setModel(TreeModel newModel) |
| { |
| treeModel = newModel; |
| // The root node is expanded by default. |
| expanded.add(treeModel.getRoot()); |
| dirty = true; |
| } |
| |
| /** |
| * Inform the instance if the tree root node is visible. If this method |
| * is not called, it is assumed that the tree root node is not visible. |
| * |
| * @param visible true if the tree root node is visible, false |
| * otherwise. |
| */ |
| public void setRootVisible(boolean visible) |
| { |
| rootVisible = visible; |
| dirty = true; |
| } |
| |
| /** |
| * Get the sum of heights for all rows. |
| */ |
| public int getPreferredHeight() |
| { |
| if (dirty) |
| update(); |
| totalHeight = 0; |
| Enumeration en = nodes.elements(); |
| while (en.hasMoreElements()) |
| { |
| NodeRecord nr = (NodeRecord) en.nextElement(); |
| Rectangle r = nr.getBounds(); |
| totalHeight += r.height; |
| } |
| return totalHeight; |
| } |
| |
| /** |
| * Get the maximal width. |
| */ |
| public int getPreferredWidth(Rectangle value) |
| { |
| if (dirty) |
| update(); |
| |
| maximalWidth = 0; |
| Enumeration en = nodes.elements(); |
| while (en.hasMoreElements()) |
| { |
| NodeRecord nr = (NodeRecord) en.nextElement(); |
| Rectangle r = nr.getBounds(); |
| if (r.x + r.width > maximalWidth) |
| maximalWidth = r.x + r.width; |
| } |
| return maximalWidth; |
| } |
| |
| /** |
| * Returns true if this layout supposes that all rows have the fixed |
| * height. |
| * |
| * @return boolean true if all rows in the tree must have the fixed |
| * height (true by default). |
| */ |
| protected boolean isFixedRowHeight() |
| { |
| return true; |
| } |
| |
| } |