Source for javax.swing.tree.FixedHeightLayoutCache

   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 lpath = new LinkedList();
 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 expanded = new HashSet();
 177:   
 178:   /**
 179:    * Maps nodes to the row numbers.
 180:    */
 181:   Hashtable nodes = new Hashtable();
 182:   
 183:   /**
 184:    * Maps row numbers to nodes.
 185:    */
 186:   Hashtable row2node = new Hashtable();
 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 = (NodeRecord) 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 = (NodeRecord) 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 = (NodeRecord) 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 en = nodes.elements();
 417:     
 418:     int dist = Integer.MAX_VALUE;
 419: 
 420:     while (en.hasMoreElements() && dist > 0)
 421:       {
 422:         r = (NodeRecord) 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 pathes 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 p = new Vector(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 = (NodeRecord) nodes.get(node);
 495:         if (nr.row >= 0)
 496:           p.add(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 en = nodes.elements();
 587:     while (en.hasMoreElements())
 588:       {
 589:         NodeRecord nr = (NodeRecord) 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 en = nodes.elements();
 606:     while (en.hasMoreElements())
 607:       {
 608:         NodeRecord nr = (NodeRecord) 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: }