source: josm/trunk/src/org/openstreetmap/josm/gui/widgets/JosmComboBox.java@ 6017

Last change on this file since 6017 was 6017, checked in by Don-vip, 11 years ago

fix #8818 - NPE in JosmComboBox

File size: 10.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.widgets;
3
4import java.awt.Component;
5import java.awt.Dimension;
6import java.awt.Toolkit;
7import java.awt.event.MouseAdapter;
8import java.awt.event.MouseEvent;
9import java.beans.PropertyChangeEvent;
10import java.beans.PropertyChangeListener;
11import java.util.ArrayList;
12import java.util.Arrays;
13import java.util.Collection;
14import java.util.Vector;
15
16import javax.accessibility.Accessible;
17import javax.swing.ComboBoxEditor;
18import javax.swing.ComboBoxModel;
19import javax.swing.DefaultComboBoxModel;
20import javax.swing.JComboBox;
21import javax.swing.JList;
22import javax.swing.plaf.basic.ComboPopup;
23import javax.swing.text.JTextComponent;
24
25/**
26 * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br/>
27 * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
28 *
29 * @since 5429
30 */
31public class JosmComboBox extends JComboBox {
32
33 /**
34 * The default prototype value used to compute the maximum number of elements to be displayed at once before
35 * displaying a scroll bar
36 */
37 public static final String DEFAULT_PROTOTYPE_DISPLAY_VALUE = "Prototype display value";
38
39 /**
40 * Creates a <code>JosmComboBox</code> with a default data model.
41 * The default data model is an empty list of objects.
42 * Use <code>addItem</code> to add items. By default the first item
43 * in the data model becomes selected.
44 *
45 * @see DefaultComboBoxModel
46 */
47 public JosmComboBox() {
48 this(DEFAULT_PROTOTYPE_DISPLAY_VALUE);
49 }
50
51 /**
52 * Creates a <code>JosmComboBox</code> with a default data model and
53 * the specified prototype display value.
54 * The default data model is an empty list of objects.
55 * Use <code>addItem</code> to add items. By default the first item
56 * in the data model becomes selected.
57 *
58 * @param prototypeDisplayValue the <code>Object</code> used to compute
59 * the maximum number of elements to be displayed at once before
60 * displaying a scroll bar
61 *
62 * @see DefaultComboBoxModel
63 * @since 5450
64 */
65 public JosmComboBox(Object prototypeDisplayValue) {
66 super();
67 init(prototypeDisplayValue);
68 }
69
70 /**
71 * Creates a <code>JosmComboBox</code> that takes its items from an
72 * existing <code>ComboBoxModel</code>. Since the
73 * <code>ComboBoxModel</code> is provided, a combo box created using
74 * this constructor does not create a default combo box model and
75 * may impact how the insert, remove and add methods behave.
76 *
77 * @param aModel the <code>ComboBoxModel</code> that provides the
78 * displayed list of items
79 * @see DefaultComboBoxModel
80 */
81 public JosmComboBox(ComboBoxModel aModel) {
82 super(aModel);
83 ArrayList<Object> list = new ArrayList<Object>(aModel.getSize());
84 for (int i = 0; i<aModel.getSize(); i++) {
85 list.add(aModel.getElementAt(i));
86 }
87 init(findPrototypeDisplayValue(list));
88 }
89
90 /**
91 * Creates a <code>JosmComboBox</code> that contains the elements
92 * in the specified array. By default the first item in the array
93 * (and therefore the data model) becomes selected.
94 *
95 * @param items an array of objects to insert into the combo box
96 * @see DefaultComboBoxModel
97 */
98 public JosmComboBox(Object[] items) {
99 super(items);
100 init(findPrototypeDisplayValue(Arrays.asList(items)));
101 }
102
103 /**
104 * Creates a <code>JosmComboBox</code> that contains the elements
105 * in the specified Vector. By default the first item in the vector
106 * (and therefore the data model) becomes selected.
107 *
108 * @param items an array of vectors to insert into the combo box
109 * @see DefaultComboBoxModel
110 */
111 public JosmComboBox(Vector<?> items) {
112 super(items);
113 init(findPrototypeDisplayValue(items));
114 }
115
116 /**
117 * Finds the prototype display value to use among the given possible candidates.
118 * @param possibleValues The possible candidates that will be iterated.
119 * @return The value that needs the largest display height on screen.
120 * @since 5558
121 */
122 protected Object findPrototypeDisplayValue(Collection<?> possibleValues) {
123 Object result = null;
124 int maxHeight = -1;
125 if (possibleValues != null) {
126 // Remind old prototype to restore it later
127 Object oldPrototype = getPrototypeDisplayValue();
128 // Get internal JList to directly call the renderer
129 JList list = getList();
130 try {
131 // Index to give to renderer
132 int i = 0;
133 for (Object value : possibleValues) {
134 if (value != null) {
135 // These two lines work with a "classic" renderer,
136 // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
137 //setPrototypeDisplayValue(value);
138 //Dimension dim = getPreferredSize();
139
140 // So we explicitely call the renderer by simulating a correct index for the current value
141 Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
142 if (c != null) {
143 // Get the real preferred size for the current value
144 Dimension dim = c.getPreferredSize();
145 if (dim.height > maxHeight) {
146 // Larger ? This is our new prototype
147 maxHeight = dim.height;
148 result = value;
149 }
150 }
151 }
152 i++;
153 }
154 } finally {
155 // Restore original prototype
156 setPrototypeDisplayValue(oldPrototype);
157 }
158 }
159 return result;
160 }
161
162 protected final JList getList() {
163 for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) {
164 Accessible child = getUI().getAccessibleChild(this, i);
165 if (child instanceof ComboPopup) {
166 return ((ComboPopup)child).getList();
167 }
168 }
169 return null;
170 }
171
172 protected void init(Object prototype) {
173 if (prototype != null) {
174 setPrototypeDisplayValue(prototype);
175 int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
176 // Compute maximum number of visible items based on the preferred size of the combo box.
177 // This assumes that items have the same height as the combo box, which is not granted by the look and feel
178 int maxsize = (screenHeight/getPreferredSize().height) / 2;
179 // If possible, adjust the maximum number of items with the real height of items
180 // It is not granted this works on every platform (tested OK on Windows)
181 JList list = getList();
182 if (list != null) {
183 if (list.getPrototypeCellValue() != prototype) {
184 list.setPrototypeCellValue(prototype);
185 }
186 int height = list.getFixedCellHeight();
187 if (height > 0) {
188 maxsize = (screenHeight/height) / 2;
189 }
190 }
191 setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize));
192 }
193 // Handle text contextual menus for editable comboboxes
194 ContextMenuHandler handler = new ContextMenuHandler();
195 addPropertyChangeListener("editable", handler);
196 addPropertyChangeListener("editor", handler);
197 }
198
199 protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener {
200
201 private JTextComponent component;
202 private PopupMenuLauncher launcher;
203
204 @Override public void propertyChange(PropertyChangeEvent evt) {
205 if (evt.getPropertyName().equals("editable")) {
206 if (evt.getNewValue().equals(true)) {
207 enableMenu();
208 } else {
209 disableMenu();
210 }
211 } else if (evt.getPropertyName().equals("editor")) {
212 disableMenu();
213 if (isEditable()) {
214 enableMenu();
215 }
216 }
217 }
218
219 private void enableMenu() {
220 if (launcher == null) {
221 ComboBoxEditor editor = getEditor();
222 if (editor != null) {
223 Component editorComponent = editor.getEditorComponent();
224 if (editorComponent instanceof JTextComponent) {
225 component = (JTextComponent) editorComponent;
226 component.addMouseListener(this);
227 launcher = TextContextualPopupMenu.enableMenuFor(component);
228 }
229 }
230 }
231 }
232
233 private void disableMenu() {
234 if (launcher != null) {
235 TextContextualPopupMenu.disableMenuFor(component, launcher);
236 launcher = null;
237 component.removeMouseListener(this);
238 component = null;
239 }
240 }
241
242 @Override public void mousePressed(MouseEvent e) { processEvent(e); }
243 @Override public void mouseClicked(MouseEvent e) { processEvent(e); }
244 @Override public void mouseReleased(MouseEvent e) { processEvent(e); }
245
246 private void processEvent(MouseEvent e) {
247 if (launcher != null && !e.isPopupTrigger()) {
248 if (launcher.getMenu().isShowing()) {
249 launcher.getMenu().setVisible(false);
250 }
251 }
252 }
253 }
254
255 /**
256 * Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used.
257 * @param values The values displayed in the combo box.
258 * @since 5558
259 */
260 public final void reinitialize(Collection<?> values) {
261 init(findPrototypeDisplayValue(values));
262 }
263}
Note: See TracBrowser for help on using the repository browser.