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 }