Source for javax.swing.plaf.basic.BasicListUI

   1: /* BasicListUI.java --
   2:    Copyright (C) 2002, 2004, 2005 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: 
  39: package javax.swing.plaf.basic;
  40: 
  41: import java.awt.Color;
  42: import java.awt.Component;
  43: import java.awt.Dimension;
  44: import java.awt.Graphics;
  45: import java.awt.Point;
  46: import java.awt.Rectangle;
  47: import java.awt.event.ActionEvent;
  48: import java.awt.event.ActionListener;
  49: import java.awt.event.ComponentAdapter;
  50: import java.awt.event.ComponentEvent;
  51: import java.awt.event.ComponentListener;
  52: import java.awt.event.FocusEvent;
  53: import java.awt.event.FocusListener;
  54: import java.awt.event.KeyEvent;
  55: import java.awt.event.MouseEvent;
  56: import java.beans.PropertyChangeEvent;
  57: import java.beans.PropertyChangeListener;
  58: 
  59: import javax.swing.AbstractAction;
  60: import javax.swing.ActionMap;
  61: import javax.swing.CellRendererPane;
  62: import javax.swing.DefaultListSelectionModel;
  63: import javax.swing.InputMap;
  64: import javax.swing.JComponent;
  65: import javax.swing.JList;
  66: import javax.swing.JViewport;
  67: import javax.swing.KeyStroke;
  68: import javax.swing.ListCellRenderer;
  69: import javax.swing.ListModel;
  70: import javax.swing.ListSelectionModel;
  71: import javax.swing.LookAndFeel;
  72: import javax.swing.UIDefaults;
  73: import javax.swing.UIManager;
  74: import javax.swing.event.ListDataEvent;
  75: import javax.swing.event.ListDataListener;
  76: import javax.swing.event.ListSelectionEvent;
  77: import javax.swing.event.ListSelectionListener;
  78: import javax.swing.event.MouseInputListener;
  79: import javax.swing.plaf.ComponentUI;
  80: import javax.swing.plaf.InputMapUIResource;
  81: import javax.swing.plaf.ListUI;
  82: 
  83: /**
  84:  * The Basic Look and Feel UI delegate for the 
  85:  * JList.
  86:  */
  87: public class BasicListUI extends ListUI
  88: {
  89: 
  90:   /**
  91:    * A helper class which listens for {@link ComponentEvent}s from
  92:    * the JList.
  93:    */
  94:   private class ComponentHandler extends ComponentAdapter {
  95: 
  96:     /**
  97:      * Called when the component is hidden. Invalidates the internal
  98:      * layout.
  99:      */
 100:     public void componentResized(ComponentEvent ev) {
 101:       BasicListUI.this.damageLayout();
 102:     }
 103:   }
 104: 
 105:   /**
 106:    * A helper class which listens for {@link FocusEvent}s
 107:    * from the JList.
 108:    */
 109:   public class FocusHandler implements FocusListener
 110:   {
 111:     /**
 112:      * Called when the JList acquires focus.
 113:      *
 114:      * @param e The FocusEvent representing focus acquisition
 115:      */
 116:     public void focusGained(FocusEvent e)
 117:     {
 118:       repaintCellFocus();
 119:     }
 120: 
 121:     /**
 122:      * Called when the JList loses focus.
 123:      *
 124:      * @param e The FocusEvent representing focus loss
 125:      */
 126:     public void focusLost(FocusEvent e)
 127:     {
 128:       repaintCellFocus();
 129:     }
 130: 
 131:     /**
 132:      * Helper method to repaint the focused cell's 
 133:      * lost or acquired focus state.
 134:      */
 135:     protected void repaintCellFocus()
 136:     {
 137:       // TODO: Implement this properly.
 138:     }
 139:   }
 140: 
 141:   /**
 142:    * A helper class which listens for {@link ListDataEvent}s generated by
 143:    * the {@link JList}'s {@link ListModel}.
 144:    *
 145:    * @see javax.swing.JList#getModel()
 146:    */
 147:   public class ListDataHandler implements ListDataListener
 148:   {
 149:     /**
 150:      * Called when a general change has happened in the model which cannot
 151:      * be represented in terms of a simple addition or deletion.
 152:      *
 153:      * @param e The event representing the change
 154:      */
 155:     public void contentsChanged(ListDataEvent e)
 156:     {
 157:       BasicListUI.this.damageLayout();
 158:     }
 159: 
 160:     /**
 161:      * Called when an interval of objects has been added to the model.
 162:      *
 163:      * @param e The event representing the addition
 164:      */
 165:     public void intervalAdded(ListDataEvent e)
 166:     {
 167:       BasicListUI.this.damageLayout();
 168:     }
 169: 
 170:     /**
 171:      * Called when an inteval of objects has been removed from the model.
 172:      *
 173:      * @param e The event representing the removal
 174:      */
 175:     public void intervalRemoved(ListDataEvent e)
 176:     {
 177:       BasicListUI.this.damageLayout();
 178:     }
 179:   }
 180: 
 181:   /**
 182:    * A helper class which listens for {@link ListSelectionEvent}s
 183:    * from the {@link JList}'s {@link ListSelectionModel}.
 184:    */
 185:   public class ListSelectionHandler implements ListSelectionListener
 186:   {
 187:     /**
 188:      * Called when the list selection changes.  
 189:      *
 190:      * @param e The event representing the change
 191:      */
 192:     public void valueChanged(ListSelectionEvent e)
 193:     {
 194:       // TODO: Implement this properly.
 195:     }
 196:   }
 197: 
 198:   /**
 199:    * This class is used to mimmic the behaviour of the JDK when registering
 200:    * keyboard actions.  It is the same as the private class used in JComponent
 201:    * for the same reason.  This class receives an action event and dispatches
 202:    * it to the true receiver after altering the actionCommand property of the
 203:    * event.
 204:    */
 205:   private static class ActionListenerProxy
 206:     extends AbstractAction
 207:   {
 208:     ActionListener target;
 209:     String bindingCommandName;
 210: 
 211:     public ActionListenerProxy(ActionListener li, 
 212:                                String cmd)
 213:     {
 214:       target = li;
 215:       bindingCommandName = cmd;
 216:     }
 217: 
 218:     public void actionPerformed(ActionEvent e)
 219:     {
 220:       ActionEvent derivedEvent = new ActionEvent(e.getSource(),
 221:                                                  e.getID(),
 222:                                                  bindingCommandName,
 223:                                                  e.getModifiers());
 224:       target.actionPerformed(derivedEvent);
 225:     }
 226:   }
 227:   
 228:   class ListAction extends AbstractAction
 229:   {
 230:     public void actionPerformed (ActionEvent e)
 231:     {
 232:       int lead = list.getLeadSelectionIndex();
 233:       int max = list.getModel().getSize() - 1;
 234:       DefaultListSelectionModel selModel = (DefaultListSelectionModel)list.getSelectionModel();
 235:       String command = e.getActionCommand();
 236:       // Do nothing if list is empty
 237:       if (max == -1)
 238:         return;
 239:       
 240:       if (command.equals("selectNextRow"))
 241:         {
 242:           selectNextIndex();
 243:         }
 244:       else if (command.equals("selectPreviousRow"))
 245:         {
 246:           selectPreviousIndex();
 247:         }
 248:       else if (command.equals("clearSelection"))
 249:         {
 250:           list.clearSelection();
 251:         }
 252:       else if (command.equals("selectAll"))
 253:         {
 254:           list.setSelectionInterval(0, max);
 255:           // this next line is to restore the lead selection index to the old
 256:           // position, because select-all should not change the lead index
 257:           list.addSelectionInterval(lead, lead);
 258:         }
 259:       else if (command.equals("selectLastRow"))
 260:         {
 261:           list.setSelectedIndex(list.getModel().getSize() - 1); 
 262:         }
 263:       else if (command.equals("selectLastRowChangeLead"))
 264:         {
 265:           selModel.moveLeadSelectionIndex(list.getModel().getSize() - 1);
 266:         }
 267:       else if (command.equals("scrollDownExtendSelection"))
 268:         {
 269:           int target;
 270:           if (lead == list.getLastVisibleIndex())
 271:             {
 272:               target = Math.min
 273:                 (max, lead + (list.getLastVisibleIndex() -
 274:                     list.getFirstVisibleIndex() + 1));
 275:             }
 276:           else
 277:             target = list.getLastVisibleIndex();
 278:           selModel.setLeadSelectionIndex(target);
 279:         }
 280:       else if (command.equals("scrollDownChangeLead"))
 281:         {
 282:           int target;
 283:           if (lead == list.getLastVisibleIndex())
 284:             {
 285:               target = Math.min
 286:                 (max, lead + (list.getLastVisibleIndex() -
 287:                     list.getFirstVisibleIndex() + 1));
 288:             }
 289:           else
 290:             target = list.getLastVisibleIndex();
 291:           selModel.moveLeadSelectionIndex(target);
 292:         }
 293:       else if (command.equals("scrollUpExtendSelection"))
 294:         {
 295:           int target;
 296:           if (lead == list.getFirstVisibleIndex())
 297:             {
 298:               target = Math.max 
 299:                 (0, lead - (list.getLastVisibleIndex() - 
 300:                     list.getFirstVisibleIndex() + 1));
 301:             }
 302:           else
 303:             target = list.getFirstVisibleIndex();
 304:           selModel.setLeadSelectionIndex(target);
 305:         }
 306:       else if (command.equals("scrollUpChangeLead"))
 307:         {
 308:           int target;
 309:           if (lead == list.getFirstVisibleIndex())
 310:             {
 311:               target = Math.max 
 312:                 (0, lead - (list.getLastVisibleIndex() - 
 313:                     list.getFirstVisibleIndex() + 1));
 314:             }
 315:           else
 316:             target = list.getFirstVisibleIndex();
 317:           selModel.moveLeadSelectionIndex(target);
 318:         }
 319:       else if (command.equals("selectNextRowExtendSelection"))
 320:         {
 321:           selModel.setLeadSelectionIndex(Math.min(lead + 1,max));
 322:         }
 323:       else if (command.equals("selectFirstRow"))
 324:         {
 325:           list.setSelectedIndex(0);
 326:         }
 327:       else if (command.equals("selectFirstRowChangeLead"))
 328:           {
 329:             selModel.moveLeadSelectionIndex(0);
 330:           }
 331:       else if (command.equals("selectFirstRowExtendSelection"))
 332:         {
 333:           selModel.setLeadSelectionIndex(0);
 334:         }
 335:       else if (command.equals("selectPreviousRowExtendSelection"))
 336:         {
 337:           selModel.setLeadSelectionIndex(Math.max(0,lead - 1));
 338:         }
 339:       else if (command.equals("scrollUp"))
 340:         {
 341:           int target;
 342:           if (lead == list.getFirstVisibleIndex())
 343:             {
 344:               target = Math.max 
 345:                 (0, lead - (list.getLastVisibleIndex() - 
 346:                     list.getFirstVisibleIndex() + 1));
 347:             }
 348:           else
 349:             target = list.getFirstVisibleIndex();
 350:           list.setSelectedIndex(target);          
 351:         }
 352:       else if (command.equals("selectLastRowExtendSelection"))
 353:         {
 354:           selModel.setLeadSelectionIndex(list.getModel().getSize() - 1);
 355:         }
 356:       else if (command.equals("scrollDown"))
 357:         {
 358:           int target;
 359:           if (lead == list.getLastVisibleIndex())
 360:             {
 361:               target = Math.min
 362:                 (max, lead + (list.getLastVisibleIndex() -
 363:                     list.getFirstVisibleIndex() + 1));
 364:             }
 365:           else
 366:             target = list.getLastVisibleIndex();
 367:           list.setSelectedIndex(target);
 368:         }
 369:       else if (command.equals("selectNextRowChangeLead"))
 370:           {
 371:             if (selModel.getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
 372:               selectNextIndex();
 373:             else
 374:               {
 375:                 selModel.moveLeadSelectionIndex(Math.min(max, lead + 1));
 376:               }
 377:           }
 378:       else if (command.equals("selectPreviousRowChangeLead"))
 379:         {
 380:           if (selModel.getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
 381:             selectPreviousIndex();
 382:           else
 383:             {
 384:               selModel.moveLeadSelectionIndex(Math.max(0, lead - 1));
 385:             }
 386:         }      
 387:       else if (command.equals("addToSelection"))
 388:         {
 389:           list.addSelectionInterval(lead, lead);
 390:         }
 391:       else if (command.equals("extendTo"))
 392:         {
 393:           selModel.setSelectionInterval(selModel.getAnchorSelectionIndex(),
 394:                                         lead);
 395:         }
 396:       else if (command.equals("toggleAndAnchor"))
 397:         {
 398:           if (!list.isSelectedIndex(lead))
 399:             list.addSelectionInterval(lead, lead);
 400:           else
 401:             list.removeSelectionInterval(lead, lead);
 402:           selModel.setAnchorSelectionIndex(lead);
 403:         }
 404:       else 
 405:         {
 406:           // DEBUG: uncomment the following line to print out 
 407:           // key bindings that aren't implemented yet
 408:           
 409:           // System.out.println ("not implemented: "+e.getActionCommand());
 410:         }
 411:       
 412:       list.ensureIndexIsVisible(list.getLeadSelectionIndex());
 413:     }
 414:   }
 415:      
 416:   /**
 417:    * A helper class which listens for {@link MouseEvent}s 
 418:    * from the {@link JList}.
 419:    */
 420:   public class MouseInputHandler implements MouseInputListener
 421:   {
 422:     /**
 423:      * Called when a mouse button press/release cycle completes
 424:      * on the {@link JList}
 425:      *
 426:      * @param event The event representing the mouse click
 427:      */
 428:     public void mouseClicked(MouseEvent event)
 429:     {
 430:       Point click = event.getPoint();
 431:       int index = locationToIndex(list, click);
 432:       if (index == -1)
 433:         return;
 434:       if (event.isShiftDown())
 435:         {
 436:           if (list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)
 437:             list.setSelectedIndex(index);
 438:           else if (list.getSelectionMode() == 
 439:                    ListSelectionModel.SINGLE_INTERVAL_SELECTION)
 440:             // COMPAT: the IBM VM is compatible with the following line of code.
 441:             // However, compliance with Sun's VM would correspond to replacing 
 442:             // getAnchorSelectionIndex() with getLeadSelectionIndex().This is 
 443:             // both unnatural and contradictory to the way they handle other 
 444:             // similar UI interactions.
 445:             list.setSelectionInterval(list.getAnchorSelectionIndex(), index);
 446:           else
 447:             // COMPAT: both Sun and IBM are compatible instead with:
 448:             // list.setSelectionInterval
 449:             //     (list.getLeadSelectionIndex(),index);
 450:             // Note that for IBM this is contradictory to what they did in 
 451:             // the above situation for SINGLE_INTERVAL_SELECTION.  
 452:             // The most natural thing to do is the following:
 453:             if (list.isSelectedIndex(list.getAnchorSelectionIndex()))
 454:               list.getSelectionModel().setLeadSelectionIndex(index);
 455:             else
 456:               list.addSelectionInterval(list.getAnchorSelectionIndex(), index);
 457:         }
 458:       else if (event.isControlDown())
 459:         {
 460:           if (list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)
 461:             list.setSelectedIndex(index);
 462:           else if (list.isSelectedIndex(index))
 463:             list.removeSelectionInterval(index,index);
 464:           else
 465:             list.addSelectionInterval(index,index);
 466:         }
 467:       else
 468:         list.setSelectedIndex(index);
 469:       
 470:       list.ensureIndexIsVisible(list.getLeadSelectionIndex());
 471:     }
 472: 
 473:     /**
 474:      * Called when a mouse button is pressed down on the
 475:      * {@link JList}.
 476:      *
 477:      * @param event The event representing the mouse press
 478:      */
 479:     public void mousePressed(MouseEvent event)
 480:     {
 481:       // TODO: What should be done here, if anything?
 482:     }
 483: 
 484:     /**
 485:      * Called when a mouse button is released on
 486:      * the {@link JList}
 487:      *
 488:      * @param event The event representing the mouse press
 489:      */
 490:     public void mouseReleased(MouseEvent event)
 491:     {
 492:       // TODO: What should be done here, if anything?
 493:     }
 494: 
 495:     /**
 496:      * Called when the mouse pointer enters the area bounded
 497:      * by the {@link JList}
 498:      *
 499:      * @param event The event representing the mouse entry
 500:      */
 501:     public void mouseEntered(MouseEvent event)
 502:     {
 503:       // TODO: What should be done here, if anything?
 504:     }
 505: 
 506:     /**
 507:      * Called when the mouse pointer leaves the area bounded
 508:      * by the {@link JList}
 509:      *
 510:      * @param event The event representing the mouse exit
 511:      */
 512:     public void mouseExited(MouseEvent event)
 513:     {
 514:       // TODO: What should be done here, if anything?
 515:     }
 516: 
 517:     /**
 518:      * Called when the mouse pointer moves over the area bounded
 519:      * by the {@link JList} while a button is held down.
 520:      *
 521:      * @param event The event representing the mouse drag
 522:      */
 523:     public void mouseDragged(MouseEvent event)
 524:     {
 525:       // TODO: What should be done here, if anything?
 526:     }
 527: 
 528:     /**
 529:      * Called when the mouse pointer moves over the area bounded
 530:      * by the {@link JList}.
 531:      *
 532:      * @param event The event representing the mouse move
 533:      */
 534:     public void mouseMoved(MouseEvent event)
 535:     {
 536:       // TODO: What should be done here, if anything?
 537:     }
 538:   }
 539: 
 540:   /**
 541:    * Helper class which listens to {@link PropertyChangeEvent}s
 542:    * from the {@link JList}.
 543:    */
 544:   public class PropertyChangeHandler implements PropertyChangeListener
 545:   {
 546:     /**
 547:      * Called when the {@link JList} changes one of its bound properties.
 548:      *
 549:      * @param e The event representing the property change
 550:      */
 551:     public void propertyChange(PropertyChangeEvent e)
 552:     {
 553:       if (e.getSource() == BasicListUI.this.list)
 554:         {
 555:           if (e.getOldValue() != null && e.getOldValue() instanceof ListModel)
 556:             ((ListModel) e.getOldValue()).removeListDataListener(BasicListUI.this.listDataListener);
 557: 
 558:           if (e.getNewValue() != null && e.getNewValue() instanceof ListModel)
 559:             ((ListModel) e.getNewValue()).addListDataListener(BasicListUI.this.listDataListener);
 560:         }
 561:       // Update the updateLayoutStateNeeded flag.
 562:       if (e.getPropertyName().equals("model"))
 563:         updateLayoutStateNeeded += modelChanged;
 564:       else if (e.getPropertyName().equals("selectionModel"))
 565:         updateLayoutStateNeeded += selectionModelChanged;
 566:       else if (e.getPropertyName().equals("font"))
 567:         updateLayoutStateNeeded += fontChanged;
 568:       else if (e.getPropertyName().equals("fixedCellWidth"))
 569:         updateLayoutStateNeeded += fixedCellWidthChanged;
 570:       else if (e.getPropertyName().equals("fixedCellHeight"))
 571:         updateLayoutStateNeeded += fixedCellHeightChanged;
 572:       else if (e.getPropertyName().equals("prototypeCellValue"))
 573:         updateLayoutStateNeeded += prototypeCellValueChanged;
 574:       else if (e.getPropertyName().equals("cellRenderer"))
 575:         updateLayoutStateNeeded += cellRendererChanged;
 576: 
 577:       BasicListUI.this.damageLayout();
 578:     }
 579:   }
 580: 
 581:   /**
 582:    * A constant to indicate that the model has changed.
 583:    */
 584:   protected static final int modelChanged = 1;
 585: 
 586:   /**
 587:    * A constant to indicate that the selection model has changed.
 588:    */
 589:   protected static final int selectionModelChanged = 2;
 590: 
 591:   /**
 592:    * A constant to indicate that the font has changed.
 593:    */
 594:   protected static final int fontChanged = 4;
 595: 
 596:   /**
 597:    * A constant to indicate that the fixedCellWidth has changed.
 598:    */
 599:   protected static final int fixedCellWidthChanged = 8;
 600: 
 601:   /**
 602:    * A constant to indicate that the fixedCellHeight has changed.
 603:    */
 604:   protected static final int fixedCellHeightChanged = 16;
 605: 
 606:   /**
 607:    * A constant to indicate that the prototypeCellValue has changed.
 608:    */
 609:   protected static final int prototypeCellValueChanged = 32;
 610: 
 611:   /**
 612:    * A constant to indicate that the cellRenderer has changed.
 613:    */
 614:   protected static final int cellRendererChanged = 64;
 615: 
 616:   /**
 617:    * Creates a new BasicListUI for the component.
 618:    *
 619:    * @param c The component to create a UI for
 620:    *
 621:    * @return A new UI
 622:    */
 623:   public static ComponentUI createUI(final JComponent c)
 624:   {
 625:     return new BasicListUI();
 626:   }
 627: 
 628:   /** The current focus listener. */
 629:   protected FocusListener focusListener;
 630: 
 631:   /** The data listener listening to the model. */
 632:   protected ListDataListener listDataListener;
 633: 
 634:   /** The selection listener listening to the selection model. */
 635:   protected ListSelectionListener listSelectionListener;
 636: 
 637:   /** The mouse listener listening to the list. */
 638:   protected MouseInputListener mouseInputListener;
 639: 
 640:   /** The property change listener listening to the list. */
 641:   protected PropertyChangeListener propertyChangeListener;
 642: 
 643: 
 644:   /** The component listener that receives notification for resizing the
 645:    * JList component.*/
 646:   private ComponentListener componentListener;
 647: 
 648:   /** Saved reference to the list this UI was created for. */
 649:   protected JList list;
 650: 
 651:   /** The height of a single cell in the list. */
 652:   protected int cellHeight;
 653: 
 654:   /** The width of a single cell in the list. */
 655:   protected int cellWidth;
 656: 
 657:   /** 
 658:    * An array of varying heights of cells in the list, in cases where each
 659:    * cell might have a different height.
 660:    */
 661:   protected int[] cellHeights;
 662: 
 663:   /**
 664:    * A bitmask that indicates which properties of the JList have changed.
 665:    * When nonzero, indicates that the UI class is out of
 666:    * date with respect to the underlying list, and must recalculate the
 667:    * list layout before painting or performing size calculations.
 668:    *
 669:    * @see #modelChanged
 670:    * @see #selectionModelChanged
 671:    * @see #fontChanged
 672:    * @see #fixedCellWidthChanged
 673:    * @see #fixedCellHeightChanged
 674:    * @see #prototypeCellValueChanged
 675:    * @see #cellRendererChanged
 676:    */
 677:   protected int updateLayoutStateNeeded;
 678: 
 679:   /**
 680:    * The {@link CellRendererPane} that is used for painting.
 681:    */
 682:   protected CellRendererPane rendererPane;
 683:   
 684:   /** The action bound to KeyStrokes. */
 685:   ListAction action;
 686: 
 687:   /**
 688:    * Calculate the height of a particular row. If there is a fixed {@link
 689:    * #cellHeight}, return it; otherwise return the specific row height
 690:    * requested from the {@link #cellHeights} array. If the requested row
 691:    * is invalid, return <code>-1</code>.
 692:    *
 693:    * @param row The row to get the height of
 694:    *
 695:    * @return The height, in pixels, of the specified row
 696:    */
 697:   protected int getRowHeight(int row)
 698:   {
 699:     if (row < 0 || row >= cellHeights.length)
 700:       return -1;
 701:     else if (cellHeight != -1)
 702:       return cellHeight;
 703:     else
 704:       return cellHeights[row];
 705:   }
 706: 
 707:   /**
 708:    * Calculate the bounds of a particular cell, considering the upper left
 709:    * corner of the list as the origin position <code>(0,0)</code>.
 710:    *
 711:    * @param l Ignored; calculates over <code>this.list</code>
 712:    * @param index1 The first row to include in the bounds
 713:    * @param index2 The last row to incude in the bounds
 714:    *
 715:    * @return A rectangle encompassing the range of rows between 
 716:    * <code>index1</code> and <code>index2</code> inclusive
 717:    */
 718:   public Rectangle getCellBounds(JList l, int index1, int index2)
 719:   {
 720:     maybeUpdateLayoutState();
 721: 
 722:     if (l != list || cellWidth == -1)
 723:       return null;
 724: 
 725:     int minIndex = Math.min(index1, index2);
 726:     int maxIndex = Math.max(index1, index2);
 727:     Point loc = indexToLocation(list, minIndex);
 728:     Rectangle bounds = new Rectangle(loc.x, loc.y, cellWidth,
 729:                                      getRowHeight(minIndex));
 730: 
 731:     for (int i = minIndex + 1; i <= maxIndex; i++)
 732:       {
 733:         Point hiLoc = indexToLocation(list, i);
 734:         Rectangle hibounds = new Rectangle(hiLoc.x, hiLoc.y, cellWidth,
 735:                                        getRowHeight(i));
 736:         bounds = bounds.union(hibounds);
 737:       }
 738: 
 739:     return bounds;
 740:   }
 741: 
 742:   /**
 743:    * Calculate the Y coordinate of the upper edge of a particular row,
 744:    * considering the Y coordinate <code>0</code> to occur at the top of the
 745:    * list.
 746:    *
 747:    * @param row The row to calculate the Y coordinate of
 748:    *
 749:    * @return The Y coordinate of the specified row, or <code>-1</code> if
 750:    * the specified row number is invalid
 751:    */
 752:   protected int convertRowToY(int row)
 753:   {
 754:     int y = 0;
 755:     for (int i = 0; i < row; ++i)
 756:       {
 757:         int h = getRowHeight(i);
 758:         if (h == -1)
 759:           return -1;
 760:         y += h;
 761:       }
 762:     return y;
 763:   }
 764: 
 765:   /**
 766:    * Calculate the row number containing a particular Y coordinate,
 767:    * considering the Y coodrinate <code>0</code> to occur at the top of the
 768:    * list.
 769:    *
 770:    * @param y0 The Y coordinate to calculate the row number for
 771:    *
 772:    * @return The row number containing the specified Y value, or <code>-1</code>
 773:    * if the specified Y coordinate is invalid
 774:    */
 775:   protected int convertYToRow(int y0)
 776:   {
 777:     for (int row = 0; row < cellHeights.length; ++row)
 778:       {
 779:         int h = getRowHeight(row);
 780: 
 781:         if (y0 < h)
 782:           return row;
 783:         y0 -= h;
 784:       }
 785:     return -1;
 786:   }
 787: 
 788:   /**
 789:    * Recomputes the {@link #cellHeights}, {@link #cellHeight}, and {@link
 790:    * #cellWidth} properties by examining the variouis properties of the
 791:    * {@link JList}.
 792:    */
 793:   protected void updateLayoutState()
 794:   {
 795:     int nrows = list.getModel().getSize();
 796:     cellHeight = -1;
 797:     cellWidth = -1;
 798:     if (cellHeights == null || cellHeights.length != nrows)
 799:       cellHeights = new int[nrows];
 800:     if (list.getFixedCellHeight() == -1 || list.getFixedCellWidth() == -1)
 801:       {
 802:         ListCellRenderer rend = list.getCellRenderer();
 803:         for (int i = 0; i < nrows; ++i)
 804:           {
 805:             Component flyweight = rend.getListCellRendererComponent(list,
 806:                                                                     list.getModel()
 807:                                                                         .getElementAt(i),
 808:                                                                     0, false,
 809:                                                                     false);
 810:             Dimension dim = flyweight.getPreferredSize();
 811:             cellHeights[i] = dim.height;
 812:             // compute average cell height (little hack here)
 813:             cellHeight = (cellHeight * i + cellHeights[i]) / (i + 1);
 814:             cellWidth = Math.max(cellWidth, dim.width);
 815:             if (list.getLayoutOrientation() == JList.VERTICAL)
 816:                 cellWidth = Math.max(cellWidth, list.getSize().width);
 817:           }
 818:       }
 819:     else
 820:       {
 821:         cellHeight = list.getFixedCellHeight();
 822:         cellWidth = list.getFixedCellWidth();
 823:       }
 824:   }
 825: 
 826:   /**
 827:    * Marks the current layout as damaged and requests revalidation from the
 828:    * JList.
 829:    * This is package-private to avoid an accessor method.
 830:    *
 831:    * @see #updateLayoutStateNeeded
 832:    */
 833:   void damageLayout()
 834:   {
 835:     updateLayoutStateNeeded = 1;
 836:   }
 837: 
 838:   /**
 839:    * Calls {@link #updateLayoutState} if {@link #updateLayoutStateNeeded}
 840:    * is nonzero, then resets {@link #updateLayoutStateNeeded} to zero.
 841:    */
 842:   protected void maybeUpdateLayoutState()
 843:   {
 844:     if (updateLayoutStateNeeded != 0)
 845:       {
 846:         updateLayoutState();
 847:         updateLayoutStateNeeded = 0;
 848:       }
 849:   }
 850: 
 851:   /**
 852:    * Creates a new BasicListUI object.
 853:    */
 854:   public BasicListUI()
 855:   {
 856:     updateLayoutStateNeeded = 1;
 857:     rendererPane = new CellRendererPane();
 858:   }
 859: 
 860:   /**
 861:    * Installs various default settings (mostly colors) from the {@link
 862:    * UIDefaults} into the {@link JList}
 863:    *
 864:    * @see #uninstallDefaults
 865:    */
 866:   protected void installDefaults()
 867:   {
 868:     LookAndFeel.installColorsAndFont(list, "List.background",
 869:                                      "List.foreground", "List.font");
 870:     list.setSelectionForeground(UIManager.getColor("List.selectionForeground"));
 871:     list.setSelectionBackground(UIManager.getColor("List.selectionBackground"));
 872:     list.setOpaque(true);
 873:   }
 874: 
 875:   /**
 876:    * Resets to <code>null</code> those defaults which were installed in 
 877:    * {@link #installDefaults}
 878:    */
 879:   protected void uninstallDefaults()
 880:   {
 881:     UIDefaults defaults = UIManager.getLookAndFeelDefaults();
 882:     list.setForeground(null);
 883:     list.setBackground(null);
 884:     list.setSelectionForeground(null);
 885:     list.setSelectionBackground(null);
 886:   }
 887: 
 888:   /**
 889:    * Attaches all the listeners we have in the UI class to the {@link
 890:    * JList}, its model and its selection model.
 891:    *
 892:    * @see #uninstallListeners
 893:    */
 894:   protected void installListeners()
 895:   {
 896:     if (focusListener == null)
 897:       focusListener = createFocusListener();
 898:     list.addFocusListener(focusListener);
 899:     if (listDataListener == null)
 900:       listDataListener = createListDataListener();
 901:     list.getModel().addListDataListener(listDataListener);
 902:     if (listSelectionListener == null)
 903:       listSelectionListener = createListSelectionListener();
 904:     list.addListSelectionListener(listSelectionListener);
 905:     if (mouseInputListener == null)
 906:       mouseInputListener = createMouseInputListener();
 907:     list.addMouseListener(mouseInputListener);
 908:     list.addMouseMotionListener(mouseInputListener);
 909:     if (propertyChangeListener == null)
 910:       propertyChangeListener = createPropertyChangeListener();
 911:     list.addPropertyChangeListener(propertyChangeListener);
 912: 
 913:     // FIXME: Are these two really needed? At least they are not documented.
 914:     //keyListener = new KeyHandler();
 915:     list.addComponentListener(componentListener);
 916:     componentListener = new ComponentHandler();
 917:     //list.addKeyListener(keyListener);
 918:   }
 919: 
 920:   /**
 921:    * Detaches all the listeners we attached in {@link #installListeners}.
 922:    */
 923:   protected void uninstallListeners()
 924:   {
 925:     list.removeFocusListener(focusListener);
 926:     list.getModel().removeListDataListener(listDataListener);
 927:     list.removeListSelectionListener(listSelectionListener);
 928:     list.removeMouseListener(mouseInputListener);
 929:     //list.removeKeyListener(keyListener);
 930:     list.removeMouseMotionListener(mouseInputListener);
 931:     list.removePropertyChangeListener(propertyChangeListener);
 932:   }
 933:   
 934:   /**
 935:    * Installs keyboard actions for this UI in the {@link JList}.
 936:    */
 937:   protected void installKeyboardActions()
 938:   {
 939:     UIDefaults defaults = UIManager.getLookAndFeelDefaults();
 940:     InputMap focusInputMap = (InputMap)defaults.get("List.focusInputMap");
 941:     InputMapUIResource parentInputMap = new InputMapUIResource();
 942:     // FIXME: The JDK uses a LazyActionMap for parentActionMap
 943:     ActionMap parentActionMap = new ActionMap();
 944:     action = new ListAction();
 945:     Object keys[] = focusInputMap.allKeys();
 946:     // Register key bindings in the UI InputMap-ActionMap pair
 947:     for (int i = 0; i < keys.length; i++)
 948:       {
 949:         KeyStroke stroke = (KeyStroke)keys[i];
 950:         String actionString = (String) focusInputMap.get(stroke);
 951:         parentInputMap.put(KeyStroke.getKeyStroke(stroke.getKeyCode(),
 952:                                                   stroke.getModifiers()),
 953:                            actionString);
 954: 
 955:         parentActionMap.put (actionString, 
 956:                              new ActionListenerProxy(action, actionString));
 957:       }
 958:     // Register the new InputMap-ActionMap as the parents of the list's
 959:     // InputMap and ActionMap
 960:     parentInputMap.setParent(list.getInputMap().getParent());
 961:     parentActionMap.setParent(list.getActionMap().getParent());
 962:     list.getInputMap().setParent(parentInputMap);
 963:     list.getActionMap().setParent(parentActionMap);
 964:   }
 965: 
 966:   /**
 967:    * Uninstalls keyboard actions for this UI in the {@link JList}.
 968:    */
 969:   protected void uninstallKeyboardActions()
 970:   {
 971:     // TODO: Implement this properly.
 972:   }
 973: 
 974:   /**
 975:    * Installs the various aspects of the UI in the {@link JList}. In
 976:    * particular, calls {@link #installDefaults}, {@link #installListeners}
 977:    * and {@link #installKeyboardActions}. Also saves a reference to the
 978:    * provided component, cast to a {@link JList}.
 979:    *
 980:    * @param c The {@link JList} to install the UI into
 981:    */
 982:   public void installUI(final JComponent c)
 983:   {
 984:     super.installUI(c);
 985:     list = (JList) c;
 986:     installDefaults();
 987:     installListeners();
 988:     installKeyboardActions();
 989:     maybeUpdateLayoutState();
 990:   }
 991: 
 992:   /**
 993:    * Uninstalls all the aspects of the UI which were installed in {@link
 994:    * #installUI}. When finished uninstalling, drops the saved reference to
 995:    * the {@link JList}.
 996:    *
 997:    * @param c Ignored; the UI is uninstalled from the {@link JList}
 998:    * reference saved during the call to {@link #installUI}
 999:    */
1000:   public void uninstallUI(final JComponent c)
1001:   {
1002:     uninstallKeyboardActions();
1003:     uninstallListeners();
1004:     uninstallDefaults();
1005:     list = null;
1006:   }
1007: 
1008:   /**
1009:    * Gets the size this list would prefer to assume. This is calculated by
1010:    * calling {@link #getCellBounds} over the entire list.
1011:    *
1012:    * @param c Ignored; uses the saved {@link JList} reference 
1013:    *
1014:    * @return DOCUMENT ME!
1015:    */
1016:   public Dimension getPreferredSize(JComponent c)
1017:   {
1018:     int size = list.getModel().getSize();
1019:     if (size == 0)
1020:       return new Dimension(0, 0);
1021:     int visibleRows = list.getVisibleRowCount();
1022:     int layoutOrientation = list.getLayoutOrientation();
1023:     Rectangle bounds = getCellBounds(list, 0, list.getModel().getSize() - 1);
1024:     Dimension retVal = bounds.getSize();
1025:     Component parent = list.getParent();
1026:     if ((visibleRows == -1) && (parent instanceof JViewport))
1027:       {
1028:         JViewport viewport = (JViewport) parent;
1029: 
1030:         if (layoutOrientation == JList.HORIZONTAL_WRAP)
1031:           {
1032:             int h = viewport.getSize().height;
1033:             int cellsPerCol = h / cellHeight;
1034:             int w = size / cellsPerCol * cellWidth;
1035:             retVal = new Dimension(w, h);
1036:           }
1037:         else if (layoutOrientation == JList.VERTICAL_WRAP)
1038:           {
1039:             int w = viewport.getSize().width;
1040:             int cellsPerRow = Math.max(w / cellWidth, 1);
1041:             int h = size / cellsPerRow * cellHeight;
1042:             retVal = new Dimension(w, h);
1043:           }
1044:       }
1045:     return retVal;
1046:   }
1047: 
1048:   /**
1049:    * Paints the packground of the list using the background color
1050:    * of the specified component.
1051:    *
1052:    * @param g The graphics context to paint in
1053:    * @param c The component to paint the background of
1054:    */
1055:   private void paintBackground(Graphics g, JComponent c)
1056:   {
1057:     Dimension size = getPreferredSize(c);
1058:     Color save = g.getColor();
1059:     g.setColor(c.getBackground());
1060:     g.fillRect(0, 0, size.width, size.height);
1061:     g.setColor(save);
1062:   }
1063: 
1064:   /**
1065:    * Paints a single cell in the list.
1066:    *
1067:    * @param g The graphics context to paint in
1068:    * @param row The row number to paint
1069:    * @param bounds The bounds of the cell to paint, assuming a coordinate
1070:    * system beginning at <code>(0,0)</code> in the upper left corner of the
1071:    * list
1072:    * @param rend A cell renderer to paint with
1073:    * @param data The data to provide to the cell renderer
1074:    * @param sel A selection model to provide to the cell renderer
1075:    * @param lead The lead selection index of the list
1076:    */
1077:   protected void paintCell(Graphics g, int row, Rectangle bounds,
1078:                  ListCellRenderer rend, ListModel data,
1079:                  ListSelectionModel sel, int lead)
1080:   {
1081:     boolean isSel = list.isSelectedIndex(row);
1082:     boolean hasFocus = (list.getLeadSelectionIndex() == row) && BasicListUI.this.list.hasFocus();
1083:     Component comp = rend.getListCellRendererComponent(list,
1084:                                                        data.getElementAt(row),
1085:                                                        0, isSel, hasFocus);
1086:     //comp.setBounds(new Rectangle(0, 0, bounds.width, bounds.height));
1087:     //comp.paint(g);
1088:     rendererPane.paintComponent(g, comp, list, bounds);
1089:   }
1090: 
1091:   /**
1092:    * Paints the list by calling {@link #paintBackground} and then repeatedly
1093:    * calling {@link #paintCell} for each visible cell in the list.
1094:    *
1095:    * @param g The graphics context to paint with
1096:    * @param c Ignored; uses the saved {@link JList} reference 
1097:    */
1098:   public void paint(Graphics g, JComponent c)
1099:   {
1100:     int nrows = list.getModel().getSize();
1101:     if (nrows == 0)
1102:       return;
1103: 
1104:     maybeUpdateLayoutState();
1105:     ListCellRenderer render = list.getCellRenderer();
1106:     ListModel model = list.getModel();
1107:     ListSelectionModel sel = list.getSelectionModel();
1108:     int lead = sel.getLeadSelectionIndex();
1109:     Rectangle clip = g.getClipBounds();
1110:     paintBackground(g, list);
1111: 
1112:     for (int row = 0; row < nrows; ++row)
1113:       {
1114:         Rectangle bounds = getCellBounds(list, row, row);
1115:         if (bounds.intersects(clip))
1116:           paintCell(g, row, bounds, render, model, sel, lead);
1117:       }
1118:   }
1119: 
1120:   /**
1121:    * Computes the index of a list cell given a point within the list.
1122:    *
1123:    * @param list the list which on which the computation is based on
1124:    * @param location the coordinates
1125:    *
1126:    * @return the index of the list item that is located at the given
1127:    *         coordinates or <code>null</code> if the location is invalid
1128:    */
1129:   public int locationToIndex(JList list, Point location)
1130:   {
1131:     int layoutOrientation = list.getLayoutOrientation();
1132:     int index = -1;
1133:     switch (layoutOrientation)
1134:       {
1135:       case JList.VERTICAL:
1136:         index = convertYToRow(location.y);
1137:         break;
1138:       case JList.HORIZONTAL_WRAP:
1139:         // determine visible rows and cells per row
1140:         int visibleRows = list.getVisibleRowCount();
1141:         int cellsPerRow = -1;
1142:         int numberOfItems = list.getModel().getSize();
1143:         Dimension listDim = list.getSize();
1144:         if (visibleRows <= 0)
1145:           {
1146:             try
1147:               {
1148:                 cellsPerRow = listDim.width / cellWidth;
1149:               }
1150:             catch (ArithmeticException ex)
1151:               {
1152:                 cellsPerRow = 1;
1153:               }
1154:           }
1155:         else
1156:           {
1157:             cellsPerRow = numberOfItems / visibleRows + 1;
1158:           }
1159: 
1160:         // determine index for the given location
1161:         int cellsPerColumn = numberOfItems / cellsPerRow + 1;
1162:         int gridX = Math.min(location.x / cellWidth, cellsPerRow - 1);
1163:         int gridY = Math.min(location.y / cellHeight, cellsPerColumn);
1164:         index = gridX + gridY * cellsPerRow;
1165:         break;
1166:       case JList.VERTICAL_WRAP:
1167:         // determine visible rows and cells per column
1168:         int visibleRows2 = list.getVisibleRowCount();
1169:         if (visibleRows2 <= 0)
1170:           {
1171:             Dimension listDim2 = list.getSize();
1172:             visibleRows2 = listDim2.height / cellHeight;
1173:           }
1174:         int numberOfItems2 = list.getModel().getSize();
1175:         int cellsPerRow2 = numberOfItems2 / visibleRows2 + 1;
1176: 
1177:         Dimension listDim2 = list.getSize();
1178:         int gridX2 = Math.min(location.x / cellWidth, cellsPerRow2 - 1);
1179:         int gridY2 = Math.min(location.y / cellHeight, visibleRows2);
1180:         index = gridY2 + gridX2 * visibleRows2;
1181:         break;
1182:       }
1183:     return index;
1184:   }
1185: 
1186:   public Point indexToLocation(JList list, int index)
1187:   {
1188:     int layoutOrientation = list.getLayoutOrientation();
1189:     Point loc = null;
1190:     switch (layoutOrientation)
1191:       {
1192:       case JList.VERTICAL:
1193:         loc = new Point(0, convertRowToY(index));
1194:         break;
1195:       case JList.HORIZONTAL_WRAP:
1196:         // determine visible rows and cells per row
1197:         int visibleRows = list.getVisibleRowCount();
1198:         int numberOfCellsPerRow = -1;
1199:         if (visibleRows <= 0)
1200:           {
1201:             Dimension listDim = list.getSize();
1202:             numberOfCellsPerRow = Math.max(listDim.width / cellWidth, 1);
1203:           }
1204:         else
1205:           {
1206:             int numberOfItems = list.getModel().getSize();
1207:             numberOfCellsPerRow = numberOfItems / visibleRows + 1;
1208:           }
1209:         // compute coordinates inside the grid
1210:         int gridX = index % numberOfCellsPerRow;
1211:         int gridY = index / numberOfCellsPerRow;
1212:         int locX = gridX * cellWidth;
1213:         int locY = gridY * cellHeight;
1214:         loc = new Point(locX, locY);
1215:         break;
1216:       case JList.VERTICAL_WRAP:
1217:         // determine visible rows and cells per column
1218:         int visibleRows2 = list.getVisibleRowCount();
1219:         if (visibleRows2 <= 0)
1220:           {
1221:             Dimension listDim2 = list.getSize();
1222:             visibleRows2 = listDim2.height / cellHeight;
1223:           }
1224:         // compute coordinates inside the grid
1225:         if (visibleRows2 > 0)
1226:           {
1227:             int gridY2 = index % visibleRows2;
1228:             int gridX2 = index / visibleRows2;
1229:             int locX2 = gridX2 * cellWidth;
1230:             int locY2 = gridY2 * cellHeight;
1231:             loc = new Point(locX2, locY2);
1232:           }
1233:         else
1234:           loc = new Point(0, convertRowToY(index));
1235:         break;
1236:       }
1237:     return loc;
1238:   }
1239: 
1240:   /**
1241:    * Creates and returns the focus listener for this UI.
1242:    *
1243:    * @return the focus listener for this UI
1244:    */
1245:   protected FocusListener createFocusListener()
1246:   {
1247:     return new FocusHandler();
1248:   }
1249: 
1250:   /**
1251:    * Creates and returns the list data listener for this UI.
1252:    *
1253:    * @return the list data listener for this UI
1254:    */
1255:   protected ListDataListener createListDataListener()
1256:   {
1257:     return new ListDataHandler();
1258:   }
1259: 
1260:   /**
1261:    * Creates and returns the list selection listener for this UI.
1262:    *
1263:    * @return the list selection listener for this UI
1264:    */
1265:   protected ListSelectionListener createListSelectionListener()
1266:   {
1267:     return new ListSelectionHandler();
1268:   }
1269: 
1270:   /**
1271:    * Creates and returns the mouse input listener for this UI.
1272:    *
1273:    * @return the mouse input listener for this UI
1274:    */
1275:   protected MouseInputListener createMouseInputListener()
1276:   {
1277:     return new MouseInputHandler();
1278:   }
1279: 
1280:   /**
1281:    * Creates and returns the property change listener for this UI.
1282:    *
1283:    * @return the property change listener for this UI
1284:    */
1285:   protected PropertyChangeListener createPropertyChangeListener()
1286:   {
1287:     return new PropertyChangeHandler();
1288:   }
1289: 
1290:   /**
1291:    * Selects the next list item and force it to be visible.
1292:    */
1293:   protected void selectNextIndex()
1294:   {
1295:     int index = list.getSelectionModel().getLeadSelectionIndex();
1296:     if (index < list.getModel().getSize() - 1)
1297:       {
1298:         index++;
1299:         list.setSelectedIndex(index);
1300:       }
1301:     list.ensureIndexIsVisible(index);
1302:   }
1303: 
1304:   /**
1305:    * Selects the previous list item and force it to be visible.
1306:    */
1307:   protected void selectPreviousIndex()
1308:   {
1309:     int index = list.getSelectionModel().getLeadSelectionIndex();
1310:     if (index > 0)
1311:       {
1312:         index--;
1313:         list.setSelectedIndex(index);
1314:       }
1315:     list.ensureIndexIsVisible(index);
1316:   }
1317: }