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 }