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