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 /** 038 * Creates a <code>JosmComboBox</code> with a default data model. 039 * The default data model is an empty list of objects. 040 * Use <code>addItem</code> to add items. By default the first item 041 * in the data model becomes selected. 042 * 043 * @see DefaultComboBoxModel 044 */ 045 public JosmComboBox() { 046 init(null); 047 } 048 049 /** 050 * Creates a <code>JosmComboBox</code> with a default data model and 051 * the specified prototype display value. 052 * The default data model is an empty list of objects. 053 * Use <code>addItem</code> to add items. By default the first item 054 * in the data model becomes selected. 055 * 056 * @param prototypeDisplayValue the <code>Object</code> used to compute 057 * the maximum number of elements to be displayed at once before 058 * displaying a scroll bar 059 * 060 * @see DefaultComboBoxModel 061 * @since 5450 062 */ 063 public JosmComboBox(E prototypeDisplayValue) { 064 init(prototypeDisplayValue); 065 } 066 067 /** 068 * Creates a <code>JosmComboBox</code> that takes its items from an 069 * existing <code>ComboBoxModel</code>. Since the 070 * <code>ComboBoxModel</code> is provided, a combo box created using 071 * this constructor does not create a default combo box model and 072 * may impact how the insert, remove and add methods behave. 073 * 074 * @param aModel the <code>ComboBoxModel</code> that provides the 075 * displayed list of items 076 * @see DefaultComboBoxModel 077 */ 078 public JosmComboBox(ComboBoxModel<E> aModel) { 079 super(aModel); 080 List<E> list = new ArrayList<>(aModel.getSize()); 081 for (int i = 0; i < aModel.getSize(); i++) { 082 list.add(aModel.getElementAt(i)); 083 } 084 init(findPrototypeDisplayValue(list)); 085 } 086 087 /** 088 * Creates a <code>JosmComboBox</code> that contains the elements 089 * in the specified array. By default the first item in the array 090 * (and therefore the data model) becomes selected. 091 * 092 * @param items an array of objects to insert into the combo box 093 * @see DefaultComboBoxModel 094 */ 095 public JosmComboBox(E[] items) { 096 super(items); 097 init(findPrototypeDisplayValue(Arrays.asList(items))); 098 } 099 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}