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
26package javax.swing.plaf.basic;
27
28import javax.accessibility.AccessibleContext;
29import javax.swing.*;
30import javax.swing.border.Border;
31import javax.swing.border.LineBorder;
32import javax.swing.event.*;
33import java.awt.*;
34import java.awt.event.*;
35import java.beans.PropertyChangeListener;
36import java.beans.PropertyChangeEvent;
37import 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
64public 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}
1406