1 /* 2 * Copyright (c) 1998, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javax.swing.plaf.basic; 27 28 import javax.accessibility.AccessibleContext; 29 import javax.swing.*; 30 import javax.swing.border.Border; 31 import javax.swing.border.LineBorder; 32 import javax.swing.event.*; 33 import java.awt.*; 34 import java.awt.event.*; 35 import java.beans.PropertyChangeListener; 36 import java.beans.PropertyChangeEvent; 37 import java.io.Serializable; 38 39 40 /** 41 * This is a basic implementation of the <code>ComboPopup</code> interface. 42 * 43 * This class represents the ui for the popup portion of the combo box. 44 * <p> 45 * All event handling is handled by listener classes created with the 46 * <code>createxxxListener()</code> methods and internal classes. 47 * You can change the behavior of this class by overriding the 48 * <code>createxxxListener()</code> methods and supplying your own 49 * event listeners or subclassing from the ones supplied in this class. 50 * <p> 51 * <strong>Warning:</strong> 52 * Serialized objects of this class will not be compatible with 53 * future Swing releases. The current serialization support is 54 * appropriate for short term storage or RMI between applications running 55 * the same version of Swing. As of 1.4, support for long term storage 56 * of all JavaBeans™ 57 * has been added to the <code>java.beans</code> package. 58 * Please see {@link java.beans.XMLEncoder}. 59 * 60 * @author Tom Santos 61 * @author Mark Davidson 62 */ 63 @SuppressWarnings("serial") // Same-version serialization only 64 public class BasicComboPopup extends JPopupMenu implements ComboPopup { 65 // An empty ListMode, this is used when the UI changes to allow 66 // the JList to be gc'ed. 67 private static class EmptyListModelClass implements ListModel<Object>, Serializable { 68 public int getSize() { return 0; } 69 public Object getElementAt(int index) { return null; } 70 public void addListDataListener(ListDataListener l) {} 71 public void removeListDataListener(ListDataListener l) {} 72 }; 73 74 static final ListModel<Object> EmptyListModel = new EmptyListModelClass(); 75 76 private static Border LIST_BORDER = new LineBorder(Color.BLACK, 1); 77 78 /** 79 * The instance of {@code JComboBox}. 80 */ 81 protected JComboBox<Object> comboBox; 82 /** 83 * This protected field is implementation specific. Do not access directly 84 * or override. Use the accessor methods instead. 85 * 86 * @see #getList 87 * @see #createList 88 */ 89 protected JList<Object> list; 90 /** 91 * This protected field is implementation specific. Do not access directly 92 * or override. Use the create method instead 93 * 94 * @see #createScroller 95 */ 96 protected JScrollPane scroller; 97 98 /** 99 * As of Java 2 platform v1.4 this previously undocumented field is no 100 * longer used. 101 */ 102 protected boolean valueIsAdjusting = false; 103 104 // Listeners that are required by the ComboPopup interface 105 106 /** 107 * Implementation of all the listener classes. 108 */ 109 private Handler handler; 110 111 /** 112 * This protected field is implementation specific. Do not access directly 113 * or override. Use the accessor or create methods instead. 114 * 115 * @see #getMouseMotionListener 116 * @see #createMouseMotionListener 117 */ 118 protected MouseMotionListener mouseMotionListener; 119 /** 120 * This protected field is implementation specific. Do not access directly 121 * or override. Use the accessor or create methods instead. 122 * 123 * @see #getMouseListener 124 * @see #createMouseListener 125 */ 126 protected MouseListener mouseListener; 127 128 /** 129 * This protected field is implementation specific. Do not access directly 130 * or override. Use the accessor or create methods instead. 131 * 132 * @see #getKeyListener 133 * @see #createKeyListener 134 */ 135 protected KeyListener keyListener; 136 137 /** 138 * This protected field is implementation specific. Do not access directly 139 * or override. Use the create method instead. 140 * 141 * @see #createListSelectionListener 142 */ 143 protected ListSelectionListener listSelectionListener; 144 145 // Listeners that are attached to the list 146 /** 147 * This protected field is implementation specific. Do not access directly 148 * or override. Use the create method instead. 149 * 150 * @see #createListMouseListener 151 */ 152 protected MouseListener listMouseListener; 153 /** 154 * This protected field is implementation specific. Do not access directly 155 * or override. Use the create method instead 156 * 157 * @see #createListMouseMotionListener 158 */ 159 protected MouseMotionListener listMouseMotionListener; 160 161 // Added to the combo box for bound properties 162 /** 163 * This protected field is implementation specific. Do not access directly 164 * or override. Use the create method instead 165 * 166 * @see #createPropertyChangeListener 167 */ 168 protected PropertyChangeListener propertyChangeListener; 169 170 // Added to the combo box model 171 /** 172 * This protected field is implementation specific. Do not access directly 173 * or override. Use the create method instead 174 * 175 * @see #createListDataListener 176 */ 177 protected ListDataListener listDataListener; 178 179 /** 180 * This protected field is implementation specific. Do not access directly 181 * or override. Use the create method instead 182 * 183 * @see #createItemListener 184 */ 185 protected ItemListener itemListener; 186 187 /** 188 * This protected field is implementation specific. Do not access directly 189 * or override. 190 */ 191 protected Timer autoscrollTimer; 192 193 /** 194 * {@code true} if the mouse cursor is in the popup. 195 */ 196 protected boolean hasEntered = false; 197 198 /** 199 * If {@code true} the auto-scrolling is enabled. 200 */ 201 protected boolean isAutoScrolling = false; 202 203 /** 204 * The direction of scrolling. 205 */ 206 protected int scrollDirection = SCROLL_UP; 207 208 /** 209 * The direction of scrolling up. 210 */ 211 protected static final int SCROLL_UP = 0; 212 213 /** 214 * The direction of scrolling down. 215 */ 216 protected static final int SCROLL_DOWN = 1; 217 218 219 //======================================== 220 // begin ComboPopup method implementations 221 // 222 223 /** 224 * Implementation of ComboPopup.show(). 225 */ 226 public void show() { 227 comboBox.firePopupMenuWillBecomeVisible(); 228 setListSelection(comboBox.getSelectedIndex()); 229 Point location = getPopupLocation(); 230 show( comboBox, location.x, location.y ); 231 } 232 233 234 /** 235 * Implementation of ComboPopup.hide(). 236 */ 237 public void hide() { 238 MenuSelectionManager manager = MenuSelectionManager.defaultManager(); 239 MenuElement [] selection = manager.getSelectedPath(); 240 for ( int i = 0 ; i < selection.length ; i++ ) { 241 if ( selection[i] == this ) { 242 manager.clearSelectedPath(); 243 break; 244 } 245 } 246 if (selection.length > 0) { 247 comboBox.repaint(); 248 } 249 } 250 251 /** 252 * Implementation of ComboPopup.getList(). 253 */ 254 public JList<Object> getList() { 255 return list; 256 } 257 258 /** 259 * Implementation of ComboPopup.getMouseListener(). 260 * 261 * @return a <code>MouseListener</code> or null 262 * @see ComboPopup#getMouseListener 263 */ 264 public MouseListener getMouseListener() { 265 if (mouseListener == null) { 266 mouseListener = createMouseListener(); 267 } 268 return mouseListener; 269 } 270 271 /** 272 * Implementation of ComboPopup.getMouseMotionListener(). 273 * 274 * @return a <code>MouseMotionListener</code> or null 275 * @see ComboPopup#getMouseMotionListener 276 */ 277 public MouseMotionListener getMouseMotionListener() { 278 if (mouseMotionListener == null) { 279 mouseMotionListener = createMouseMotionListener(); 280 } 281 return mouseMotionListener; 282 } 283 284 /** 285 * Implementation of ComboPopup.getKeyListener(). 286 * 287 * @return a <code>KeyListener</code> or null 288 * @see ComboPopup#getKeyListener 289 */ 290 public KeyListener getKeyListener() { 291 if (keyListener == null) { 292 keyListener = createKeyListener(); 293 } 294 return keyListener; 295 } 296 297 /** 298 * Called when the UI is uninstalling. Since this popup isn't in the component 299 * tree, it won't get it's uninstallUI() called. It removes the listeners that 300 * were added in addComboBoxListeners(). 301 */ 302 public void uninstallingUI() { 303 if (propertyChangeListener != null) { 304 comboBox.removePropertyChangeListener( propertyChangeListener ); 305 } 306 if (itemListener != null) { 307 comboBox.removeItemListener( itemListener ); 308 } 309 uninstallComboBoxModelListeners(comboBox.getModel()); 310 uninstallKeyboardActions(); 311 uninstallListListeners(); 312 // We do this, otherwise the listener the ui installs on 313 // the model (the combobox model in this case) will keep a 314 // reference to the list, causing the list (and us) to never get gced. 315 list.setModel(EmptyListModel); 316 } 317 318 // 319 // end ComboPopup method implementations 320 //====================================== 321 322 /** 323 * Removes the listeners from the combo box model 324 * 325 * @param model The combo box model to install listeners 326 * @see #installComboBoxModelListeners 327 */ 328 protected void uninstallComboBoxModelListeners( ComboBoxModel<?> model ) { 329 if (model != null && listDataListener != null) { 330 model.removeListDataListener(listDataListener); 331 } 332 } 333 334 /** 335 * Unregisters keyboard actions. 336 */ 337 protected void uninstallKeyboardActions() { 338 // XXX - shouldn't call this method 339 // comboBox.unregisterKeyboardAction( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ) ); 340 } 341 342 343 344 //=================================================================== 345 // begin Initialization routines 346 // 347 348 /** 349 * Constructs a new instance of {@code BasicComboPopup}. 350 * 351 * @param combo an instance of {@code JComboBox} 352 */ 353 public BasicComboPopup( JComboBox<Object> combo ) { 354 super(); 355 setName("ComboPopup.popup"); 356 comboBox = combo; 357 358 setLightWeightPopupEnabled( comboBox.isLightWeightPopupEnabled() ); 359 360 // UI construction of the popup. 361 list = createList(); 362 list.setName("ComboBox.list"); 363 configureList(); 364 scroller = createScroller(); 365 scroller.setName("ComboBox.scrollPane"); 366 configureScroller(); 367 configurePopup(); 368 369 installComboBoxListeners(); 370 installKeyboardActions(); 371 } 372 373 // Overriden PopupMenuListener notification methods to inform combo box 374 // PopupMenuListeners. 375 376 protected void firePopupMenuWillBecomeVisible() { 377 super.firePopupMenuWillBecomeVisible(); 378 // comboBox.firePopupMenuWillBecomeVisible() is called from BasicComboPopup.show() method 379 // to let the user change the popup menu from the PopupMenuListener.popupMenuWillBecomeVisible() 380 } 381 382 protected void firePopupMenuWillBecomeInvisible() { 383 super.firePopupMenuWillBecomeInvisible(); 384 comboBox.firePopupMenuWillBecomeInvisible(); 385 } 386 387 protected void firePopupMenuCanceled() { 388 super.firePopupMenuCanceled(); 389 comboBox.firePopupMenuCanceled(); 390 } 391 392 /** 393 * Creates a listener 394 * that will watch for mouse-press and release events on the combo box. 395 * 396 * <strong>Warning:</strong> 397 * When overriding this method, make sure to maintain the existing 398 * behavior. 399 * 400 * @return a <code>MouseListener</code> which will be added to 401 * the combo box or null 402 */ 403 protected MouseListener createMouseListener() { 404 return getHandler(); 405 } 406 407 /** 408 * Creates the mouse motion listener which will be added to the combo 409 * box. 410 * 411 * <strong>Warning:</strong> 412 * When overriding this method, make sure to maintain the existing 413 * behavior. 414 * 415 * @return a <code>MouseMotionListener</code> which will be added to 416 * the combo box or null 417 */ 418 protected MouseMotionListener createMouseMotionListener() { 419 return getHandler(); 420 } 421 422 /** 423 * Creates the key listener that will be added to the combo box. If 424 * this method returns null then it will not be added to the combo box. 425 * 426 * @return a <code>KeyListener</code> or null 427 */ 428 protected KeyListener createKeyListener() { 429 return null; 430 } 431 432 /** 433 * Creates a list selection listener that watches for selection changes in 434 * the popup's list. If this method returns null then it will not 435 * be added to the popup list. 436 * 437 * @return an instance of a <code>ListSelectionListener</code> or null 438 */ 439 protected ListSelectionListener createListSelectionListener() { 440 return null; 441 } 442 443 /** 444 * Creates a list data listener which will be added to the 445 * <code>ComboBoxModel</code>. If this method returns null then 446 * it will not be added to the combo box model. 447 * 448 * @return an instance of a <code>ListDataListener</code> or null 449 */ 450 protected ListDataListener createListDataListener() { 451 return null; 452 } 453 454 /** 455 * Creates a mouse listener that watches for mouse events in 456 * the popup's list. If this method returns null then it will 457 * not be added to the combo box. 458 * 459 * @return an instance of a <code>MouseListener</code> or null 460 */ 461 protected MouseListener createListMouseListener() { 462 return getHandler(); 463 } 464 465 /** 466 * Creates a mouse motion listener that watches for mouse motion 467 * events in the popup's list. If this method returns null then it will 468 * not be added to the combo box. 469 * 470 * @return an instance of a <code>MouseMotionListener</code> or null 471 */ 472 protected MouseMotionListener createListMouseMotionListener() { 473 return getHandler(); 474 } 475 476 /** 477 * Creates a <code>PropertyChangeListener</code> which will be added to 478 * the combo box. If this method returns null then it will not 479 * be added to the combo box. 480 * 481 * @return an instance of a <code>PropertyChangeListener</code> or null 482 */ 483 protected PropertyChangeListener createPropertyChangeListener() { 484 return getHandler(); 485 } 486 487 /** 488 * Creates an <code>ItemListener</code> which will be added to the 489 * combo box. If this method returns null then it will not 490 * be added to the combo box. 491 * <p> 492 * Subclasses may override this method to return instances of their own 493 * ItemEvent handlers. 494 * 495 * @return an instance of an <code>ItemListener</code> or null 496 */ 497 protected ItemListener createItemListener() { 498 return getHandler(); 499 } 500 501 private Handler getHandler() { 502 if (handler == null) { 503 handler = new Handler(); 504 } 505 return handler; 506 } 507 508 /** 509 * Creates the JList used in the popup to display 510 * the items in the combo box model. This method is called when the UI class 511 * is created. 512 * 513 * @return a <code>JList</code> used to display the combo box items 514 */ 515 protected JList<Object> createList() { 516 return new JList<Object>( comboBox.getModel() ) { 517 public void processMouseEvent(MouseEvent e) { 518 if (BasicGraphicsUtils.isMenuShortcutKeyDown(e)) { 519 // Fix for 4234053. Filter out the Control Key from the list. 520 // ie., don't allow CTRL key deselection. 521 Toolkit toolkit = Toolkit.getDefaultToolkit(); 522 e = new MouseEvent((Component)e.getSource(), e.getID(), e.getWhen(), 523 e.getModifiers() ^ toolkit.getMenuShortcutKeyMask(), 524 e.getX(), e.getY(), 525 e.getXOnScreen(), e.getYOnScreen(), 526 e.getClickCount(), 527 e.isPopupTrigger(), 528 MouseEvent.NOBUTTON); 529 } 530 super.processMouseEvent(e); 531 } 532 }; 533 } 534 535 /** 536 * Configures the list which is used to hold the combo box items in the 537 * popup. This method is called when the UI class 538 * is created. 539 * 540 * @see #createList 541 */ 542 protected void configureList() { 543 list.setFont( comboBox.getFont() ); 544 list.setForeground( comboBox.getForeground() ); 545 list.setBackground( comboBox.getBackground() ); 546 list.setSelectionForeground( UIManager.getColor( "ComboBox.selectionForeground" ) ); 547 list.setSelectionBackground( UIManager.getColor( "ComboBox.selectionBackground" ) ); 548 list.setBorder( null ); 549 list.setCellRenderer( comboBox.getRenderer() ); 550 list.setFocusable( false ); 551 list.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); 552 setListSelection( comboBox.getSelectedIndex() ); 553 installListListeners(); 554 } 555 556 /** 557 * Adds the listeners to the list control. 558 */ 559 protected void installListListeners() { 560 if ((listMouseListener = createListMouseListener()) != null) { 561 list.addMouseListener( listMouseListener ); 562 } 563 if ((listMouseMotionListener = createListMouseMotionListener()) != null) { 564 list.addMouseMotionListener( listMouseMotionListener ); 565 } 566 if ((listSelectionListener = createListSelectionListener()) != null) { 567 list.addListSelectionListener( listSelectionListener ); 568 } 569 } 570 571 void uninstallListListeners() { 572 if (listMouseListener != null) { 573 list.removeMouseListener(listMouseListener); 574 listMouseListener = null; 575 } 576 if (listMouseMotionListener != null) { 577 list.removeMouseMotionListener(listMouseMotionListener); 578 listMouseMotionListener = null; 579 } 580 if (listSelectionListener != null) { 581 list.removeListSelectionListener(listSelectionListener); 582 listSelectionListener = null; 583 } 584 handler = null; 585 } 586 587 /** 588 * Creates the scroll pane which houses the scrollable list. 589 * 590 * @return the scroll pane which houses the scrollable list 591 */ 592 protected JScrollPane createScroller() { 593 JScrollPane sp = new JScrollPane( list, 594 ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 595 ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); 596 sp.setHorizontalScrollBar(null); 597 return sp; 598 } 599 600 /** 601 * Configures the scrollable portion which holds the list within 602 * the combo box popup. This method is called when the UI class 603 * is created. 604 */ 605 protected void configureScroller() { 606 scroller.setFocusable( false ); 607 scroller.getVerticalScrollBar().setFocusable( false ); 608 scroller.setBorder( null ); 609 } 610 611 /** 612 * Configures the popup portion of the combo box. This method is called 613 * when the UI class is created. 614 */ 615 protected void configurePopup() { 616 setLayout( new BoxLayout( this, BoxLayout.Y_AXIS ) ); 617 setBorderPainted( true ); 618 setBorder(LIST_BORDER); 619 setOpaque( false ); 620 add( scroller ); 621 setDoubleBuffered( true ); 622 setFocusable( false ); 623 } 624 625 /** 626 * This method adds the necessary listeners to the JComboBox. 627 */ 628 protected void installComboBoxListeners() { 629 if ((propertyChangeListener = createPropertyChangeListener()) != null) { 630 comboBox.addPropertyChangeListener(propertyChangeListener); 631 } 632 if ((itemListener = createItemListener()) != null) { 633 comboBox.addItemListener(itemListener); 634 } 635 installComboBoxModelListeners(comboBox.getModel()); 636 } 637 638 /** 639 * Installs the listeners on the combo box model. Any listeners installed 640 * on the combo box model should be removed in 641 * <code>uninstallComboBoxModelListeners</code>. 642 * 643 * @param model The combo box model to install listeners 644 * @see #uninstallComboBoxModelListeners 645 */ 646 protected void installComboBoxModelListeners( ComboBoxModel<?> model ) { 647 if (model != null && (listDataListener = createListDataListener()) != null) { 648 model.addListDataListener(listDataListener); 649 } 650 } 651 652 /** 653 * Registers keyboard actions. 654 */ 655 protected void installKeyboardActions() { 656 657 /* XXX - shouldn't call this method. take it out for testing. 658 ActionListener action = new ActionListener() { 659 public void actionPerformed(ActionEvent e){ 660 } 661 }; 662 663 comboBox.registerKeyboardAction( action, 664 KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), 665 JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT ); */ 666 667 } 668 669 // 670 // end Initialization routines 671 //================================================================= 672 673 674 //=================================================================== 675 // begin Event Listenters 676 // 677 678 /** 679 * A listener to be registered upon the combo box 680 * (<em>not</em> its popup menu) 681 * to handle mouse events 682 * that affect the state of the popup menu. 683 * The main purpose of this listener is to make the popup menu 684 * appear and disappear. 685 * This listener also helps 686 * with click-and-drag scenarios by setting the selection if the mouse was 687 * released over the list during a drag. 688 * 689 * <p> 690 * <strong>Warning:</strong> 691 * We recommend that you <em>not</em> 692 * create subclasses of this class. 693 * If you absolutely must create a subclass, 694 * be sure to invoke the superclass 695 * version of each method. 696 * 697 * @see BasicComboPopup#createMouseListener 698 */ 699 protected class InvocationMouseHandler extends MouseAdapter { 700 /** 701 * Responds to mouse-pressed events on the combo box. 702 * 703 * @param e the mouse-press event to be handled 704 */ 705 public void mousePressed( MouseEvent e ) { 706 getHandler().mousePressed(e); 707 } 708 709 /** 710 * Responds to the user terminating 711 * a click or drag that began on the combo box. 712 * 713 * @param e the mouse-release event to be handled 714 */ 715 public void mouseReleased( MouseEvent e ) { 716 getHandler().mouseReleased(e); 717 } 718 } 719 720 /** 721 * This listener watches for dragging and updates the current selection in the 722 * list if it is dragging over the list. 723 */ 724 protected class InvocationMouseMotionHandler extends MouseMotionAdapter { 725 public void mouseDragged( MouseEvent e ) { 726 getHandler().mouseDragged(e); 727 } 728 } 729 730 /** 731 * As of Java 2 platform v 1.4, this class is now obsolete and is only included for 732 * backwards API compatibility. Do not instantiate or subclass. 733 * <p> 734 * All the functionality of this class has been included in 735 * BasicComboBoxUI ActionMap/InputMap methods. 736 */ 737 public class InvocationKeyHandler extends KeyAdapter { 738 public void keyReleased( KeyEvent e ) {} 739 } 740 741 /** 742 * As of Java 2 platform v 1.4, this class is now obsolete, doesn't do anything, and 743 * is only included for backwards API compatibility. Do not call or 744 * override. 745 */ 746 protected class ListSelectionHandler implements ListSelectionListener { 747 public void valueChanged( ListSelectionEvent e ) {} 748 } 749 750 /** 751 * As of 1.4, this class is now obsolete, doesn't do anything, and 752 * is only included for backwards API compatibility. Do not call or 753 * override. 754 * <p> 755 * The functionality has been migrated into <code>ItemHandler</code>. 756 * 757 * @see #createItemListener 758 */ 759 public class ListDataHandler implements ListDataListener { 760 public void contentsChanged( ListDataEvent e ) {} 761 762 public void intervalAdded( ListDataEvent e ) { 763 } 764 765 public void intervalRemoved( ListDataEvent e ) { 766 } 767 } 768 769 /** 770 * This listener hides the popup when the mouse is released in the list. 771 */ 772 protected class ListMouseHandler extends MouseAdapter { 773 public void mousePressed( MouseEvent e ) { 774 } 775 public void mouseReleased(MouseEvent anEvent) { 776 getHandler().mouseReleased(anEvent); 777 } 778 } 779 780 /** 781 * This listener changes the selected item as you move the mouse over the list. 782 * The selection change is not committed to the model, this is for user feedback only. 783 */ 784 protected class ListMouseMotionHandler extends MouseMotionAdapter { 785 public void mouseMoved( MouseEvent anEvent ) { 786 getHandler().mouseMoved(anEvent); 787 } 788 } 789 790 /** 791 * This listener watches for changes to the selection in the 792 * combo box. 793 */ 794 protected class ItemHandler implements ItemListener { 795 public void itemStateChanged( ItemEvent e ) { 796 getHandler().itemStateChanged(e); 797 } 798 } 799 800 /** 801 * This listener watches for bound properties that have changed in the 802 * combo box. 803 * <p> 804 * Subclasses which wish to listen to combo box property changes should 805 * call the superclass methods to ensure that the combo popup correctly 806 * handles property changes. 807 * 808 * @see #createPropertyChangeListener 809 */ 810 protected class PropertyChangeHandler implements PropertyChangeListener { 811 public void propertyChange( PropertyChangeEvent e ) { 812 getHandler().propertyChange(e); 813 } 814 } 815 816 817 private class AutoScrollActionHandler implements ActionListener { 818 private int direction; 819 820 AutoScrollActionHandler(int direction) { 821 this.direction = direction; 822 } 823 824 public void actionPerformed(ActionEvent e) { 825 if (direction == SCROLL_UP) { 826 autoScrollUp(); 827 } 828 else { 829 autoScrollDown(); 830 } 831 } 832 } 833 834 835 private class Handler implements ItemListener, MouseListener, 836 MouseMotionListener, PropertyChangeListener, 837 Serializable { 838 // 839 // MouseListener 840 // NOTE: this is added to both the JList and JComboBox 841 // 842 public void mouseClicked(MouseEvent e) { 843 } 844 845 public void mousePressed(MouseEvent e) { 846 if (e.getSource() == list) { 847 return; 848 } 849 if (!SwingUtilities.isLeftMouseButton(e) || !comboBox.isEnabled()) 850 return; 851 852 if ( comboBox.isEditable() ) { 853 Component comp = comboBox.getEditor().getEditorComponent(); 854 if ((!(comp instanceof JComponent)) || ((JComponent)comp).isRequestFocusEnabled()) { 855 comp.requestFocus(); 856 } 857 } 858 else if (comboBox.isRequestFocusEnabled()) { 859 comboBox.requestFocus(); 860 } 861 togglePopup(); 862 } 863 864 public void mouseReleased(MouseEvent e) { 865 if (e.getSource() == list) { 866 if (list.getModel().getSize() > 0) { 867 // JList mouse listener 868 if (comboBox.getSelectedIndex() == list.getSelectedIndex()) { 869 comboBox.getEditor().setItem(list.getSelectedValue()); 870 } 871 comboBox.setSelectedIndex(list.getSelectedIndex()); 872 } 873 comboBox.setPopupVisible(false); 874 // workaround for cancelling an edited item (bug 4530953) 875 if (comboBox.isEditable() && comboBox.getEditor() != null) { 876 comboBox.configureEditor(comboBox.getEditor(), 877 comboBox.getSelectedItem()); 878 } 879 return; 880 } 881 // JComboBox mouse listener 882 Component source = (Component)e.getSource(); 883 Dimension size = source.getSize(); 884 Rectangle bounds = new Rectangle( 0, 0, size.width - 1, size.height - 1 ); 885 if ( !bounds.contains( e.getPoint() ) ) { 886 MouseEvent newEvent = convertMouseEvent( e ); 887 Point location = newEvent.getPoint(); 888 Rectangle r = new Rectangle(); 889 list.computeVisibleRect( r ); 890 if ( r.contains( location ) ) { 891 if (comboBox.getSelectedIndex() == list.getSelectedIndex()) { 892 comboBox.getEditor().setItem(list.getSelectedValue()); 893 } 894 comboBox.setSelectedIndex(list.getSelectedIndex()); 895 } 896 comboBox.setPopupVisible(false); 897 } 898 hasEntered = false; 899 stopAutoScrolling(); 900 } 901 902 public void mouseEntered(MouseEvent e) { 903 } 904 905 public void mouseExited(MouseEvent e) { 906 } 907 908 // 909 // MouseMotionListener: 910 // NOTE: this is added to both the List and ComboBox 911 // 912 public void mouseMoved(MouseEvent anEvent) { 913 if (anEvent.getSource() == list) { 914 Point location = anEvent.getPoint(); 915 Rectangle r = new Rectangle(); 916 list.computeVisibleRect( r ); 917 if ( r.contains( location ) ) { 918 updateListBoxSelectionForEvent( anEvent, false ); 919 } 920 } 921 } 922 923 public void mouseDragged( MouseEvent e ) { 924 if (e.getSource() == list) { 925 return; 926 } 927 if ( isVisible() ) { 928 MouseEvent newEvent = convertMouseEvent( e ); 929 Rectangle r = new Rectangle(); 930 list.computeVisibleRect( r ); 931 932 if ( newEvent.getPoint().y >= r.y && newEvent.getPoint().y <= r.y + r.height - 1 ) { 933 hasEntered = true; 934 if ( isAutoScrolling ) { 935 stopAutoScrolling(); 936 } 937 Point location = newEvent.getPoint(); 938 if ( r.contains( location ) ) { 939 updateListBoxSelectionForEvent( newEvent, false ); 940 } 941 } 942 else { 943 if ( hasEntered ) { 944 int directionToScroll = newEvent.getPoint().y < r.y ? SCROLL_UP : SCROLL_DOWN; 945 if ( isAutoScrolling && scrollDirection != directionToScroll ) { 946 stopAutoScrolling(); 947 startAutoScrolling( directionToScroll ); 948 } 949 else if ( !isAutoScrolling ) { 950 startAutoScrolling( directionToScroll ); 951 } 952 } 953 else { 954 if ( e.getPoint().y < 0 ) { 955 hasEntered = true; 956 startAutoScrolling( SCROLL_UP ); 957 } 958 } 959 } 960 } 961 } 962 963 // 964 // PropertyChangeListener 965 // 966 public void propertyChange(PropertyChangeEvent e) { 967 @SuppressWarnings("unchecked") 968 JComboBox<Object> comboBox = (JComboBox)e.getSource(); 969 String propertyName = e.getPropertyName(); 970 971 if ( propertyName == "model" ) { 972 @SuppressWarnings("unchecked") 973 ComboBoxModel<Object> oldModel = (ComboBoxModel)e.getOldValue(); 974 @SuppressWarnings("unchecked") 975 ComboBoxModel<Object> newModel = (ComboBoxModel)e.getNewValue(); 976 uninstallComboBoxModelListeners(oldModel); 977 installComboBoxModelListeners(newModel); 978 979 list.setModel(newModel); 980 981 if ( isVisible() ) { 982 hide(); 983 } 984 } 985 else if ( propertyName == "renderer" ) { 986 list.setCellRenderer( comboBox.getRenderer() ); 987 if ( isVisible() ) { 988 hide(); 989 } 990 } 991 else if (propertyName == "componentOrientation") { 992 // Pass along the new component orientation 993 // to the list and the scroller 994 995 ComponentOrientation o =(ComponentOrientation)e.getNewValue(); 996 997 JList<?> list = getList(); 998 if (list!=null && list.getComponentOrientation()!=o) { 999 list.setComponentOrientation(o); 1000 } 1001 1002 if (scroller!=null && scroller.getComponentOrientation()!=o) { 1003 scroller.setComponentOrientation(o); 1004 } 1005 1006 if (o!=getComponentOrientation()) { 1007 setComponentOrientation(o); 1008 } 1009 } 1010 else if (propertyName == "lightWeightPopupEnabled") { 1011 setLightWeightPopupEnabled(comboBox.isLightWeightPopupEnabled()); 1012 } 1013 } 1014 1015 // 1016 // ItemListener 1017 // 1018 public void itemStateChanged( ItemEvent e ) { 1019 if (e.getStateChange() == ItemEvent.SELECTED) { 1020 @SuppressWarnings("unchecked") 1021 JComboBox<Object> comboBox = (JComboBox)e.getSource(); 1022 setListSelection(comboBox.getSelectedIndex()); 1023 } 1024 } 1025 } 1026 1027 // 1028 // end Event Listeners 1029 //================================================================= 1030 1031 1032 /** 1033 * Overridden to unconditionally return false. 1034 */ 1035 public boolean isFocusTraversable() { 1036 return false; 1037 } 1038 1039 //=================================================================== 1040 // begin Autoscroll methods 1041 // 1042 1043 /** 1044 * This protected method is implementation specific and should be private. 1045 * do not call or override. 1046 * 1047 * @param direction the direction of scrolling 1048 */ 1049 protected void startAutoScrolling( int direction ) { 1050 // XXX - should be a private method within InvocationMouseMotionHandler 1051 // if possible. 1052 if ( isAutoScrolling ) { 1053 autoscrollTimer.stop(); 1054 } 1055 1056 isAutoScrolling = true; 1057 1058 if ( direction == SCROLL_UP ) { 1059 scrollDirection = SCROLL_UP; 1060 Point convertedPoint = SwingUtilities.convertPoint( scroller, new Point( 1, 1 ), list ); 1061 int top = list.locationToIndex( convertedPoint ); 1062 list.setSelectedIndex( top ); 1063 1064 autoscrollTimer = new Timer( 100, new AutoScrollActionHandler( 1065 SCROLL_UP) ); 1066 } 1067 else if ( direction == SCROLL_DOWN ) { 1068 scrollDirection = SCROLL_DOWN; 1069 Dimension size = scroller.getSize(); 1070 Point convertedPoint = SwingUtilities.convertPoint( scroller, 1071 new Point( 1, (size.height - 1) - 2 ), 1072 list ); 1073 int bottom = list.locationToIndex( convertedPoint ); 1074 list.setSelectedIndex( bottom ); 1075 1076 autoscrollTimer = new Timer(100, new AutoScrollActionHandler( 1077 SCROLL_DOWN)); 1078 } 1079 autoscrollTimer.start(); 1080 } 1081 1082 /** 1083 * This protected method is implementation specific and should be private. 1084 * do not call or override. 1085 */ 1086 protected void stopAutoScrolling() { 1087 isAutoScrolling = false; 1088 1089 if ( autoscrollTimer != null ) { 1090 autoscrollTimer.stop(); 1091 autoscrollTimer = null; 1092 } 1093 } 1094 1095 /** 1096 * This protected method is implementation specific and should be private. 1097 * do not call or override. 1098 */ 1099 protected void autoScrollUp() { 1100 int index = list.getSelectedIndex(); 1101 if ( index > 0 ) { 1102 list.setSelectedIndex( index - 1 ); 1103 list.ensureIndexIsVisible( index - 1 ); 1104 } 1105 } 1106 1107 /** 1108 * This protected method is implementation specific and should be private. 1109 * do not call or override. 1110 */ 1111 protected void autoScrollDown() { 1112 int index = list.getSelectedIndex(); 1113 int lastItem = list.getModel().getSize() - 1; 1114 if ( index < lastItem ) { 1115 list.setSelectedIndex( index + 1 ); 1116 list.ensureIndexIsVisible( index + 1 ); 1117 } 1118 } 1119 1120 // 1121 // end Autoscroll methods 1122 //================================================================= 1123 1124 1125 //=================================================================== 1126 // begin Utility methods 1127 // 1128 1129 /** 1130 * Gets the AccessibleContext associated with this BasicComboPopup. 1131 * The AccessibleContext will have its parent set to the ComboBox. 1132 * 1133 * @return an AccessibleContext for the BasicComboPopup 1134 * @since 1.5 1135 */ 1136 public AccessibleContext getAccessibleContext() { 1137 AccessibleContext context = super.getAccessibleContext(); 1138 context.setAccessibleParent(comboBox); 1139 return context; 1140 } 1141 1142 1143 /** 1144 * This is is a utility method that helps event handlers figure out where to 1145 * send the focus when the popup is brought up. The standard implementation 1146 * delegates the focus to the editor (if the combo box is editable) or to 1147 * the JComboBox if it is not editable. 1148 * 1149 * @param e a mouse event 1150 */ 1151 protected void delegateFocus( MouseEvent e ) { 1152 if ( comboBox.isEditable() ) { 1153 Component comp = comboBox.getEditor().getEditorComponent(); 1154 if ((!(comp instanceof JComponent)) || ((JComponent)comp).isRequestFocusEnabled()) { 1155 comp.requestFocus(); 1156 } 1157 } 1158 else if (comboBox.isRequestFocusEnabled()) { 1159 comboBox.requestFocus(); 1160 } 1161 } 1162 1163 /** 1164 * Makes the popup visible if it is hidden and makes it hidden if it is 1165 * visible. 1166 */ 1167 protected void togglePopup() { 1168 if ( isVisible() ) { 1169 hide(); 1170 } 1171 else { 1172 show(); 1173 } 1174 } 1175 1176 /** 1177 * Sets the list selection index to the selectedIndex. This 1178 * method is used to synchronize the list selection with the 1179 * combo box selection. 1180 * 1181 * @param selectedIndex the index to set the list 1182 */ 1183 private void setListSelection(int selectedIndex) { 1184 if ( selectedIndex == -1 ) { 1185 list.clearSelection(); 1186 } 1187 else { 1188 list.setSelectedIndex( selectedIndex ); 1189 list.ensureIndexIsVisible( selectedIndex ); 1190 } 1191 } 1192 1193 /** 1194 * Converts mouse event. 1195 * 1196 * @param e a mouse event 1197 * @return converted mouse event 1198 */ 1199 protected MouseEvent convertMouseEvent( MouseEvent e ) { 1200 Point convertedPoint = SwingUtilities.convertPoint( (Component)e.getSource(), 1201 e.getPoint(), list ); 1202 MouseEvent newEvent = new MouseEvent( (Component)e.getSource(), 1203 e.getID(), 1204 e.getWhen(), 1205 e.getModifiers(), 1206 convertedPoint.x, 1207 convertedPoint.y, 1208 e.getXOnScreen(), 1209 e.getYOnScreen(), 1210 e.getClickCount(), 1211 e.isPopupTrigger(), 1212 MouseEvent.NOBUTTON ); 1213 return newEvent; 1214 } 1215 1216 1217 /** 1218 * Retrieves the height of the popup based on the current 1219 * ListCellRenderer and the maximum row count. 1220 * 1221 * @param maxRowCount the row count 1222 * @return the height of the popup 1223 */ 1224 protected int getPopupHeightForRowCount(int maxRowCount) { 1225 // Set the cached value of the minimum row count 1226 int minRowCount = Math.min( maxRowCount, comboBox.getItemCount() ); 1227 int height = 0; 1228 ListCellRenderer<Object> renderer = list.getCellRenderer(); 1229 Object value = null; 1230 1231 for ( int i = 0; i < minRowCount; ++i ) { 1232 value = list.getModel().getElementAt( i ); 1233 Component c = renderer.getListCellRendererComponent( list, value, i, false, false ); 1234 height += c.getPreferredSize().height; 1235 } 1236 1237 if (height == 0) { 1238 height = comboBox.getHeight(); 1239 } 1240 1241 Border border = scroller.getViewportBorder(); 1242 if (border != null) { 1243 Insets insets = border.getBorderInsets(null); 1244 height += insets.top + insets.bottom; 1245 } 1246 1247 border = scroller.getBorder(); 1248 if (border != null) { 1249 Insets insets = border.getBorderInsets(null); 1250 height += insets.top + insets.bottom; 1251 } 1252 1253 return height; 1254 } 1255 1256 /** 1257 * Calculate the placement and size of the popup portion of the combo box based 1258 * on the combo box location and the enclosing screen bounds. If 1259 * no transformations are required, then the returned rectangle will 1260 * have the same values as the parameters. 1261 * 1262 * @param px starting x location 1263 * @param py starting y location 1264 * @param pw starting width 1265 * @param ph starting height 1266 * @return a rectangle which represents the placement and size of the popup 1267 */ 1268 protected Rectangle computePopupBounds(int px,int py,int pw,int ph) { 1269 Toolkit toolkit = Toolkit.getDefaultToolkit(); 1270 Rectangle screenBounds; 1271 1272 // Calculate the desktop dimensions relative to the combo box. 1273 GraphicsConfiguration gc = comboBox.getGraphicsConfiguration(); 1274 Point p = new Point(); 1275 SwingUtilities.convertPointFromScreen(p, comboBox); 1276 if (gc != null) { 1277 Insets screenInsets = toolkit.getScreenInsets(gc); 1278 screenBounds = gc.getBounds(); 1279 screenBounds.width -= (screenInsets.left + screenInsets.right); 1280 screenBounds.height -= (screenInsets.top + screenInsets.bottom); 1281 screenBounds.x += (p.x + screenInsets.left); 1282 screenBounds.y += (p.y + screenInsets.top); 1283 } 1284 else { 1285 screenBounds = new Rectangle(p, toolkit.getScreenSize()); 1286 } 1287 1288 Rectangle rect = new Rectangle(px,py,pw,ph); 1289 if (py+ph > screenBounds.y+screenBounds.height 1290 && ph < screenBounds.height) { 1291 rect.y = -rect.height; 1292 } 1293 return rect; 1294 } 1295 1296 /** 1297 * Calculates the upper left location of the Popup. 1298 */ 1299 private Point getPopupLocation() { 1300 Dimension popupSize = comboBox.getSize(); 1301 Insets insets = getInsets(); 1302 1303 // reduce the width of the scrollpane by the insets so that the popup 1304 // is the same width as the combo box. 1305 popupSize.setSize(popupSize.width - (insets.right + insets.left), 1306 getPopupHeightForRowCount( comboBox.getMaximumRowCount())); 1307 Rectangle popupBounds = computePopupBounds( 0, comboBox.getBounds().height, 1308 popupSize.width, popupSize.height); 1309 Dimension scrollSize = popupBounds.getSize(); 1310 Point popupLocation = popupBounds.getLocation(); 1311 1312 scroller.setMaximumSize( scrollSize ); 1313 scroller.setPreferredSize( scrollSize ); 1314 scroller.setMinimumSize( scrollSize ); 1315 1316 list.revalidate(); 1317 1318 return popupLocation; 1319 } 1320 1321 /** 1322 * A utility method used by the event listeners. Given a mouse event, it changes 1323 * the list selection to the list item below the mouse. 1324 * 1325 * @param anEvent a mouse event 1326 * @param shouldScroll if {@code true} list should be scrolled. 1327 */ 1328 protected void updateListBoxSelectionForEvent(MouseEvent anEvent,boolean shouldScroll) { 1329 // XXX - only seems to be called from this class. shouldScroll flag is 1330 // never true 1331 Point location = anEvent.getPoint(); 1332 if ( list == null ) 1333 return; 1334 int index = list.locationToIndex(location); 1335 if ( index == -1 ) { 1336 if ( location.y < 0 ) 1337 index = 0; 1338 else 1339 index = comboBox.getModel().getSize() - 1; 1340 } 1341 if ( list.getSelectedIndex() != index ) { 1342 list.setSelectedIndex(index); 1343 if ( shouldScroll ) 1344 list.ensureIndexIsVisible(index); 1345 } 1346 } 1347 1348 // 1349 // end Utility methods 1350 //================================================================= 1351 }