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