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