001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.widgets;
003    
004    import java.awt.Component;
005    import java.awt.Dimension;
006    import java.awt.Toolkit;
007    import java.util.ArrayList;
008    import java.util.Arrays;
009    import java.util.Collection;
010    import java.util.Vector;
011    
012    import javax.accessibility.Accessible;
013    import javax.swing.ComboBoxModel;
014    import javax.swing.DefaultComboBoxModel;
015    import javax.swing.JComboBox;
016    import javax.swing.JList;
017    import javax.swing.plaf.basic.ComboPopup;
018    
019    /**
020     * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br/>
021     * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
022     * 
023     * @since 5429
024     */
025    public class JosmComboBox extends JComboBox {
026    
027        /**
028         * The default prototype value used to compute the maximum number of elements to be displayed at once before 
029         * displaying a scroll bar
030         */
031        public static final String DEFAULT_PROTOTYPE_DISPLAY_VALUE = "Prototype display value";
032        
033        /**
034         * Creates a <code>JosmComboBox</code> with a default data model.
035         * The default data model is an empty list of objects.
036         * Use <code>addItem</code> to add items. By default the first item
037         * in the data model becomes selected.
038         *
039         * @see DefaultComboBoxModel
040         */
041        public JosmComboBox() {
042            this(DEFAULT_PROTOTYPE_DISPLAY_VALUE);
043        }
044    
045        /**
046         * Creates a <code>JosmComboBox</code> with a default data model and
047         * the specified prototype display value.
048         * The default data model is an empty list of objects.
049         * Use <code>addItem</code> to add items. By default the first item
050         * in the data model becomes selected.
051         * 
052         * @param prototypeDisplayValue the <code>Object</code> used to compute 
053         *      the maximum number of elements to be displayed at once before 
054         *      displaying a scroll bar
055         *
056         * @see DefaultComboBoxModel
057         * @since 5450
058         */
059        public JosmComboBox(Object prototypeDisplayValue) {
060            super();
061            init(prototypeDisplayValue);
062        }
063    
064        /**
065         * Creates a <code>JosmComboBox</code> that takes its items from an
066         * existing <code>ComboBoxModel</code>. Since the
067         * <code>ComboBoxModel</code> is provided, a combo box created using
068         * this constructor does not create a default combo box model and
069         * may impact how the insert, remove and add methods behave.
070         *
071         * @param aModel the <code>ComboBoxModel</code> that provides the 
072         *      displayed list of items
073         * @see DefaultComboBoxModel
074         */
075        public JosmComboBox(ComboBoxModel aModel) {
076            super(aModel);
077            ArrayList<Object> list = new ArrayList<Object>(aModel.getSize());
078            for (int i = 0; i<aModel.getSize(); i++) {
079                list.add(aModel.getElementAt(i));
080            }
081            init(findPrototypeDisplayValue(list));
082        }
083    
084        /** 
085         * Creates a <code>JosmComboBox</code> that contains the elements
086         * in the specified array. By default the first item in the array
087         * (and therefore the data model) becomes selected.
088         *
089         * @param items  an array of objects to insert into the combo box
090         * @see DefaultComboBoxModel
091         */
092        public JosmComboBox(Object[] items) {
093            super(items);
094            init(findPrototypeDisplayValue(Arrays.asList(items)));
095        }
096    
097        /**
098         * Creates a <code>JosmComboBox</code> that contains the elements
099         * in the specified Vector. By default the first item in the vector
100         * (and therefore the data model) becomes selected.
101         *
102         * @param items  an array of vectors to insert into the combo box
103         * @see DefaultComboBoxModel
104         */
105        public JosmComboBox(Vector<?> items) {
106            super(items);
107            init(findPrototypeDisplayValue(items));
108        }
109        
110        /**
111         * Finds the prototype display value to use among the given possible candidates.
112         * @param possibleValues The possible candidates that will be iterated.
113         * @return The value that needs the largest display height on screen.
114         * @since 5558
115         */
116        protected Object findPrototypeDisplayValue(Collection<?> possibleValues) {
117            Object result = null;
118            int maxHeight = -1;
119            if (possibleValues != null) {
120                // Remind old prototype to restore it later
121                Object oldPrototype = getPrototypeDisplayValue();
122                // Get internal JList to directly call the renderer 
123                JList list = getList();
124                try {
125                    // Index to give to renderer
126                    int i = 0;
127                    for (Object value : possibleValues) {
128                        if (value != null) {
129                            // These two lines work with a "classic" renderer, 
130                            // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
131                            //setPrototypeDisplayValue(value);
132                            //Dimension dim = getPreferredSize();
133                            
134                            // So we explicitely call the renderer by simulating a correct index for the current value
135                            Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
136                            if (c != null) {
137                                // Get the real preferred size for the current value
138                                Dimension dim = c.getPreferredSize();
139                                if (dim.height > maxHeight) {
140                                    // Larger ? This is our new prototype
141                                    maxHeight = dim.height;
142                                    result = value;
143                                }
144                            }
145                        }
146                        i++;
147                    }
148                } finally {
149                    // Restore original prototype
150                    setPrototypeDisplayValue(oldPrototype);
151                }
152            }
153            return result;
154        }
155        
156        protected final JList getList() {
157            for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) {
158                Accessible child = getUI().getAccessibleChild(this, i);
159                if (child instanceof ComboPopup) {
160                    return ((ComboPopup)child).getList();
161                }
162            }
163            return null;
164        }
165        
166        protected void init(Object prototype) {
167            if (prototype != null) {
168                setPrototypeDisplayValue(prototype);
169                int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
170                // Compute maximum number of visible items based on the preferred size of the combo box. 
171                // This assumes that items have the same height as the combo box, which is not granted by the look and feel
172                int maxsize = (screenHeight/getPreferredSize().height) / 2;
173                // If possible, adjust the maximum number of items with the real height of items
174                // It is not granted this works on every platform (tested OK on Windows)
175                JList list = getList();
176                if (list != null) {
177                    if (list.getPrototypeCellValue() != prototype) {
178                        list.setPrototypeCellValue(prototype);
179                    }
180                    int height = list.getFixedCellHeight();
181                    if (height > 0) {
182                        maxsize = (screenHeight/height) / 2;
183                    }
184                }
185                setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
186            }
187        }
188        
189        /**
190         * Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used.
191         * @param values The values displayed in the combo box.
192         * @since 5558
193         */
194        public final void reinitialize(Collection<?> values) {
195            init(findPrototypeDisplayValue(values));
196        }
197    }