Source for javax.swing.tree.VariableHeightLayoutCache

   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:     Object root = treeModel.getRoot();
 236: 
 237:     if (rootVisible)
 238:       {
 239:         countRows(root, null, 0);
 240:       }
 241:     else
 242:       {
 243:         int sc = treeModel.getChildCount(root);
 244:         for (int i = 0; i < sc; i++)
 245:           {
 246:             Object child = treeModel.getChild(root, i);
 247:             countRows(child, root, 0);
 248:           }
 249:       }
 250:     dirty = false;
 251:   }
 252:   
 253:   /**
 254:    * Recursively counts all rows in the tree.
 255:    */
 256:   private final void countRows(Object node, Object parent, int depth)
 257:   {
 258:     Integer n = new Integer(row2node.size());
 259:     row2node.put(n, node);
 260:     
 261:     NodeRecord nr = new NodeRecord(n.intValue(), depth, node, parent);
 262:     nodes.put(node, nr);
 263:      
 264:     // For expanded nodes
 265:     if (expanded.contains(node))
 266:       {
 267:         int sc = treeModel.getChildCount(node);
 268:         int deeper = depth+1;
 269:         for (int i = 0; i < sc; i++)
 270:           {
 271:             Object child = treeModel.getChild(node, i);
 272:             countRows(child, node, deeper);
 273:           }
 274:       }
 275:   }
 276: 
 277:   /**
 278:    * Discard the bound information for the given path.
 279:    * 
 280:    * @param path the path, for that the bound information must be recomputed.
 281:    */
 282:   public void invalidatePathBounds(TreePath path)
 283:   {
 284:     NodeRecord r = (NodeRecord) nodes.get(path.getLastPathComponent());
 285:     if (r!=null)
 286:       r.bounds = null;
 287:   } 
 288: 
 289:   /**
 290:    * Mark all cached information as invalid.
 291:    */
 292:   public void invalidateSizes()
 293:   {
 294:     dirty = true;
 295:   } 
 296: 
 297:   /**
 298:    * Set the expanded state of the given path. The expansion states must be
 299:    * always updated when expanding and colapsing the tree nodes. Otherwise 
 300:    * other methods will not work correctly after the nodes are collapsed or
 301:    * expanded.
 302:    *
 303:    * @param path the tree path, for that the state is being set.
 304:    * @param isExpanded the expanded state of the given path.
 305:    */
 306:   public void setExpandedState(TreePath path, boolean isExpanded)
 307:   {
 308:     if (isExpanded)
 309:       expanded.add(path.getLastPathComponent());
 310:     else
 311:       expanded.remove(path.getLastPathComponent());
 312:     
 313:     dirty = true;
 314:   }
 315:   
 316:   /**
 317:    * Get the expanded state for the given tree path.
 318:    * 
 319:    * @return true if the given path is expanded, false otherwise.
 320:    */
 321:   public boolean isExpanded(TreePath path)
 322:   {
 323:     return expanded.contains(path.getLastPathComponent());
 324:   } 
 325: 
 326:   /**
 327:    * Get bounds for the given tree path.
 328:    * 
 329:    * @param path the tree path
 330:    * @param rect the rectangle that will be reused to return the result.
 331:    * @return Rectangle the bounds of the last line, defined by the given path.
 332:    */
 333:   public Rectangle getBounds(TreePath path, Rectangle rect)
 334:   {
 335:     if (path == null)
 336:       return null;
 337:     if (dirty)
 338:       update();
 339:     Object last = path.getLastPathComponent();
 340:     NodeRecord r = (NodeRecord) nodes.get(last);
 341:     if (r == null)
 342:     // This node is not visible.
 343:       {
 344:         rect.x = rect.y = rect.width = rect.height = 0;
 345:       }
 346:     else
 347:       {
 348:         if (r.bounds == null)
 349:           {
 350:             Rectangle dim = getNodeDimensions(last, r.row, r.depth,
 351:                                               r.isExpanded, rect);
 352:             r.bounds = dim;
 353:           }
 354: 
 355:         rect.setRect(r.bounds);
 356:       }
 357:     return rect;
 358:   } 
 359: 
 360:   /**
 361:    * Get the path, the last element of that is displayed in the given row.
 362:    * 
 363:    * @param row the row
 364:    * @return TreePath the path
 365:    */
 366:   public TreePath getPathForRow(int row)
 367:   {
 368:     if (dirty)
 369:       update();
 370:     Object last = row2node.get(new Integer(row));
 371:     if (last == null)
 372:       return null;
 373:     else
 374:       {
 375:         NodeRecord r = (NodeRecord) nodes.get(last);
 376:         return r.getPath();
 377:       }
 378:   } 
 379: 
 380:   /**
 381:    * Get the row, displaying the last node of the given path.
 382:    * 
 383:    * @param path the path
 384:    * @return int the row number or -1 if the end of the path is not visible.
 385:    */
 386:   public int getRowForPath(TreePath path)
 387:   {
 388:     if (path == null)
 389:       return -1;
 390:     if (dirty) update();
 391: 
 392:     NodeRecord r = (NodeRecord) nodes.get(path.getLastPathComponent());
 393:     if (r == null)
 394:       return - 1;
 395:     else
 396:       return r.row;
 397:   } 
 398: 
 399:   /**
 400:    * Get the path, closest to the given point.
 401:    * 
 402:    * @param x the point x coordinate
 403:    * @param y the point y coordinate
 404:    * @return the tree path, closest to the the given point
 405:    */
 406:   public TreePath getPathClosestTo(int x, int y)
 407:   {
 408:     if (dirty)
 409:       update();
 410: 
 411:     // As the rows have arbitrary height, we need to iterate.
 412:     NodeRecord best = null;
 413:     NodeRecord r;
 414:     Enumeration en = nodes.elements();
 415:     
 416:     int dist = Integer.MAX_VALUE;
 417: 
 418:     while (en.hasMoreElements() && dist > 0)
 419:       {
 420:         r = (NodeRecord) en.nextElement();
 421:         if (best == null)
 422:           {
 423:             best = r;
 424:             dist = distance(r.getBounds(), x, y);
 425:           }
 426:         else
 427:           {
 428:             int rr = distance(r.getBounds(), x, y);
 429:             if (rr < dist)
 430:               {
 431:                 best = r;
 432:                 dist = rr;
 433:               }
 434:           }
 435:       }
 436: 
 437:     if (best == null)
 438:       return null;
 439:     else
 440:       return best.getPath();
 441:   } 
 442:   
 443:   /**
 444:    * Get the closest distance from this point till the given rectangle. Only
 445:    * vertical distance is taken into consideration.
 446:    */
 447:   int distance(Rectangle r, int x, int y)
 448:   {
 449:     if (y < r.y)
 450:       return r.y - y;
 451:     else if (y > r.y + r.height)
 452:       return y - (r.y + r.height);
 453:     else
 454:       return 0;
 455:   }
 456: 
 457:   /**
 458:    * Get the number of the visible childs for the given tree path. If the node
 459:    * is not expanded, 0 is returned. Otherwise, the number of children is
 460:    * obtained from the model as the number of children for the last path
 461:    * component.
 462:    * 
 463:    * @param path the tree path
 464:    * @return int the number of the visible childs (for row).
 465:    */
 466:   public int getVisibleChildCount(TreePath path)  
 467:   {
 468:     if (isExpanded(path))
 469:       return 0; 
 470:     else
 471:       return treeModel.getChildCount(path.getLastPathComponent());
 472:   } 
 473: 
 474:   /**
 475:    * Get the enumeration over all visible pathes that start from the given
 476:    * parent path.
 477:    * 
 478:    * @param parentPath the parent path
 479:    * @return the enumeration over pathes
 480:    */
 481:   public Enumeration getVisiblePathsFrom(TreePath parentPath)
 482:   {
 483:     if (dirty)
 484:       update();
 485:     Vector p = new Vector(parentPath.getPathCount());
 486:     Object node;
 487:     NodeRecord nr;
 488: 
 489:     for (int i = 0; i < parentPath.getPathCount(); i++)
 490:       {
 491:         node = parentPath.getPathComponent(i);
 492:         nr = (NodeRecord) nodes.get(node);
 493:         if (nr.row >= 0)
 494:           p.add(node);
 495:       }
 496:     return p.elements();
 497:   }
 498: 
 499:   /**
 500:    * Return the expansion state of the given tree path. The expansion state
 501:    * must be previously set with the 
 502:    * {@link #setExpandedState(TreePath, boolean)}
 503:    * 
 504:    * @param path the path being checked
 505:    * @return true if the last node of the path is expanded, false otherwise.
 506:    */
 507:   public boolean getExpandedState(TreePath path)
 508:   {
 509:     return expanded.contains(path.getLastPathComponent());
 510:   }
 511: 
 512:   /**
 513:    * The listener method, called when the tree nodes are changed.
 514:    * 
 515:    * @param event the change event
 516:    */
 517:   public void treeNodesChanged(TreeModelEvent event)
 518:   {
 519:     dirty = true;
 520:   } 
 521: 
 522:   /**
 523:    * The listener method, called when the tree nodes are inserted.
 524:    * 
 525:    * @param event the change event
 526:    */
 527:   public void treeNodesInserted(TreeModelEvent event)
 528:   {
 529:     dirty = true;
 530:   } 
 531: 
 532:   /**
 533:    * The listener method, called when the tree nodes are removed.
 534:    * 
 535:    * @param event the change event
 536:    */
 537:   public void treeNodesRemoved(TreeModelEvent event)
 538:   {
 539:     dirty = true;
 540:   } 
 541: 
 542:   /**
 543:    * Called when the tree structure has been changed. 
 544:    * 
 545:    * @param event the change event
 546:    */
 547:   public void treeStructureChanged(TreeModelEvent event)
 548:   {
 549:     dirty = true;
 550:   } 
 551:   
 552:   /**
 553:    * Set the tree model that will provide the data.
 554:    */
 555:   public void setModel(TreeModel newModel)
 556:   {
 557:     treeModel = newModel;
 558:     // The root node is expanded by default.
 559:     expanded.add(treeModel.getRoot());
 560:     dirty = true;
 561:   }
 562:   
 563:   /**
 564:    * Inform the instance if the tree root node is visible. If this method
 565:    * is not called, it is assumed that the tree root node is not visible.
 566:    * 
 567:    * @param visible true if the tree root node is visible, false
 568:    * otherwise.
 569:    */
 570:   public void setRootVisible(boolean visible)
 571:   {
 572:     rootVisible = visible;
 573:     dirty = true;
 574:   }
 575: 
 576:   /**
 577:    * Get the sum of heights for all rows.
 578:    */
 579:   public int getPreferredHeight()
 580:   {
 581:     if (dirty)
 582:       update();
 583:     totalHeight = 0;
 584:     Enumeration en = nodes.elements();
 585:     while (en.hasMoreElements())
 586:       {
 587:         NodeRecord nr = (NodeRecord) en.nextElement();
 588:         Rectangle r = nr.getBounds();
 589:         totalHeight += r.height;
 590:       }
 591:     return totalHeight;
 592:   }
 593: 
 594:   /**
 595:    * Get the maximal width.
 596:    */
 597:   public int getPreferredWidth(Rectangle value)
 598:   {
 599:     if (dirty)
 600:       update();
 601:     
 602:     maximalWidth = 0;
 603:     Enumeration en = nodes.elements();
 604:     while (en.hasMoreElements())
 605:       {
 606:         NodeRecord nr = (NodeRecord) en.nextElement();
 607:         Rectangle r = nr.getBounds();
 608:         if (r.x + r.width > maximalWidth)
 609:           maximalWidth = r.x + r.width;
 610:       }
 611:     return maximalWidth;
 612:   }
 613: }