001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.widgets;
003    
004    import java.awt.Toolkit;
005    import java.util.Vector;
006    
007    import javax.accessibility.Accessible;
008    import javax.swing.ComboBoxModel;
009    import javax.swing.DefaultComboBoxModel;
010    import javax.swing.JComboBox;
011    import javax.swing.JList;
012    import javax.swing.plaf.basic.ComboPopup;
013    
014    /**
015     * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br/>
016     * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
017     * 
018     * @since 5429
019     */
020    public class JosmComboBox extends JComboBox {
021    
022        /**
023         * The default prototype value used to compute the maximum number of elements to be displayed at once before 
024         * displaying a scroll bar
025         */
026        public static final String DEFAULT_PROTOTYPE_DISPLAY_VALUE = "Prototype display value";
027        
028        /**
029         * Creates a <code>JosmComboBox</code> with a default data model.
030         * The default data model is an empty list of objects.
031         * Use <code>addItem</code> to add items. By default the first item
032         * in the data model becomes selected.
033         *
034         * @see DefaultComboBoxModel
035         */
036        public JosmComboBox() {
037            this(DEFAULT_PROTOTYPE_DISPLAY_VALUE);
038        }
039    
040        /**
041         * Creates a <code>JosmComboBox</code> with a default data model and
042         * the specified prototype display value.
043         * The default data model is an empty list of objects.
044         * Use <code>addItem</code> to add items. By default the first item
045         * in the data model becomes selected.
046         * 
047         * @param prototypeDisplayValue the <code>Object</code> used to compute 
048         *      the maximum number of elements to be displayed at once before 
049         *      displaying a scroll bar
050         *
051         * @see DefaultComboBoxModel
052         * @since 5450
053         */
054        public JosmComboBox(Object prototypeDisplayValue) {
055            super();
056            init(prototypeDisplayValue);
057        }
058    
059        /**
060         * Creates a <code>JosmComboBox</code> that takes its items from an
061         * existing <code>ComboBoxModel</code>. Since the
062         * <code>ComboBoxModel</code> is provided, a combo box created using
063         * this constructor does not create a default combo box model and
064         * may impact how the insert, remove and add methods behave.
065         *
066         * @param aModel the <code>ComboBoxModel</code> that provides the 
067         *      displayed list of items
068         * @see DefaultComboBoxModel
069         */
070        public JosmComboBox(ComboBoxModel aModel) {
071            super(aModel);
072            init(aModel != null && aModel.getSize() > 0 ? aModel.getElementAt(0) : null);
073        }
074    
075        /** 
076         * Creates a <code>JosmComboBox</code> that contains the elements
077         * in the specified array. By default the first item in the array
078         * (and therefore the data model) becomes selected.
079         *
080         * @param items  an array of objects to insert into the combo box
081         * @see DefaultComboBoxModel
082         */
083        public JosmComboBox(Object[] items) {
084            super(items);
085            init(items != null && items.length > 0 ? items[0] : null);
086        }
087    
088        /**
089         * Creates a <code>JosmComboBox</code> that contains the elements
090         * in the specified Vector. By default the first item in the vector
091         * (and therefore the data model) becomes selected.
092         *
093         * @param items  an array of vectors to insert into the combo box
094         * @see DefaultComboBoxModel
095         */
096        public JosmComboBox(Vector<?> items) {
097            super(items);
098            init(items != null && !items.isEmpty() ? items.get(0) : null);
099        }
100        
101        protected void init(Object prototype) {
102            if (prototype != null) {
103                setPrototypeDisplayValue(prototype);
104                int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
105                // Compute maximum number of visible items based on the preferred size of the combo box. 
106                // This assumes that items have the same height as the combo box, which is not granted by the look and feel
107                int maxsize = (screenHeight/getPreferredSize().height) / 2;
108                // If possible, adjust the maximum number of items with the real height of items
109                // It is not granted this works on every platform (tested OK on Windows)
110                for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) {
111                    Accessible child = getUI().getAccessibleChild(this, i);
112                    if (child instanceof ComboPopup) {
113                        JList list = ((ComboPopup)child).getList();
114                        if (list != null) {
115                            if (list.getPrototypeCellValue() != prototype) {
116                                list.setPrototypeCellValue(prototype);
117                            }
118                            int height = list.getFixedCellHeight();
119                            if (height > 0) {
120                                maxsize = (screenHeight/height) / 2;
121                            }
122                        }
123                        break;
124                    }
125                }
126                setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
127            }
128        }
129    }