001    /* BasicListUI.java --
002       Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing.plaf.basic;
040    
041    import java.awt.Component;
042    import java.awt.Dimension;
043    import java.awt.Graphics;
044    import java.awt.Insets;
045    import java.awt.Point;
046    import java.awt.Rectangle;
047    import java.awt.event.ActionEvent;
048    import java.awt.event.ActionListener;
049    import java.awt.event.FocusEvent;
050    import java.awt.event.FocusListener;
051    import java.awt.event.MouseEvent;
052    import java.beans.PropertyChangeEvent;
053    import java.beans.PropertyChangeListener;
054    
055    import javax.swing.AbstractAction;
056    import javax.swing.ActionMap;
057    import javax.swing.CellRendererPane;
058    import javax.swing.DefaultListSelectionModel;
059    import javax.swing.InputMap;
060    import javax.swing.JComponent;
061    import javax.swing.JList;
062    import javax.swing.ListCellRenderer;
063    import javax.swing.ListModel;
064    import javax.swing.ListSelectionModel;
065    import javax.swing.LookAndFeel;
066    import javax.swing.SwingUtilities;
067    import javax.swing.TransferHandler;
068    import javax.swing.UIDefaults;
069    import javax.swing.UIManager;
070    import javax.swing.event.ListDataEvent;
071    import javax.swing.event.ListDataListener;
072    import javax.swing.event.ListSelectionEvent;
073    import javax.swing.event.ListSelectionListener;
074    import javax.swing.event.MouseInputListener;
075    import javax.swing.plaf.ActionMapUIResource;
076    import javax.swing.plaf.ComponentUI;
077    import javax.swing.plaf.ListUI;
078    import javax.swing.plaf.UIResource;
079    
080    /**
081     * The Basic Look and Feel UI delegate for the
082     * JList.
083     */
084    public 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    }