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