001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import java.awt.Component; 005import java.awt.Dimension; 006import java.awt.event.MouseAdapter; 007import java.awt.event.MouseEvent; 008import java.beans.PropertyChangeEvent; 009import java.beans.PropertyChangeListener; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.List; 014 015import javax.accessibility.Accessible; 016import javax.swing.ComboBoxEditor; 017import javax.swing.ComboBoxModel; 018import javax.swing.DefaultComboBoxModel; 019import javax.swing.JComboBox; 020import javax.swing.JList; 021import javax.swing.JTextField; 022import javax.swing.plaf.basic.ComboPopup; 023import javax.swing.text.JTextComponent; 024 025import org.openstreetmap.josm.gui.util.GuiHelper; 026 027/** 028 * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br> 029 * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917). 030 * @param <E> the type of the elements of this combo box 031 * 032 * @since 5429 (creation) 033 * @since 7015 (generics for Java 7) 034 */ 035public class JosmComboBox<E> extends JComboBox<E> { 036 037 private final ContextMenuHandler handler = new ContextMenuHandler(); 038 039 /** 040 * Creates a <code>JosmComboBox</code> with a default data model. 041 * The default data model is an empty list of objects. 042 * Use <code>addItem</code> to add items. By default the first item 043 * in the data model becomes selected. 044 * 045 * @see DefaultComboBoxModel 046 */ 047 public JosmComboBox() { 048 init(null); 049 } 050 051 /** 052 * Creates a <code>JosmComboBox</code> with a default data model and 053 * the specified prototype display value. 054 * The default data model is an empty list of objects. 055 * Use <code>addItem</code> to add items. By default the first item 056 * in the data model becomes selected. 057 * 058 * @param prototypeDisplayValue the <code>Object</code> used to compute 059 * the maximum number of elements to be displayed at once before 060 * displaying a scroll bar 061 * 062 * @see DefaultComboBoxModel 063 * @since 5450 064 */ 065 public JosmComboBox(E prototypeDisplayValue) { 066 init(prototypeDisplayValue); 067 } 068 069 /** 070 * Creates a <code>JosmComboBox</code> that takes its items from an 071 * existing <code>ComboBoxModel</code>. Since the 072 * <code>ComboBoxModel</code> is provided, a combo box created using 073 * this constructor does not create a default combo box model and 074 * may impact how the insert, remove and add methods behave. 075 * 076 * @param aModel the <code>ComboBoxModel</code> that provides the 077 * displayed list of items 078 * @see DefaultComboBoxModel 079 */ 080 public JosmComboBox(ComboBoxModel<E> aModel) { 081 super(aModel); 082 List<E> list = new ArrayList<>(aModel.getSize()); 083 for (int i = 0; i < aModel.getSize(); i++) { 084 list.add(aModel.getElementAt(i)); 085 } 086 init(findPrototypeDisplayValue(list)); 087 } 088 089 /** 090 * Creates a <code>JosmComboBox</code> that contains the elements 091 * in the specified array. By default the first item in the array 092 * (and therefore the data model) becomes selected. 093 * 094 * @param items an array of objects to insert into the combo box 095 * @see DefaultComboBoxModel 096 */ 097 public JosmComboBox(E[] items) { 098 super(items); 099 init(findPrototypeDisplayValue(Arrays.asList(items))); 100 } 101 102 /** 103 * Returns the editor component 104 * @return the editor component 105 * @see ComboBoxEditor#getEditorComponent() 106 * @since 9484 107 */ 108 public JTextField getEditorComponent() { 109 return (JTextField) getEditor().getEditorComponent(); 110 } 111 112 /** 113 * Finds the prototype display value to use among the given possible candidates. 114 * @param possibleValues The possible candidates that will be iterated. 115 * @return The value that needs the largest display height on screen. 116 * @since 5558 117 */ 118 protected final E findPrototypeDisplayValue(Collection<E> possibleValues) { 119 E result = null; 120 int maxHeight = -1; 121 if (possibleValues != null) { 122 // Remind old prototype to restore it later 123 E oldPrototype = getPrototypeDisplayValue(); 124 // Get internal JList to directly call the renderer 125 @SuppressWarnings("rawtypes") 126 JList list = getList(); 127 try { 128 // Index to give to renderer 129 int i = 0; 130 for (E value : possibleValues) { 131 if (value != null) { 132 // With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize() 133 // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1 134 // So we explicitly call the renderer by simulating a correct index for the current value 135 @SuppressWarnings("unchecked") 136 Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true); 137 if (c != null) { 138 // Get the real preferred size for the current value 139 Dimension dim = c.getPreferredSize(); 140 if (dim.height > maxHeight) { 141 // Larger ? This is our new prototype 142 maxHeight = dim.height; 143 result = value; 144 } 145 } 146 } 147 i++; 148 } 149 } finally { 150 // Restore original prototype 151 setPrototypeDisplayValue(oldPrototype); 152 } 153 } 154 return result; 155 } 156 157 @SuppressWarnings("unchecked") 158 protected final JList<Object> getList() { 159 for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) { 160 Accessible child = getUI().getAccessibleChild(this, i); 161 if (child instanceof ComboPopup) { 162 return ((ComboPopup) child).getList(); 163 } 164 } 165 return null; 166 } 167 168 protected final void init(E prototype) { 169 init(prototype, true); 170 } 171 172 protected final void init(E prototype, boolean registerPropertyChangeListener) { 173 if (prototype != null) { 174 setPrototypeDisplayValue(prototype); 175 int screenHeight = GuiHelper.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<Object> list = getList(); 182 if (list != null) { 183 if (!prototype.equals(list.getPrototypeCellValue())) { 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 if (registerPropertyChangeListener) { 195 addPropertyChangeListener("editable", handler); 196 addPropertyChangeListener("editor", handler); 197 } 198 } 199 200 protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener { 201 202 private JTextComponent component; 203 private PopupMenuLauncher launcher; 204 205 @Override 206 public void propertyChange(PropertyChangeEvent evt) { 207 if ("editable".equals(evt.getPropertyName())) { 208 if (evt.getNewValue().equals(Boolean.TRUE)) { 209 enableMenu(); 210 } else { 211 disableMenu(); 212 } 213 } else if ("editor".equals(evt.getPropertyName())) { 214 disableMenu(); 215 if (isEditable()) { 216 enableMenu(); 217 } 218 } 219 } 220 221 private void enableMenu() { 222 if (launcher == null && 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, true); 228 } 229 } 230 } 231 232 private void disableMenu() { 233 if (launcher != null) { 234 TextContextualPopupMenu.disableMenuFor(component, launcher); 235 launcher = null; 236 component.removeMouseListener(this); 237 component = null; 238 } 239 } 240 241 private void discardAllUndoableEdits() { 242 if (launcher != null) { 243 launcher.discardAllUndoableEdits(); 244 } 245 } 246 247 @Override 248 public void mousePressed(MouseEvent e) { 249 processEvent(e); 250 } 251 252 @Override 253 public void mouseReleased(MouseEvent e) { 254 processEvent(e); 255 } 256 257 private void processEvent(MouseEvent e) { 258 if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) { 259 launcher.getMenu().setVisible(false); 260 } 261 } 262 } 263 264 /** 265 * Reinitializes this {@link JosmComboBox} to the specified values. This may be needed if a custom renderer is used. 266 * @param values The values displayed in the combo box. 267 * @since 5558 268 */ 269 public final void reinitialize(Collection<E> values) { 270 init(findPrototypeDisplayValue(values), false); 271 discardAllUndoableEdits(); 272 } 273 274 /** 275 * Empties the internal undo manager, if any. 276 * @since 14977 277 */ 278 public final void discardAllUndoableEdits() { 279 handler.discardAllUndoableEdits(); 280 } 281}