001/* BasicListUI.java --
002   Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.plaf.basic;
040
041import java.awt.Component;
042import java.awt.Dimension;
043import java.awt.Graphics;
044import java.awt.Insets;
045import java.awt.Point;
046import java.awt.Rectangle;
047import java.awt.event.ActionEvent;
048import java.awt.event.ActionListener;
049import java.awt.event.FocusEvent;
050import java.awt.event.FocusListener;
051import java.awt.event.MouseEvent;
052import java.beans.PropertyChangeEvent;
053import java.beans.PropertyChangeListener;
054
055import javax.swing.AbstractAction;
056import javax.swing.ActionMap;
057import javax.swing.CellRendererPane;
058import javax.swing.DefaultListSelectionModel;
059import javax.swing.InputMap;
060import javax.swing.JComponent;
061import javax.swing.JList;
062import javax.swing.ListCellRenderer;
063import javax.swing.ListModel;
064import javax.swing.ListSelectionModel;
065import javax.swing.LookAndFeel;
066import javax.swing.SwingUtilities;
067import javax.swing.TransferHandler;
068import javax.swing.UIDefaults;
069import javax.swing.UIManager;
070import javax.swing.event.ListDataEvent;
071import javax.swing.event.ListDataListener;
072import javax.swing.event.ListSelectionEvent;
073import javax.swing.event.ListSelectionListener;
074import javax.swing.event.MouseInputListener;
075import javax.swing.plaf.ActionMapUIResource;
076import javax.swing.plaf.ComponentUI;
077import javax.swing.plaf.ListUI;
078import javax.swing.plaf.UIResource;
079
080/**
081 * The Basic Look and Feel UI delegate for the
082 * JList.
083 */
084public class BasicListUI extends ListUI
085{
086
087  /**
088   * A helper class which listens for {@link FocusEvent}s
089   * from the JList.
090   */
091  public class FocusHandler implements FocusListener
092  {
093    /**
094     * Called when the JList acquires focus.
095     *
096     * @param e The FocusEvent representing focus acquisition
097     */
098    public void focusGained(FocusEvent e)
099    {
100      repaintCellFocus();
101    }
102
103    /**
104     * Called when the JList loses focus.
105     *
106     * @param e The FocusEvent representing focus loss
107     */
108    public void focusLost(FocusEvent e)
109    {
110      repaintCellFocus();
111    }
112
113    /**
114     * Helper method to repaint the focused cell's
115     * lost or acquired focus state.
116     */
117    protected void repaintCellFocus()
118    {
119      // TODO: Implement this properly.
120    }
121  }
122
123  /**
124   * A helper class which listens for {@link ListDataEvent}s generated by
125   * the {@link JList}'s {@link ListModel}.
126   *
127   * @see javax.swing.JList#getModel()
128   */
129  public class ListDataHandler implements ListDataListener
130  {
131    /**
132     * Called when a general change has happened in the model which cannot
133     * be represented in terms of a simple addition or deletion.
134     *
135     * @param e The event representing the change
136     */
137    public void contentsChanged(ListDataEvent e)
138    {
139      updateLayoutStateNeeded |= modelChanged;
140      list.revalidate();
141    }
142
143    /**
144     * Called when an interval of objects has been added to the model.
145     *
146     * @param e The event representing the addition
147     */
148    public void intervalAdded(ListDataEvent e)
149    {
150      updateLayoutStateNeeded |= modelChanged;
151      list.revalidate();
152    }
153
154    /**
155     * Called when an inteval of objects has been removed from the model.
156     *
157     * @param e The event representing the removal
158     */
159    public void intervalRemoved(ListDataEvent e)
160    {
161      updateLayoutStateNeeded |= modelChanged;
162      list.revalidate();
163    }
164  }
165
166  /**
167   * A helper class which listens for {@link ListSelectionEvent}s
168   * from the {@link JList}'s {@link ListSelectionModel}.
169   */
170  public class ListSelectionHandler implements ListSelectionListener
171  {
172    /**
173     * Called when the list selection changes.
174     *
175     * @param e The event representing the change
176     */
177    public void valueChanged(ListSelectionEvent e)
178    {
179      int index1 = e.getFirstIndex();
180      int index2 = e.getLastIndex();
181      Rectangle damaged = getCellBounds(list, index1, index2);
182      if (damaged != null)
183        list.repaint(damaged);
184    }
185  }
186
187  /**
188   * This class is used to mimmic the behaviour of the JDK when registering
189   * keyboard actions.  It is the same as the private class used in JComponent
190   * for the same reason.  This class receives an action event and dispatches
191   * it to the true receiver after altering the actionCommand property of the
192   * event.
193   */
194  private static class ActionListenerProxy
195    extends AbstractAction
196  {
197    ActionListener target;
198    String bindingCommandName;
199
200    public ActionListenerProxy(ActionListener li,
201                               String cmd)
202    {
203      target = li;
204      bindingCommandName = cmd;
205    }
206
207    public void actionPerformed(ActionEvent e)
208    {
209      ActionEvent derivedEvent = new ActionEvent(e.getSource(),
210                                                 e.getID(),
211                                                 bindingCommandName,
212                                                 e.getModifiers());
213      target.actionPerformed(derivedEvent);
214    }
215  }
216
217  /**
218   * Implements the action for the JList's keyboard commands.
219   */
220  private class ListAction
221    extends AbstractAction
222  {
223    // TODO: Maybe make a couple of classes out of this bulk Action.
224    // Form logical groups of Actions when doing this.
225
226    /**
227     * Creates a new ListAction for the specified command.
228     *
229     * @param cmd the actionCommand to set
230     */
231    ListAction(String cmd)
232    {
233      putValue(ACTION_COMMAND_KEY, cmd);
234    }
235
236    public void actionPerformed(ActionEvent e)
237    {
238      int lead = list.getLeadSelectionIndex();
239      int max = list.getModel().getSize() - 1;
240      DefaultListSelectionModel selModel
241          = (DefaultListSelectionModel) list.getSelectionModel();
242      String command = e.getActionCommand();
243      // Do nothing if list is empty
244      if (max == -1)
245        return;
246
247      if (command.equals("selectNextRow"))
248        {
249          selectNextIndex();
250        }
251      else if (command.equals("selectPreviousRow"))
252        {
253          selectPreviousIndex();
254        }
255      else if (command.equals("clearSelection"))
256        {
257          list.clearSelection();
258        }
259      else if (command.equals("selectAll"))
260        {
261          list.setSelectionInterval(0, max);
262          // this next line is to restore the lead selection index to the old
263          // position, because select-all should not change the lead index
264          list.addSelectionInterval(lead, lead);
265        }
266      else if (command.equals("selectLastRow"))
267        {
268          list.setSelectedIndex(list.getModel().getSize() - 1);
269        }
270      else if (command.equals("selectLastRowChangeLead"))
271        {
272          selModel.moveLeadSelectionIndex(list.getModel().getSize() - 1);
273        }
274      else if (command.equals("scrollDownExtendSelection"))
275        {
276          int target;
277          if (lead == list.getLastVisibleIndex())
278            {
279              target = Math.min(max, lead + (list.getLastVisibleIndex()
280                  - list.getFirstVisibleIndex() + 1));
281            }
282          else
283            target = list.getLastVisibleIndex();
284          selModel.setLeadSelectionIndex(target);
285        }
286      else if (command.equals("scrollDownChangeLead"))
287        {
288          int target;
289          if (lead == list.getLastVisibleIndex())
290            {
291              target = Math.min(max, lead + (list.getLastVisibleIndex()
292                  - list.getFirstVisibleIndex() + 1));
293            }
294          else
295            target = list.getLastVisibleIndex();
296          selModel.moveLeadSelectionIndex(target);
297        }
298      else if (command.equals("scrollUpExtendSelection"))
299        {
300          int target;
301          if (lead == list.getFirstVisibleIndex())
302            {
303              target = Math.max(0, lead - (list.getLastVisibleIndex()
304                  - list.getFirstVisibleIndex() + 1));
305            }
306          else
307            target = list.getFirstVisibleIndex();
308          selModel.setLeadSelectionIndex(target);
309        }
310      else if (command.equals("scrollUpChangeLead"))
311        {
312          int target;
313          if (lead == list.getFirstVisibleIndex())
314            {
315              target = Math.max(0, lead - (list.getLastVisibleIndex()
316                  - list.getFirstVisibleIndex() + 1));
317            }
318          else
319            target = list.getFirstVisibleIndex();
320          selModel.moveLeadSelectionIndex(target);
321        }
322      else if (command.equals("selectNextRowExtendSelection"))
323        {
324          selModel.setLeadSelectionIndex(Math.min(lead + 1, max));
325        }
326      else if (command.equals("selectFirstRow"))
327        {
328          list.setSelectedIndex(0);
329        }
330      else if (command.equals("selectFirstRowChangeLead"))
331        {
332          selModel.moveLeadSelectionIndex(0);
333        }
334      else if (command.equals("selectFirstRowExtendSelection"))
335        {
336          selModel.setLeadSelectionIndex(0);
337        }
338      else if (command.equals("selectPreviousRowExtendSelection"))
339        {
340          selModel.setLeadSelectionIndex(Math.max(0, lead - 1));
341        }
342      else if (command.equals("scrollUp"))
343        {
344          int target;
345          if (lead == list.getFirstVisibleIndex())
346            {
347              target = Math.max(0, lead - (list.getLastVisibleIndex()
348                  - list.getFirstVisibleIndex() + 1));
349            }
350          else
351            target = list.getFirstVisibleIndex();
352          list.setSelectedIndex(target);
353        }
354      else if (command.equals("selectLastRowExtendSelection"))
355        {
356          selModel.setLeadSelectionIndex(list.getModel().getSize() - 1);
357        }
358      else if (command.equals("scrollDown"))
359        {
360          int target;
361          if (lead == list.getLastVisibleIndex())
362            {
363              target = Math.min(max, lead + (list.getLastVisibleIndex()
364                  - list.getFirstVisibleIndex() + 1));
365            }
366          else
367            target = list.getLastVisibleIndex();
368          list.setSelectedIndex(target);
369        }
370      else if (command.equals("selectNextRowChangeLead"))
371          {
372            if (selModel.getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
373              selectNextIndex();
374            else
375              {
376                selModel.moveLeadSelectionIndex(Math.min(max, lead + 1));
377              }
378          }
379      else if (command.equals("selectPreviousRowChangeLead"))
380        {
381          if (selModel.getSelectionMode() != ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
382            selectPreviousIndex();
383          else
384            {
385              selModel.moveLeadSelectionIndex(Math.max(0, lead - 1));
386            }
387        }
388      else if (command.equals("addToSelection"))
389        {
390          list.addSelectionInterval(lead, lead);
391        }
392      else if (command.equals("extendTo"))
393        {
394          selModel.setSelectionInterval(selModel.getAnchorSelectionIndex(),
395                                        lead);
396        }
397      else if (command.equals("toggleAndAnchor"))
398        {
399          if (!list.isSelectedIndex(lead))
400            list.addSelectionInterval(lead, lead);
401          else
402            list.removeSelectionInterval(lead, lead);
403          selModel.setAnchorSelectionIndex(lead);
404        }
405      else
406        {
407          // DEBUG: uncomment the following line to print out
408          // key bindings that aren't implemented yet
409
410          // System.out.println ("not implemented: "+e.getActionCommand());
411        }
412
413      list.ensureIndexIsVisible(list.getLeadSelectionIndex());
414    }
415  }
416
417  /**
418   * A helper class which listens for {@link MouseEvent}s
419   * from the {@link JList}.
420   */
421  public class MouseInputHandler implements MouseInputListener
422  {
423    /**
424     * Called when a mouse button press/release cycle completes
425     * on the {@link JList}
426     *
427     * @param event The event representing the mouse click
428     */
429    public void mouseClicked(MouseEvent event)
430    {
431      Point click = event.getPoint();
432      int index = locationToIndex(list, click);
433      if (index == -1)
434        return;
435      if (event.isShiftDown())
436        {
437          if (list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)
438            list.setSelectedIndex(index);
439          else if (list.getSelectionMode() ==
440                   ListSelectionModel.SINGLE_INTERVAL_SELECTION)
441            // COMPAT: the IBM VM is compatible with the following line of code.
442            // However, compliance with Sun's VM would correspond to replacing
443            // getAnchorSelectionIndex() with getLeadSelectionIndex().This is
444            // both unnatural and contradictory to the way they handle other
445            // similar UI interactions.
446            list.setSelectionInterval(list.getAnchorSelectionIndex(), index);
447          else
448            // COMPAT: both Sun and IBM are compatible instead with:
449            // list.setSelectionInterval
450            //     (list.getLeadSelectionIndex(),index);
451            // Note that for IBM this is contradictory to what they did in
452            // the above situation for SINGLE_INTERVAL_SELECTION.
453            // The most natural thing to do is the following:
454            if (list.isSelectedIndex(list.getAnchorSelectionIndex()))
455              list.getSelectionModel().setLeadSelectionIndex(index);
456            else
457              list.addSelectionInterval(list.getAnchorSelectionIndex(), index);
458        }
459      else if (event.isControlDown())
460        {
461          if (list.getSelectionMode() == ListSelectionModel.SINGLE_SELECTION)
462            list.setSelectedIndex(index);
463          else if (list.isSelectedIndex(index))
464            list.removeSelectionInterval(index, index);
465          else
466            list.addSelectionInterval(index, index);
467        }
468      else
469        list.setSelectedIndex(index);
470
471      list.ensureIndexIsVisible(list.getLeadSelectionIndex());
472    }
473
474    /**
475     * Called when a mouse button is pressed down on the
476     * {@link JList}.
477     *
478     * @param event The event representing the mouse press
479     */
480    public void mousePressed(MouseEvent event)
481    {
482      // We need to explicitly request focus.
483      list.requestFocusInWindow();
484    }
485
486    /**
487     * Called when a mouse button is released on
488     * the {@link JList}
489     *
490     * @param event The event representing the mouse press
491     */
492    public void mouseReleased(MouseEvent event)
493    {
494      // TODO: What should be done here, if anything?
495    }
496
497    /**
498     * Called when the mouse pointer enters the area bounded
499     * by the {@link JList}
500     *
501     * @param event The event representing the mouse entry
502     */
503    public void mouseEntered(MouseEvent event)
504    {
505      // TODO: What should be done here, if anything?
506    }
507
508    /**
509     * Called when the mouse pointer leaves the area bounded
510     * by the {@link JList}
511     *
512     * @param event The event representing the mouse exit
513     */
514    public void mouseExited(MouseEvent event)
515    {
516      // TODO: What should be done here, if anything?
517    }
518
519    /**
520     * Called when the mouse pointer moves over the area bounded
521     * by the {@link JList} while a button is held down.
522     *
523     * @param event The event representing the mouse drag
524     */
525    public void mouseDragged(MouseEvent event)
526    {
527      Point click = event.getPoint();
528      int index = locationToIndex(list, click);
529      if (index == -1)
530        return;
531      if (!event.isShiftDown() && !event.isControlDown())
532        list.setSelectedIndex(index);
533
534      list.ensureIndexIsVisible(list.getLeadSelectionIndex());
535    }
536
537    /**
538     * Called when the mouse pointer moves over the area bounded
539     * by the {@link JList}.
540     *
541     * @param event The event representing the mouse move
542     */
543    public void mouseMoved(MouseEvent event)
544    {
545      // TODO: What should be done here, if anything?
546    }
547  }
548
549  /**
550   * Helper class which listens to {@link PropertyChangeEvent}s
551   * from the {@link JList}.
552   */
553  public class PropertyChangeHandler implements PropertyChangeListener
554  {
555    /**
556     * Called when the {@link JList} changes one of its bound properties.
557     *
558     * @param e The event representing the property change
559     */
560    public void propertyChange(PropertyChangeEvent e)
561    {
562      if (e.getPropertyName().equals("model"))
563        {
564          if (e.getOldValue() != null && e.getOldValue() instanceof ListModel)
565            {
566              ListModel oldModel = (ListModel) e.getOldValue();
567              oldModel.removeListDataListener(listDataListener);
568            }
569          if (e.getNewValue() != null && e.getNewValue() instanceof ListModel)
570            {
571              ListModel newModel = (ListModel) e.getNewValue();
572              newModel.addListDataListener(BasicListUI.this.listDataListener);
573            }
574
575          updateLayoutStateNeeded |= modelChanged;
576        }
577      else if (e.getPropertyName().equals("selectionModel"))
578        updateLayoutStateNeeded |= selectionModelChanged;
579      else if (e.getPropertyName().equals("font"))
580        updateLayoutStateNeeded |= fontChanged;
581      else if (e.getPropertyName().equals("fixedCellWidth"))
582        updateLayoutStateNeeded |= fixedCellWidthChanged;
583      else if (e.getPropertyName().equals("fixedCellHeight"))
584        updateLayoutStateNeeded |= fixedCellHeightChanged;
585      else if (e.getPropertyName().equals("prototypeCellValue"))
586        updateLayoutStateNeeded |= prototypeCellValueChanged;
587      else if (e.getPropertyName().equals("cellRenderer"))
588        updateLayoutStateNeeded |= cellRendererChanged;
589    }
590  }
591
592  /**
593   * A constant to indicate that the model has changed.
594   */
595  protected static final int modelChanged = 1;
596
597  /**
598   * A constant to indicate that the selection model has changed.
599   */
600  protected static final int selectionModelChanged = 2;
601
602  /**
603   * A constant to indicate that the font has changed.
604   */
605  protected static final int fontChanged = 4;
606
607  /**
608   * A constant to indicate that the fixedCellWidth has changed.
609   */
610  protected static final int fixedCellWidthChanged = 8;
611
612  /**
613   * A constant to indicate that the fixedCellHeight has changed.
614   */
615  protected static final int fixedCellHeightChanged = 16;
616
617  /**
618   * A constant to indicate that the prototypeCellValue has changed.
619   */
620  protected static final int prototypeCellValueChanged = 32;
621
622  /**
623   * A constant to indicate that the cellRenderer has changed.
624   */
625  protected static final int cellRendererChanged = 64;
626
627  /**
628   * Creates a new BasicListUI for the component.
629   *
630   * @param c The component to create a UI for
631   *
632   * @return A new UI
633   */
634  public static ComponentUI createUI(final JComponent c)
635  {
636    return new BasicListUI();
637  }
638
639  /** The current focus listener. */
640  protected FocusListener focusListener;
641
642  /** The data listener listening to the model. */
643  protected ListDataListener listDataListener;
644
645  /** The selection listener listening to the selection model. */
646  protected ListSelectionListener listSelectionListener;
647
648  /** The mouse listener listening to the list. */
649  protected MouseInputListener mouseInputListener;
650
651  /** The property change listener listening to the list. */
652  protected PropertyChangeListener propertyChangeListener;
653
654  /** Saved reference to the list this UI was created for. */
655  protected JList list;
656
657  /**
658   * The height of a single cell in the list. This field is used when the
659   * fixedCellHeight property of the list is set. Otherwise this field is
660   * set to <code>-1</code> and {@link #cellHeights} is used instead.
661   */
662  protected int cellHeight;
663
664  /** The width of a single cell in the list. */
665  protected int cellWidth;
666
667  /**
668   * An array of varying heights of cells in the list, in cases where each
669   * cell might have a different height. This field is used when the
670   * <code>fixedCellHeight</code> property of the list is not set. Otherwise
671   * this field is <code>null</code> and {@link #cellHeight} is used.
672   */
673  protected int[] cellHeights;
674
675  /**
676   * A bitmask that indicates which properties of the JList have changed.
677   * When nonzero, indicates that the UI class is out of
678   * date with respect to the underlying list, and must recalculate the
679   * list layout before painting or performing size calculations.
680   *
681   * @see #modelChanged
682   * @see #selectionModelChanged
683   * @see #fontChanged
684   * @see #fixedCellWidthChanged
685   * @see #fixedCellHeightChanged
686   * @see #prototypeCellValueChanged
687   * @see #cellRendererChanged
688   */
689  protected int updateLayoutStateNeeded;
690
691  /**
692   * The {@link CellRendererPane} that is used for painting.
693   */
694  protected CellRendererPane rendererPane;
695
696  /** The action bound to KeyStrokes. */
697  ListAction action;
698
699  /**
700   * Calculate the height of a particular row. If there is a fixed {@link
701   * #cellHeight}, return it; otherwise return the specific row height
702   * requested from the {@link #cellHeights} array. If the requested row
703   * is invalid, return <code>-1</code>.
704   *
705   * @param row The row to get the height of
706   *
707   * @return The height, in pixels, of the specified row
708   */
709  protected int getRowHeight(int row)
710  {
711    int height;
712    if (cellHeights == null)
713      height = cellHeight;
714    else
715      {
716        if (row < 0 || row >= cellHeights.length)
717          height = -1;
718        else
719          height = cellHeights[row];
720      }
721    return height;
722  }
723
724  /**
725   * Calculate the bounds of a particular cell, considering the upper left
726   * corner of the list as the origin position <code>(0,0)</code>.
727   *
728   * @param l Ignored; calculates over <code>this.list</code>
729   * @param index1 The first row to include in the bounds
730   * @param index2 The last row to incude in the bounds
731   *
732   * @return A rectangle encompassing the range of rows between
733   * <code>index1</code> and <code>index2</code> inclusive, or null
734   * such a rectangle couldn't be calculated for the given indexes.
735   */
736  public Rectangle getCellBounds(JList l, int index1, int index2)
737  {
738    maybeUpdateLayoutState();
739
740    if (l != list || cellWidth == -1)
741      return null;
742
743    int minIndex = Math.min(index1, index2);
744    int maxIndex = Math.max(index1, index2);
745    Point loc = indexToLocation(list, minIndex);
746
747    // When the layoutOrientation is VERTICAL, then the width == the list
748    // width. Otherwise the cellWidth field is used.
749    int width = cellWidth;
750    if (l.getLayoutOrientation() == JList.VERTICAL)
751      width = l.getWidth();
752
753    Rectangle bounds = new Rectangle(loc.x, loc.y, width,
754                                     getCellHeight(minIndex));
755    for (int i = minIndex + 1; i <= maxIndex; i++)
756      {
757        Point hiLoc = indexToLocation(list, i);
758        bounds = SwingUtilities.computeUnion(hiLoc.x, hiLoc.y, width,
759                                             getCellHeight(i), bounds);
760      }
761
762    return bounds;
763  }
764
765  /**
766   * Calculates the maximum cell height.
767   *
768   * @param index the index of the cell
769   *
770   * @return the maximum cell height
771   */
772  private int getCellHeight(int index)
773  {
774    int height = cellHeight;
775    if (height <= 0)
776      {
777        if (list.getLayoutOrientation() == JList.VERTICAL)
778          height = getRowHeight(index);
779        else
780          {
781            for (int j = 0; j < cellHeights.length; j++)
782              height = Math.max(height, cellHeights[j]);
783          }
784      }
785    return height;
786  }
787
788  /**
789   * Calculate the Y coordinate of the upper edge of a particular row,
790   * considering the Y coordinate <code>0</code> to occur at the top of the
791   * list.
792   *
793   * @param row The row to calculate the Y coordinate of
794   *
795   * @return The Y coordinate of the specified row, or <code>-1</code> if
796   * the specified row number is invalid
797   */
798  protected int convertRowToY(int row)
799  {
800    int y = 0;
801    for (int i = 0; i < row; ++i)
802      {
803        int h = getRowHeight(i);
804        if (h == -1)
805          return -1;
806        y += h;
807      }
808    return y;
809  }
810
811  /**
812   * Calculate the row number containing a particular Y coordinate,
813   * considering the Y coodrinate <code>0</code> to occur at the top of the
814   * list.
815   *
816   * @param y0 The Y coordinate to calculate the row number for
817   *
818   * @return The row number containing the specified Y value, or <code>-1</code>
819   *         if the list model is empty
820   *
821   * @specnote This method is specified to return -1 for an invalid Y
822   *           coordinate. However, some simple tests show that the behaviour
823   *           is to return the index of the last list element for an Y
824   *           coordinate that lies outside of the list bounds (even for
825   *           negative indices). <code>-1</code>
826   *           is only returned if the list model is empty.
827   */
828  protected int convertYToRow(int y0)
829  {
830    if (list.getModel().getSize() == 0)
831      return -1;
832
833    // When y0 < 0, then the JDK returns the maximum row index of the list. So
834    // do we.
835    if (y0 < 0)
836      return list.getModel().getSize() - 1;
837
838    // Update the layout if necessary.
839    maybeUpdateLayoutState();
840
841    int index = list.getModel().getSize() - 1;
842
843    // If a fixed cell height is set, then we can work more efficient.
844    if (cellHeight > 0)
845      index = Math.min(y0 / cellHeight, index);
846    // If we have no fixed cell height, we must add up each cell height up
847    // to y0.
848    else
849      {
850        int h = 0;
851        for (int row = 0; row < cellHeights.length; ++row)
852          {
853            h += cellHeights[row];
854            if (y0 < h)
855              {
856                index = row;
857                break;
858              }
859          }
860      }
861    return index;
862  }
863
864  /**
865   * Recomputes the {@link #cellHeights}, {@link #cellHeight}, and {@link
866   * #cellWidth} properties by examining the variouis properties of the
867   * {@link JList}.
868   */
869  protected void updateLayoutState()
870  {
871    int nrows = list.getModel().getSize();
872    cellHeight = -1;
873    cellWidth = -1;
874    if (cellHeights == null || cellHeights.length != nrows)
875      cellHeights = new int[nrows];
876    ListCellRenderer rend = list.getCellRenderer();
877    // Update the cellHeight(s) fields.
878    int fixedCellHeight = list.getFixedCellHeight();
879    if (fixedCellHeight > 0)
880      {
881        cellHeight = fixedCellHeight;
882        cellHeights = null;
883      }
884    else
885      {
886        cellHeight = -1;
887        for (int i = 0; i < nrows; ++i)
888          {
889            Component flyweight =
890              rend.getListCellRendererComponent(list,
891                      list.getModel().getElementAt(i),
892                      i, list.isSelectedIndex(i),
893                      list.getSelectionModel().getAnchorSelectionIndex() == i);
894            Dimension dim = flyweight.getPreferredSize();
895            cellHeights[i] = dim.height;
896          }
897      }
898
899    // Update the cellWidth field.
900    int fixedCellWidth = list.getFixedCellWidth();
901    if (fixedCellWidth > 0)
902      cellWidth = fixedCellWidth;
903    else
904      {
905        for (int i = 0; i < nrows; ++i)
906          {
907            Component flyweight =
908              rend.getListCellRendererComponent(list,
909                                                list.getModel().getElementAt(i),
910                                                i, list.isSelectedIndex(i),
911                                                list.getSelectionModel().getAnchorSelectionIndex() == i);
912            Dimension dim = flyweight.getPreferredSize();
913            cellWidth = Math.max(cellWidth, dim.width);
914          }
915      }
916  }
917
918  /**
919   * Calls {@link #updateLayoutState} if {@link #updateLayoutStateNeeded}
920   * is nonzero, then resets {@link #updateLayoutStateNeeded} to zero.
921   */
922  protected void maybeUpdateLayoutState()
923  {
924    if (updateLayoutStateNeeded != 0 || !list.isValid())
925      {
926        updateLayoutState();
927        updateLayoutStateNeeded = 0;
928      }
929  }
930
931  /**
932   * Creates a new BasicListUI object.
933   */
934  public BasicListUI()
935  {
936    updateLayoutStateNeeded = 1;
937    rendererPane = new CellRendererPane();
938  }
939
940  /**
941   * Installs various default settings (mostly colors) from the {@link
942   * UIDefaults} into the {@link JList}
943   *
944   * @see #uninstallDefaults
945   */
946  protected void installDefaults()
947  {
948    LookAndFeel.installColorsAndFont(list, "List.background",
949                                     "List.foreground", "List.font");
950    list.setSelectionForeground(UIManager.getColor("List.selectionForeground"));
951    list.setSelectionBackground(UIManager.getColor("List.selectionBackground"));
952    list.setOpaque(true);
953  }
954
955  /**
956   * Resets to <code>null</code> those defaults which were installed in
957   * {@link #installDefaults}
958   */
959  protected void uninstallDefaults()
960  {
961    list.setForeground(null);
962    list.setBackground(null);
963    list.setSelectionForeground(null);
964    list.setSelectionBackground(null);
965  }
966
967  /**
968   * Attaches all the listeners we have in the UI class to the {@link
969   * JList}, its model and its selection model.
970   *
971   * @see #uninstallListeners
972   */
973  protected void installListeners()
974  {
975    if (focusListener == null)
976      focusListener = createFocusListener();
977    list.addFocusListener(focusListener);
978    if (listDataListener == null)
979      listDataListener = createListDataListener();
980    list.getModel().addListDataListener(listDataListener);
981    if (listSelectionListener == null)
982      listSelectionListener = createListSelectionListener();
983    list.addListSelectionListener(listSelectionListener);
984    if (mouseInputListener == null)
985      mouseInputListener = createMouseInputListener();
986    list.addMouseListener(mouseInputListener);
987    list.addMouseMotionListener(mouseInputListener);
988    if (propertyChangeListener == null)
989      propertyChangeListener = createPropertyChangeListener();
990    list.addPropertyChangeListener(propertyChangeListener);
991  }
992
993  /**
994   * Detaches all the listeners we attached in {@link #installListeners}.
995   */
996  protected void uninstallListeners()
997  {
998    list.removeFocusListener(focusListener);
999    list.getModel().removeListDataListener(listDataListener);
1000    list.removeListSelectionListener(listSelectionListener);
1001    list.removeMouseListener(mouseInputListener);
1002    list.removeMouseMotionListener(mouseInputListener);
1003    list.removePropertyChangeListener(propertyChangeListener);
1004  }
1005
1006  /**
1007   * Installs keyboard actions for this UI in the {@link JList}.
1008   */
1009  protected void installKeyboardActions()
1010  {
1011    // Install UI InputMap.
1012    InputMap focusInputMap = (InputMap) UIManager.get("List.focusInputMap");
1013    SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED,
1014                                     focusInputMap);
1015
1016    // Install UI ActionMap.
1017    ActionMap am = (ActionMap) UIManager.get("List.actionMap");
1018    if (am == null)
1019      {
1020        // Create the actionMap once and store it in the current UIDefaults
1021        // for use in other components.
1022        am = new ActionMapUIResource();
1023        ListAction action;
1024        action = new ListAction("selectPreviousRow");
1025        am.put("selectPreviousRow", action);
1026        action = new ListAction("selectNextRow");
1027        am.put("selectNextRow", action);
1028        action = new ListAction("selectPreviousRowExtendSelection");
1029        am.put("selectPreviousRowExtendSelection", action);
1030        action = new ListAction("selectNextRowExtendSelection");
1031        am.put("selectNextRowExtendSelection", action);
1032
1033        action = new ListAction("selectPreviousColumn");
1034        am.put("selectPreviousColumn", action);
1035        action = new ListAction("selectNextColumn");
1036        am.put("selectNextColumn", action);
1037        action = new ListAction("selectPreviousColumnExtendSelection");
1038        am.put("selectPreviousColumnExtendSelection", action);
1039        action = new ListAction("selectNextColumnExtendSelection");
1040        am.put("selectNextColumnExtendSelection", action);
1041
1042        action = new ListAction("selectFirstRow");
1043        am.put("selectFirstRow", action);
1044        action = new ListAction("selectLastRow");
1045        am.put("selectLastRow", action);
1046        action = new ListAction("selectFirstRowExtendSelection");
1047        am.put("selectFirstRowExtendSelection", action);
1048        action = new ListAction("selectLastRowExtendSelection");
1049        am.put("selectLastRowExtendSelection", action);
1050
1051        action = new ListAction("scrollUp");
1052        am.put("scrollUp", action);
1053        action = new ListAction("scrollUpExtendSelection");
1054        am.put("scrollUpExtendSelection", action);
1055        action = new ListAction("scrollDown");
1056        am.put("scrollDown", action);
1057        action = new ListAction("scrollDownExtendSelection");
1058        am.put("scrollDownExtendSelection", action);
1059
1060        action = new ListAction("selectAll");
1061        am.put("selectAll", action);
1062        action = new ListAction("clearSelection");
1063        am.put("clearSelection", action);
1064
1065        am.put("copy", TransferHandler.getCopyAction());
1066        am.put("cut", TransferHandler.getCutAction());
1067        am.put("paste", TransferHandler.getPasteAction());
1068
1069        UIManager.put("List.actionMap", am);
1070      }
1071
1072    SwingUtilities.replaceUIActionMap(list, am);
1073  }
1074
1075  /**
1076   * Uninstalls keyboard actions for this UI in the {@link JList}.
1077   */
1078  protected void uninstallKeyboardActions()
1079  {
1080    // Uninstall the InputMap.
1081    InputMap im = SwingUtilities.getUIInputMap(list, JComponent.WHEN_FOCUSED);
1082    if (im instanceof UIResource)
1083      SwingUtilities.replaceUIInputMap(list, JComponent.WHEN_FOCUSED, null);
1084
1085    // Uninstall the ActionMap.
1086    if (SwingUtilities.getUIActionMap(list) instanceof UIResource)
1087      SwingUtilities.replaceUIActionMap(list, null);
1088  }
1089
1090  /**
1091   * Installs the various aspects of the UI in the {@link JList}. In
1092   * particular, calls {@link #installDefaults}, {@link #installListeners}
1093   * and {@link #installKeyboardActions}. Also saves a reference to the
1094   * provided component, cast to a {@link JList}.
1095   *
1096   * @param c The {@link JList} to install the UI into
1097   */
1098  public void installUI(final JComponent c)
1099  {
1100    super.installUI(c);
1101    list = (JList) c;
1102    installDefaults();
1103    installListeners();
1104    installKeyboardActions();
1105    maybeUpdateLayoutState();
1106  }
1107
1108  /**
1109   * Uninstalls all the aspects of the UI which were installed in {@link
1110   * #installUI}. When finished uninstalling, drops the saved reference to
1111   * the {@link JList}.
1112   *
1113   * @param c Ignored; the UI is uninstalled from the {@link JList}
1114   * reference saved during the call to {@link #installUI}
1115   */
1116  public void uninstallUI(final JComponent c)
1117  {
1118    uninstallKeyboardActions();
1119    uninstallListeners();
1120    uninstallDefaults();
1121    list = null;
1122  }
1123
1124  /**
1125   * Gets the size this list would prefer to assume. This is calculated by
1126   * calling {@link #getCellBounds} over the entire list.
1127   *
1128   * @param c Ignored; uses the saved {@link JList} reference
1129   *
1130   * @return DOCUMENT ME!
1131   */
1132  public Dimension getPreferredSize(JComponent c)
1133  {
1134    maybeUpdateLayoutState();
1135    int size = list.getModel().getSize();
1136    int visibleRows = list.getVisibleRowCount();
1137    int layoutOrientation = list.getLayoutOrientation();
1138
1139    int h;
1140    int w;
1141    int maxCellHeight = cellHeight;
1142    if (maxCellHeight <= 0)
1143      {
1144        for (int i = 0; i < cellHeights.length; i++)
1145          maxCellHeight = Math.max(maxCellHeight, cellHeights[i]);
1146      }
1147    if (layoutOrientation == JList.HORIZONTAL_WRAP)
1148      {
1149        if (visibleRows > 0)
1150          {
1151            // We cast to double here to force double divisions.
1152            double modelSize = size;
1153            int neededColumns = (int) Math.ceil(modelSize / visibleRows);
1154            int adjustedRows = (int) Math.ceil(modelSize / neededColumns);
1155            h = maxCellHeight * adjustedRows;
1156            w = cellWidth * neededColumns;
1157          }
1158        else
1159          {
1160            int neededColumns = Math.min(1, list.getWidth() / cellWidth);
1161            h = size / neededColumns * maxCellHeight;
1162            w = neededColumns * cellWidth;
1163          }
1164      }
1165    else if (layoutOrientation == JList.VERTICAL_WRAP)
1166      {
1167        if (visibleRows > 0)
1168          h = visibleRows * maxCellHeight;
1169        else
1170          h = Math.max(list.getHeight(), maxCellHeight);
1171        int neededColumns = h / maxCellHeight;
1172        w = cellWidth * neededColumns;
1173      }
1174    else
1175      {
1176        if (list.getFixedCellWidth() > 0)
1177          w = list.getFixedCellWidth();
1178        else
1179          w = cellWidth;
1180        if (list.getFixedCellHeight() > 0)
1181          // FIXME: We need to add some cellVerticalMargins here, according
1182          // to the specs.
1183          h = list.getFixedCellHeight() * size;
1184        else
1185          h = maxCellHeight * size;
1186      }
1187    Insets insets = list.getInsets();
1188    Dimension retVal = new Dimension(w + insets.left + insets.right,
1189                                     h + insets.top + insets.bottom);
1190    return retVal;
1191  }
1192
1193  /**
1194   * Paints a single cell in the list.
1195   *
1196   * @param g The graphics context to paint in
1197   * @param row The row number to paint
1198   * @param bounds The bounds of the cell to paint, assuming a coordinate
1199   * system beginning at <code>(0,0)</code> in the upper left corner of the
1200   * list
1201   * @param rend A cell renderer to paint with
1202   * @param data The data to provide to the cell renderer
1203   * @param sel A selection model to provide to the cell renderer
1204   * @param lead The lead selection index of the list
1205   */
1206  protected void paintCell(Graphics g, int row, Rectangle bounds,
1207                 ListCellRenderer rend, ListModel data,
1208                 ListSelectionModel sel, int lead)
1209  {
1210    boolean isSel = list.isSelectedIndex(row);
1211    boolean hasFocus = (list.getLeadSelectionIndex() == row) && BasicListUI.this.list.hasFocus();
1212    Component comp = rend.getListCellRendererComponent(list,
1213                                                       data.getElementAt(row),
1214                                                       row, isSel, hasFocus);
1215    rendererPane.paintComponent(g, comp, list, bounds);
1216  }
1217
1218  /**
1219   * Paints the list by repeatedly calling {@link #paintCell} for each visible
1220   * cell in the list.
1221   *
1222   * @param g The graphics context to paint with
1223   * @param c Ignored; uses the saved {@link JList} reference
1224   */
1225  public void paint(Graphics g, JComponent c)
1226  {
1227    int nrows = list.getModel().getSize();
1228    if (nrows == 0)
1229      return;
1230
1231    maybeUpdateLayoutState();
1232    ListCellRenderer render = list.getCellRenderer();
1233    ListModel model = list.getModel();
1234    ListSelectionModel sel = list.getSelectionModel();
1235    int lead = sel.getLeadSelectionIndex();
1236    Rectangle clip = g.getClipBounds();
1237
1238    int startIndex = locationToIndex(list, new Point(clip.x, clip.y));
1239    int endIndex = locationToIndex(list, new Point(clip.x + clip.width,
1240                                             clip.y + clip.height));
1241
1242    for (int row = startIndex; row <= endIndex; ++row)
1243      {
1244        Rectangle bounds = getCellBounds(list, row, row);
1245        if (bounds != null && bounds.intersects(clip))
1246          paintCell(g, row, bounds, render, model, sel, lead);
1247      }
1248  }
1249
1250  /**
1251   * Computes the index of a list cell given a point within the list. If the
1252   * location lies outside the bounds of the list, the greatest index in the
1253   * list model is returned.
1254   *
1255   * @param l the list which on which the computation is based on
1256   * @param location the coordinates
1257   *
1258   * @return the index of the list item that is located at the given
1259   *         coordinates or <code>-1</code> if the list model is empty
1260   */
1261  public int locationToIndex(JList l, Point location)
1262  {
1263    int layoutOrientation = list.getLayoutOrientation();
1264    int index = -1;
1265    switch (layoutOrientation)
1266      {
1267      case JList.VERTICAL:
1268        index = convertYToRow(location.y);
1269        break;
1270      case JList.HORIZONTAL_WRAP:
1271        // determine visible rows and cells per row
1272        int maxCellHeight = getCellHeight(0);
1273        int visibleRows = list.getHeight() / maxCellHeight;
1274        int cellsPerRow = -1;
1275        int numberOfItems = list.getModel().getSize();
1276        cellsPerRow = numberOfItems / visibleRows + 1;
1277
1278        // determine index for the given location
1279        int cellsPerColumn = numberOfItems / cellsPerRow + 1;
1280        int gridX = Math.min(location.x / cellWidth, cellsPerRow - 1);
1281        int gridY = Math.min(location.y / maxCellHeight, cellsPerColumn);
1282        index = gridX + gridY * cellsPerRow;
1283        break;
1284      case JList.VERTICAL_WRAP:
1285        // determine visible rows and cells per column
1286        int maxCellHeight2 = getCellHeight(0);
1287        int visibleRows2 = list.getHeight() / maxCellHeight2;
1288        int numberOfItems2 = list.getModel().getSize();
1289        int cellsPerRow2 = numberOfItems2 / visibleRows2 + 1;
1290
1291        int gridX2 = Math.min(location.x / cellWidth, cellsPerRow2 - 1);
1292        int gridY2 = Math.min(location.y / maxCellHeight2, visibleRows2);
1293        index = gridY2 + gridX2 * visibleRows2;
1294        break;
1295      }
1296    return index;
1297  }
1298
1299  public Point indexToLocation(JList l, int index)
1300  {
1301    int layoutOrientation = list.getLayoutOrientation();
1302    Point loc = null;
1303    switch (layoutOrientation)
1304      {
1305      case JList.VERTICAL:
1306        loc = new Point(0, convertRowToY(index));
1307        break;
1308      case JList.HORIZONTAL_WRAP:
1309        // determine visible rows and cells per row
1310        int maxCellHeight = getCellHeight(0);
1311        int visibleRows = list.getHeight() / maxCellHeight;
1312        int numberOfCellsPerRow = -1;
1313        int numberOfItems = list.getModel().getSize();
1314        numberOfCellsPerRow = numberOfItems / visibleRows + 1;
1315
1316        // compute coordinates inside the grid
1317        int gridX = index % numberOfCellsPerRow;
1318        int gridY = index / numberOfCellsPerRow;
1319        int locX = gridX * cellWidth;
1320        int locY;
1321        locY = gridY * maxCellHeight;
1322        loc = new Point(locX, locY);
1323        break;
1324      case JList.VERTICAL_WRAP:
1325        // determine visible rows and cells per column
1326        int maxCellHeight2 = getCellHeight(0);
1327        int visibleRows2 = list.getHeight() / maxCellHeight2;
1328        // compute coordinates inside the grid
1329        if (visibleRows2 > 0)
1330          {
1331            int gridY2 = index % visibleRows2;
1332            int gridX2 = index / visibleRows2;
1333            int locX2 = gridX2 * cellWidth;
1334            int locY2 = gridY2 * maxCellHeight2;
1335            loc = new Point(locX2, locY2);
1336          }
1337        else
1338          loc = new Point(0, convertRowToY(index));
1339        break;
1340      }
1341    return loc;
1342  }
1343
1344  /**
1345   * Creates and returns the focus listener for this UI.
1346   *
1347   * @return the focus listener for this UI
1348   */
1349  protected FocusListener createFocusListener()
1350  {
1351    return new FocusHandler();
1352  }
1353
1354  /**
1355   * Creates and returns the list data listener for this UI.
1356   *
1357   * @return the list data listener for this UI
1358   */
1359  protected ListDataListener createListDataListener()
1360  {
1361    return new ListDataHandler();
1362  }
1363
1364  /**
1365   * Creates and returns the list selection listener for this UI.
1366   *
1367   * @return the list selection listener for this UI
1368   */
1369  protected ListSelectionListener createListSelectionListener()
1370  {
1371    return new ListSelectionHandler();
1372  }
1373
1374  /**
1375   * Creates and returns the mouse input listener for this UI.
1376   *
1377   * @return the mouse input listener for this UI
1378   */
1379  protected MouseInputListener createMouseInputListener()
1380  {
1381    return new MouseInputHandler();
1382  }
1383
1384  /**
1385   * Creates and returns the property change listener for this UI.
1386   *
1387   * @return the property change listener for this UI
1388   */
1389  protected PropertyChangeListener createPropertyChangeListener()
1390  {
1391    return new PropertyChangeHandler();
1392  }
1393
1394  /**
1395   * Selects the next list item and force it to be visible.
1396   */
1397  protected void selectNextIndex()
1398  {
1399    int index = list.getSelectionModel().getLeadSelectionIndex();
1400    if (index < list.getModel().getSize() - 1)
1401      {
1402        index++;
1403        list.setSelectedIndex(index);
1404      }
1405    list.ensureIndexIsVisible(index);
1406  }
1407
1408  /**
1409   * Selects the previous list item and force it to be visible.
1410   */
1411  protected void selectPreviousIndex()
1412  {
1413    int index = list.getSelectionModel().getLeadSelectionIndex();
1414    if (index > 0)
1415      {
1416        index--;
1417        list.setSelectedIndex(index);
1418      }
1419    list.ensureIndexIsVisible(index);
1420  }
1421}