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

Last change on this file since 13652 was 9576, checked in by Don-vip, 8 years ago

code refactoring for unit tests / headless mode

  • Property svn:eol-style set to native
File size: 9.6 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.event.MouseAdapter;
7import java.awt.event.MouseEvent;
8import java.beans.PropertyChangeEvent;
9import java.beans.PropertyChangeListener;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.List;
14
15import javax.accessibility.Accessible;
16import javax.swing.ComboBoxEditor;
17import javax.swing.ComboBoxModel;
18import javax.swing.DefaultComboBoxModel;
19import javax.swing.JComboBox;
20import javax.swing.JList;
21import javax.swing.JTextField;
22import javax.swing.plaf.basic.ComboPopup;
23import javax.swing.text.JTextComponent;
24
25import org.openstreetmap.josm.gui.util.GuiHelper;
26
27/**
28 * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br>
29 * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917).
30 * @param <E> the type of the elements of this combo box
31 *
32 * @since 5429 (creation)
33 * @since 7015 (generics for Java 7)
34 */
35public class JosmComboBox<E> extends JComboBox<E> {
36
37 /**
38 * Creates a <code>JosmComboBox</code> with a default data model.
39 * The default data model is an empty list of objects.
40 * Use <code>addItem</code> to add items. By default the first item
41 * in the data model becomes selected.
42 *
43 * @see DefaultComboBoxModel
44 */
45 public JosmComboBox() {
46 init(null);
47 }
48
49 /**
50 * Creates a <code>JosmComboBox</code> with a default data model and
51 * the specified prototype display value.
52 * The default data model is an empty list of objects.
53 * Use <code>addItem</code> to add items. By default the first item
54 * in the data model becomes selected.
55 *
56 * @param prototypeDisplayValue the <code>Object</code> used to compute
57 * the maximum number of elements to be displayed at once before
58 * displaying a scroll bar
59 *
60 * @see DefaultComboBoxModel
61 * @since 5450
62 */
63 public JosmComboBox(E prototypeDisplayValue) {
64 init(prototypeDisplayValue);
65 }
66
67 /**
68 * Creates a <code>JosmComboBox</code> that takes its items from an
69 * existing <code>ComboBoxModel</code>. Since the
70 * <code>ComboBoxModel</code> is provided, a combo box created using
71 * this constructor does not create a default combo box model and
72 * may impact how the insert, remove and add methods behave.
73 *
74 * @param aModel the <code>ComboBoxModel</code> that provides the
75 * displayed list of items
76 * @see DefaultComboBoxModel
77 */
78 public JosmComboBox(ComboBoxModel<E> aModel) {
79 super(aModel);
80 List<E> list = new ArrayList<>(aModel.getSize());
81 for (int i = 0; i < aModel.getSize(); i++) {
82 list.add(aModel.getElementAt(i));
83 }
84 init(findPrototypeDisplayValue(list));
85 }
86
87 /**
88 * Creates a <code>JosmComboBox</code> that contains the elements
89 * in the specified array. By default the first item in the array
90 * (and therefore the data model) becomes selected.
91 *
92 * @param items an array of objects to insert into the combo box
93 * @see DefaultComboBoxModel
94 */
95 public JosmComboBox(E[] items) {
96 super(items);
97 init(findPrototypeDisplayValue(Arrays.asList(items)));
98 }
99
100 /**
101 * Returns the editor component
102 * @return the editor component
103 * @see ComboBoxEditor#getEditorComponent()
104 * @since 9484
105 */
106 public JTextField getEditorComponent() {
107 return (JTextField) getEditor().getEditorComponent();
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 final E findPrototypeDisplayValue(Collection<E> possibleValues) {
117 E result = null;
118 int maxHeight = -1;
119 if (possibleValues != null) {
120 // Remind old prototype to restore it later
121 E oldPrototype = getPrototypeDisplayValue();
122 // Get internal JList to directly call the renderer
123 @SuppressWarnings("rawtypes")
124 JList list = getList();
125 try {
126 // Index to give to renderer
127 int i = 0;
128 for (E value : possibleValues) {
129 if (value != null) {
130 // With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize()
131 // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1
132 // So we explicitely call the renderer by simulating a correct index for the current value
133 @SuppressWarnings("unchecked")
134 Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true);
135 if (c != null) {
136 // Get the real preferred size for the current value
137 Dimension dim = c.getPreferredSize();
138 if (dim.height > maxHeight) {
139 // Larger ? This is our new prototype
140 maxHeight = dim.height;
141 result = value;
142 }
143 }
144 }
145 i++;
146 }
147 } finally {
148 // Restore original prototype
149 setPrototypeDisplayValue(oldPrototype);
150 }
151 }
152 return result;
153 }
154
155 @SuppressWarnings("unchecked")
156 protected final JList<Object> 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 final void init(E prototype) {
167 if (prototype != null) {
168 setPrototypeDisplayValue(prototype);
169 int screenHeight = GuiHelper.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<Object> list = getList();
176 if (list != null) {
177 if (!prototype.equals(list.getPrototypeCellValue())) {
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 // Handle text contextual menus for editable comboboxes
188 ContextMenuHandler handler = new ContextMenuHandler();
189 addPropertyChangeListener("editable", handler);
190 addPropertyChangeListener("editor", handler);
191 }
192
193 protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener {
194
195 private JTextComponent component;
196 private PopupMenuLauncher launcher;
197
198 @Override
199 public void propertyChange(PropertyChangeEvent evt) {
200 if ("editable".equals(evt.getPropertyName())) {
201 if (evt.getNewValue().equals(Boolean.TRUE)) {
202 enableMenu();
203 } else {
204 disableMenu();
205 }
206 } else if ("editor".equals(evt.getPropertyName())) {
207 disableMenu();
208 if (isEditable()) {
209 enableMenu();
210 }
211 }
212 }
213
214 private void enableMenu() {
215 if (launcher == null && editor != null) {
216 Component editorComponent = editor.getEditorComponent();
217 if (editorComponent instanceof JTextComponent) {
218 component = (JTextComponent) editorComponent;
219 component.addMouseListener(this);
220 launcher = TextContextualPopupMenu.enableMenuFor(component, true);
221 }
222 }
223 }
224
225 private void disableMenu() {
226 if (launcher != null) {
227 TextContextualPopupMenu.disableMenuFor(component, launcher);
228 launcher = null;
229 component.removeMouseListener(this);
230 component = null;
231 }
232 }
233
234 @Override
235 public void mousePressed(MouseEvent e) {
236 processEvent(e);
237 }
238
239 @Override
240 public void mouseReleased(MouseEvent e) {
241 processEvent(e);
242 }
243
244 private void processEvent(MouseEvent e) {
245 if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) {
246 launcher.getMenu().setVisible(false);
247 }
248 }
249 }
250
251 /**
252 * Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used.
253 * @param values The values displayed in the combo box.
254 * @since 5558
255 */
256 public final void reinitialize(Collection<E> values) {
257 init(findPrototypeDisplayValue(values));
258 }
259}
Note: See TracBrowser for help on using the repository browser.