1 /* BasicListUI.java --
2    Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc.
3 
4 This file is part of GNU Classpath.
5 
6 GNU Classpath is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2, or (at your option)
9 any later version.
10 
11 GNU Classpath is distributed in the hope that it will be useful, but
12 WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with GNU Classpath; see the file COPYING.  If not, write to the
18 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 02110-1301 USA.
20 
21 Linking this library statically or dynamically with other modules is
22 making a combined work based on this library.  Thus, the terms and
23 conditions of the GNU General Public License cover the whole
24 combination.
25 
26 As a special exception, the copyright holders of this library give you
27 permission to link this library with independent modules to produce an
28 executable, regardless of the license terms of these independent
29 modules, and to copy and distribute the resulting executable under
30 terms of your choice, provided that you also meet, for each linked
31 independent module, the terms and conditions of the license of that
32 module.  An independent module is a module which is not derived from
33 or based on this library.  If you modify this library, you may extend
34 this exception to your version of the library, but you are not
35 obligated to do so.  If you do not wish to do so, delete this
36 exception statement from your version. */
37 
38 
39 package javax.swing.plaf.basic;
40 
41 import java.awt.Component;
42 import java.awt.Dimension;
43 import java.awt.Graphics;
44 import java.awt.Insets;
45 import java.awt.Point;
46 import java.awt.Rectangle;
47 import java.awt.event.ActionEvent;
48 import java.awt.event.ActionListener;
49 import java.awt.event.FocusEvent;
50 import java.awt.event.FocusListener;
51 import java.awt.event.MouseEvent;
52 import java.beans.PropertyChangeEvent;
53 import java.beans.PropertyChangeListener;
54 
55 import javax.swing.AbstractAction;
56 import javax.swing.ActionMap;
57 import javax.swing.CellRendererPane;
58 import javax.swing.DefaultListSelectionModel;
59 import javax.swing.InputMap;
60 import javax.swing.JComponent;
61 import javax.swing.JList;
62 import javax.swing.ListCellRenderer;
63 import javax.swing.ListModel;
64 import javax.swing.ListSelectionModel;
65 import javax.swing.LookAndFeel;
66 import javax.swing.SwingUtilities;
67 import javax.swing.TransferHandler;
68 import javax.swing.UIDefaults;
69 import javax.swing.UIManager;
70 import javax.swing.event.ListDataEvent;
71 import javax.swing.event.ListDataListener;
72 import javax.swing.event.ListSelectionEvent;
73 import javax.swing.event.ListSelectionListener;
74 import javax.swing.event.MouseInputListener;
75 import javax.swing.plaf.ActionMapUIResource;
76 import javax.swing.plaf.ComponentUI;
77 import javax.swing.plaf.ListUI;
78 import javax.swing.plaf.UIResource;
79 
80 /**
81  * The Basic Look and Feel UI delegate for the
82  * JList.
83  */
84 public class BasicListUI extends ListUI
85 {
86 
87   /**
88    * A helper class which listens for {@link FocusEvent}s
89    * from the JList.
90    */
91   public class FocusHandler implements FocusListener
92   {
93     /**
94      * Called when the JList acquires focus.
95      *
96      * @param e The FocusEvent representing focus acquisition
97      */
focusGained(FocusEvent e)98     public void focusGained(FocusEvent e)
99     {
100       repaintCellFocus();
101     }
102 
103     /**
104      * Called when the JList loses focus.
105      *
106      * @param e The FocusEvent representing focus loss
107      */
focusLost(FocusEvent e)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      */
repaintCellFocus()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      */
contentsChanged(ListDataEvent e)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      */
intervalAdded(ListDataEvent e)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      */
intervalRemoved(ListDataEvent e)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      */
valueChanged(ListSelectionEvent e)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 
ActionListenerProxy(ActionListener li, String cmd)200     public ActionListenerProxy(ActionListener li,
201                                String cmd)
202     {
203       target = li;
204       bindingCommandName = cmd;
205     }
206 
actionPerformed(ActionEvent e)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      */
ListAction(String cmd)231     ListAction(String cmd)
232     {
233       putValue(ACTION_COMMAND_KEY, cmd);
234     }
235 
actionPerformed(ActionEvent e)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      */
mouseClicked(MouseEvent event)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      */
mousePressed(MouseEvent event)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      */
mouseReleased(MouseEvent event)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      */
mouseEntered(MouseEvent event)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      */
mouseExited(MouseEvent event)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      */
mouseDragged(MouseEvent event)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      */
mouseMoved(MouseEvent event)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      */
propertyChange(PropertyChangeEvent e)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    */
createUI(final JComponent c)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    */
getRowHeight(int row)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    */
getCellBounds(JList l, int index1, int index2)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    */
getCellHeight(int index)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    */
convertRowToY(int row)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    */
convertYToRow(int y0)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    */
updateLayoutState()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    */
maybeUpdateLayoutState()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    */
BasicListUI()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    */
installDefaults()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    */
uninstallDefaults()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    */
installListeners()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    */
uninstallListeners()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    */
installKeyboardActions()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    */
uninstallKeyboardActions()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    */
installUI(final JComponent c)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    */
uninstallUI(final JComponent c)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    */
getPreferredSize(JComponent c)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    */
paintCell(Graphics g, int row, Rectangle bounds, ListCellRenderer rend, ListModel data, ListSelectionModel sel, int lead)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    */
paint(Graphics g, JComponent c)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    */
locationToIndex(JList l, Point location)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 
indexToLocation(JList l, int index)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    */
createFocusListener()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    */
createListDataListener()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    */
createListSelectionListener()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    */
createMouseInputListener()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    */
createPropertyChangeListener()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    */
selectNextIndex()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    */
selectPreviousIndex()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 }
1422