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