Frames | No Frames |
1: /* FixedHeightLayoutCache.java -- Fixed cell height tree layout cache 2: Copyright (C) 2002, 2004, 2006, Free Software Foundation, Inc. 3: 4: This file is part of GNU Classpath. 5: 6: GNU Classpath is free software; you can redistribute it and/or modify 7: it under the terms of the GNU General Public License as published by 8: the Free Software Foundation; either version 2, or (at your option) 9: any later version. 10: 11: GNU Classpath is distributed in the hope that it will be useful, but 12: WITHOUT ANY WARRANTY; without even the implied warranty of 13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14: General Public License for more details. 15: 16: You should have received a copy of the GNU General Public License 17: along with GNU Classpath; see the file COPYING. If not, write to the 18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 19: 02110-1301 USA. 20: 21: Linking this library statically or dynamically with other modules is 22: making a combined work based on this library. Thus, the terms and 23: conditions of the GNU General Public License cover the whole 24: combination. 25: 26: As a special exception, the copyright holders of this library give you 27: permission to link this library with independent modules to produce an 28: executable, regardless of the license terms of these independent 29: modules, and to copy and distribute the resulting executable under 30: terms of your choice, provided that you also meet, for each linked 31: independent module, the terms and conditions of the license of that 32: module. An independent module is a module which is not derived from 33: or based on this library. If you modify this library, you may extend 34: this exception to your version of the library, but you are not 35: obligated to do so. If you do not wish to do so, delete this 36: exception statement from your version. */ 37: 38: package javax.swing.tree; 39: 40: import gnu.javax.swing.tree.GnuPath; 41: 42: import java.awt.Rectangle; 43: import java.util.Enumeration; 44: import java.util.HashSet; 45: import java.util.Hashtable; 46: import java.util.LinkedList; 47: import java.util.Set; 48: import java.util.Vector; 49: 50: import javax.swing.event.TreeModelEvent; 51: 52: 53: /** 54: * The fixed height tree layout. This class assumes that all cells in the tree 55: * have the same fixed height. This may be not the case, for instance, if leaves 56: * and branches have different height, of if the tree rows may have arbitrary 57: * variable height. This class will also work if the NodeDimensions are not 58: * set. 59: * 60: * @author Audrius Meskauskas 61: * @author Andrew Selkirk 62: */ 63: public class FixedHeightLayoutCache 64: extends VariableHeightLayoutCache 65: { 66: /** 67: * The cached node record. 68: */ 69: class NodeRecord 70: { 71: NodeRecord(int aRow, int aDepth, Object aNode, Object aParent) 72: { 73: row = aRow; 74: depth = aDepth; 75: parent = aParent; 76: node = aNode; 77: 78: isExpanded = expanded.contains(aNode); 79: } 80: 81: /** 82: * The row, where the tree node is displayed. 83: */ 84: final int row; 85: 86: /** 87: * The nesting depth 88: */ 89: final int depth; 90: 91: /** 92: * The parent of the given node, null for the root node. 93: */ 94: final Object parent; 95: 96: /** 97: * This node. 98: */ 99: final Object node; 100: 101: /** 102: * True for the expanded nodes. The value is calculated in constructor. 103: * Using this field saves one hashtable access operation. 104: */ 105: final boolean isExpanded; 106: 107: /** 108: * The cached bounds of the tree row. 109: */ 110: Rectangle bounds; 111: 112: /** 113: * The path from the tree top to the given node (computed under first 114: * demand) 115: */ 116: private TreePath path; 117: 118: /** 119: * Get the path for this node. The derived class is returned, 120: * making check for the last child of some parent easier. 121: */ 122: TreePath getPath() 123: { 124: if (path == null) 125: { 126: boolean lastChild = false; 127: if (parent != null) 128: { 129: int nc = treeModel.getChildCount(parent); 130: if (nc > 0) 131: { 132: int n = treeModel.getIndexOfChild(parent, node); 133: if (n == nc - 1) 134: lastChild = true; 135: } 136: } 137: 138: LinkedList<Object> lpath = new LinkedList<Object>(); 139: NodeRecord rp = this; 140: while (rp != null) 141: { 142: lpath.addFirst(rp.node); 143: if (rp.parent != null) 144: { 145: Object parent = rp.parent; 146: rp = (NodeRecord) nodes.get(parent); 147: // Add the root node, even if it is not visible. 148: if (rp == null) 149: lpath.addFirst(parent); 150: } 151: else 152: rp = null; 153: } 154: path = new GnuPath(lpath.toArray(), lastChild); 155: } 156: return path; 157: } 158: 159: /** 160: * Get the rectangle bounds (compute, if required). 161: */ 162: Rectangle getBounds() 163: { 164: // This method may be called in the context when the tree rectangle is 165: // not known. To work around this, it is assumed near infinitely large. 166: if (bounds == null) 167: bounds = getNodeDimensions(node, row, depth, isExpanded, 168: new Rectangle()); 169: return bounds; 170: } 171: } 172: 173: /** 174: * The set of all expanded tree nodes. 175: */ 176: Set<Object> expanded = new HashSet<Object>(); 177: 178: /** 179: * Maps nodes to the row numbers. 180: */ 181: Hashtable<Object,NodeRecord> nodes = new Hashtable<Object,NodeRecord>(); 182: 183: /** 184: * Maps row numbers to nodes. 185: */ 186: Hashtable<Integer,Object> row2node = new Hashtable<Integer,Object>(); 187: 188: /** 189: * If true, the row map must be recomputed before using. 190: */ 191: boolean dirty; 192: 193: /** 194: * The cumulative height of all rows. 195: */ 196: int totalHeight; 197: 198: /** 199: * The maximal width. 200: */ 201: int maximalWidth; 202: 203: /** 204: * Creates the unitialised instance. Before using the class, the row height 205: * must be set with the {@link #setRowHeight(int)} and the model must be set 206: * with {@link #setModel(TreeModel)}. The node dimensions may not be set. 207: */ 208: public FixedHeightLayoutCache() 209: { 210: // Nothing to do here. 211: } 212: 213: /** 214: * Get the total number of rows in the tree. Every displayed node occupies the 215: * single row. The root node row is included if the root node is set as 216: * visible (false by default). 217: * 218: * @return int the number of the displayed rows. 219: */ 220: public int getRowCount() 221: { 222: if (dirty) update(); 223: return row2node.size(); 224: } 225: 226: /** 227: * Refresh the row map. 228: */ 229: private final void update() 230: { 231: nodes.clear(); 232: row2node.clear(); 233: 234: totalHeight = maximalWidth = 0; 235: 236: Object root = treeModel.getRoot(); 237: 238: if (rootVisible) 239: { 240: countRows(root, null, 0); 241: } 242: else 243: { 244: int sc = treeModel.getChildCount(root); 245: for (int i = 0; i < sc; i++) 246: { 247: Object child = treeModel.getChild(root, i); 248: countRows(child, root, 0); 249: } 250: } 251: dirty = false; 252: } 253: 254: /** 255: * Recursively counts all rows in the tree. 256: */ 257: private final void countRows(Object node, Object parent, int depth) 258: { 259: Integer n = new Integer(row2node.size()); 260: row2node.put(n, node); 261: 262: NodeRecord nr = new NodeRecord(n.intValue(), depth, node, parent); 263: nodes.put(node, nr); 264: 265: // For expanded nodes and for the root node. 266: if (expanded.contains(node)) 267: { 268: int sc = treeModel.getChildCount(node); 269: int deeper = depth + 1; 270: for (int i = 0; i < sc; i++) 271: { 272: Object child = treeModel.getChild(node, i); 273: countRows(child, node, deeper); 274: } 275: } 276: } 277: 278: /** 279: * Discard the bound information for the given path. 280: * 281: * @param path the path, for that the bound information must be recomputed. 282: */ 283: public void invalidatePathBounds(TreePath path) 284: { 285: NodeRecord r = (NodeRecord) nodes.get(path.getLastPathComponent()); 286: if (r != null) 287: r.bounds = null; 288: } 289: 290: /** 291: * Mark all cached information as invalid. 292: */ 293: public void invalidateSizes() 294: { 295: dirty = true; 296: } 297: 298: /** 299: * Set the expanded state of the given path. The expansion states must be 300: * always updated when expanding and colapsing the tree nodes. Otherwise 301: * other methods will not work correctly after the nodes are collapsed or 302: * expanded. 303: * 304: * @param path the tree path, for that the state is being set. 305: * @param isExpanded the expanded state of the given path. 306: */ 307: public void setExpandedState(TreePath path, boolean isExpanded) 308: { 309: if (isExpanded) 310: expanded.add(path.getLastPathComponent()); 311: else 312: expanded.remove(path.getLastPathComponent()); 313: 314: dirty = true; 315: } 316: 317: /** 318: * Get the expanded state for the given tree path. 319: * 320: * @return true if the given path is expanded, false otherwise. 321: */ 322: public boolean isExpanded(TreePath path) 323: { 324: return expanded.contains(path.getLastPathComponent()); 325: } 326: 327: /** 328: * Get bounds for the given tree path. 329: * 330: * @param path the tree path 331: * @param rect the rectangle that will be reused to return the result. 332: * @return Rectangle the bounds of the last line, defined by the given path. 333: */ 334: public Rectangle getBounds(TreePath path, Rectangle rect) 335: { 336: if (path == null) 337: return null; 338: if (dirty) 339: update(); 340: Object last = path.getLastPathComponent(); 341: NodeRecord r = nodes.get(last); 342: if (r == null) 343: // This node is not visible. 344: { 345: rect.x = rect.y = rect.width = rect.height = 0; 346: } 347: else 348: { 349: if (r.bounds == null) 350: { 351: Rectangle dim = getNodeDimensions(last, r.row, r.depth, 352: r.isExpanded, rect); 353: r.bounds = dim; 354: } 355: 356: rect.setRect(r.bounds); 357: } 358: return rect; 359: } 360: 361: /** 362: * Get the path, the last element of that is displayed in the given row. 363: * 364: * @param row the row 365: * @return TreePath the path 366: */ 367: public TreePath getPathForRow(int row) 368: { 369: if (dirty) 370: update(); 371: Object last = row2node.get(new Integer(row)); 372: if (last == null) 373: return null; 374: else 375: { 376: NodeRecord r = nodes.get(last); 377: return r.getPath(); 378: } 379: } 380: 381: /** 382: * Get the row, displaying the last node of the given path. 383: * 384: * @param path the path 385: * @return int the row number or -1 if the end of the path is not visible. 386: */ 387: public int getRowForPath(TreePath path) 388: { 389: if (path == null) 390: return -1; 391: 392: if (dirty) update(); 393: 394: NodeRecord r = nodes.get(path.getLastPathComponent()); 395: if (r == null) 396: return - 1; 397: else 398: return r.row; 399: } 400: 401: /** 402: * Get the path, closest to the given point. 403: * 404: * @param x the point x coordinate 405: * @param y the point y coordinate 406: * @return the tree path, closest to the the given point 407: */ 408: public TreePath getPathClosestTo(int x, int y) 409: { 410: if (dirty) 411: update(); 412: 413: // As the rows have arbitrary height, we need to iterate. 414: NodeRecord best = null; 415: NodeRecord r; 416: Enumeration<NodeRecord> en = nodes.elements(); 417: 418: int dist = Integer.MAX_VALUE; 419: 420: while (en.hasMoreElements() && dist > 0) 421: { 422: r = en.nextElement(); 423: if (best == null) 424: { 425: best = r; 426: dist = distance(r.getBounds(), x, y); 427: } 428: else 429: { 430: int rr = distance(r.getBounds(), x, y); 431: if (rr < dist) 432: { 433: best = r; 434: dist = rr; 435: } 436: } 437: } 438: 439: if (best == null) 440: return null; 441: else 442: return best.getPath(); 443: } 444: 445: /** 446: * Get the closest distance from this point till the given rectangle. Only 447: * vertical distance is taken into consideration. 448: */ 449: int distance(Rectangle r, int x, int y) 450: { 451: if (y < r.y) 452: return r.y - y; 453: else if (y > r.y + r.height) 454: return y - (r.y + r.height); 455: else 456: return 0; 457: } 458: 459: /** 460: * Get the number of the visible childs for the given tree path. If the node 461: * is not expanded, 0 is returned. Otherwise, the number of children is 462: * obtained from the model as the number of children for the last path 463: * component. 464: * 465: * @param path the tree path 466: * @return int the number of the visible childs (for row). 467: */ 468: public int getVisibleChildCount(TreePath path) 469: { 470: if (isExpanded(path)) 471: return 0; 472: else 473: return treeModel.getChildCount(path.getLastPathComponent()); 474: } 475: 476: /** 477: * Get the enumeration over all visible paths that start from the given 478: * parent path. 479: * 480: * @param parentPath the parent path 481: * @return the enumeration over pathes 482: */ 483: public Enumeration<TreePath> getVisiblePathsFrom(TreePath parentPath) 484: { 485: if (dirty) 486: update(); 487: Vector<TreePath> p = new Vector<TreePath>(parentPath.getPathCount()); 488: Object node; 489: NodeRecord nr; 490: 491: for (int i = 0; i < parentPath.getPathCount(); i++) 492: { 493: node = parentPath.getPathComponent(i); 494: nr = nodes.get(node); 495: if (nr.row >= 0) 496: p.add((TreePath) node); 497: } 498: return p.elements(); 499: } 500: 501: /** 502: * Return the expansion state of the given tree path. The expansion state 503: * must be previously set with the 504: * {@link #setExpandedState(TreePath, boolean)} 505: * 506: * @param path the path being checked 507: * @return true if the last node of the path is expanded, false otherwise. 508: */ 509: public boolean getExpandedState(TreePath path) 510: { 511: return expanded.contains(path.getLastPathComponent()); 512: } 513: 514: /** 515: * The listener method, called when the tree nodes are changed. 516: * 517: * @param event the change event 518: */ 519: public void treeNodesChanged(TreeModelEvent event) 520: { 521: dirty = true; 522: } 523: 524: /** 525: * The listener method, called when the tree nodes are inserted. 526: * 527: * @param event the change event 528: */ 529: public void treeNodesInserted(TreeModelEvent event) 530: { 531: dirty = true; 532: } 533: 534: /** 535: * The listener method, called when the tree nodes are removed. 536: * 537: * @param event the change event 538: */ 539: public void treeNodesRemoved(TreeModelEvent event) 540: { 541: dirty = true; 542: } 543: 544: /** 545: * Called when the tree structure has been changed. 546: * 547: * @param event the change event 548: */ 549: public void treeStructureChanged(TreeModelEvent event) 550: { 551: dirty = true; 552: } 553: 554: /** 555: * Set the tree model that will provide the data. 556: */ 557: public void setModel(TreeModel newModel) 558: { 559: treeModel = newModel; 560: // The root node is expanded by default. 561: expanded.add(treeModel.getRoot()); 562: dirty = true; 563: } 564: 565: /** 566: * Inform the instance if the tree root node is visible. If this method 567: * is not called, it is assumed that the tree root node is not visible. 568: * 569: * @param visible true if the tree root node is visible, false 570: * otherwise. 571: */ 572: public void setRootVisible(boolean visible) 573: { 574: rootVisible = visible; 575: dirty = true; 576: } 577: 578: /** 579: * Get the sum of heights for all rows. 580: */ 581: public int getPreferredHeight() 582: { 583: if (dirty) 584: update(); 585: totalHeight = 0; 586: Enumeration<NodeRecord> en = nodes.elements(); 587: while (en.hasMoreElements()) 588: { 589: NodeRecord nr = en.nextElement(); 590: Rectangle r = nr.getBounds(); 591: totalHeight += r.height; 592: } 593: return totalHeight; 594: } 595: 596: /** 597: * Get the maximal width. 598: */ 599: public int getPreferredWidth(Rectangle value) 600: { 601: if (dirty) 602: update(); 603: 604: maximalWidth = 0; 605: Enumeration<NodeRecord> en = nodes.elements(); 606: while (en.hasMoreElements()) 607: { 608: NodeRecord nr = en.nextElement(); 609: Rectangle r = nr.getBounds(); 610: if (r.x + r.width > maximalWidth) 611: maximalWidth = r.x + r.width; 612: } 613: return maximalWidth; 614: } 615: 616: /** 617: * Returns true if this layout supposes that all rows have the fixed 618: * height. 619: * 620: * @return boolean true if all rows in the tree must have the fixed 621: * height (true by default). 622: */ 623: protected boolean isFixedRowHeight() 624: { 625: return true; 626: } 627: 628: }