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:     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: }