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.Toolkit; 007import java.awt.event.MouseAdapter; 008import java.awt.event.MouseEvent; 009import java.beans.PropertyChangeEvent; 010import java.beans.PropertyChangeListener; 011import java.util.ArrayList; 012import java.util.Arrays; 013import java.util.Collection; 014import java.util.List; 015 016import javax.accessibility.Accessible; 017import javax.swing.ComboBoxModel; 018import javax.swing.DefaultComboBoxModel; 019import javax.swing.JComboBox; 020import javax.swing.JList; 021import javax.swing.plaf.basic.ComboPopup; 022import javax.swing.text.JTextComponent; 023 024/** 025 * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br> 026 * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917). 027 * @param <E> the type of the elements of this combo box 028 * 029 * @since 5429 (creation) 030 * @since 7015 (generics for Java 7) 031 */ 032public class JosmComboBox<E> extends JComboBox<E> { 033 034 /** 035 * Creates a <code>JosmComboBox</code> with a default data model. 036 * The default data model is an empty list of objects. 037 * Use <code>addItem</code> to add items. By default the first item 038 * in the data model becomes selected. 039 * 040 * @see DefaultComboBoxModel 041 */ 042 public JosmComboBox() { 043 init(null); 044 } 045 046 /** 047 * Creates a <code>JosmComboBox</code> with a default data model and 048 * the specified prototype display value. 049 * The default data model is an empty list of objects. 050 * Use <code>addItem</code> to add items. By default the first item 051 * in the data model becomes selected. 052 * 053 * @param prototypeDisplayValue the <code>Object</code> used to compute 054 * the maximum number of elements to be displayed at once before 055 * displaying a scroll bar 056 * 057 * @see DefaultComboBoxModel 058 * @since 5450 059 */ 060 public JosmComboBox(E prototypeDisplayValue) { 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<E> aModel) { 076 super(aModel); 077 List<E> list = new ArrayList<>(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(E[] items) { 093 super(items); 094 init(findPrototypeDisplayValue(Arrays.asList(items))); 095 } 096 097 /** 098 * Finds the prototype display value to use among the given possible candidates. 099 * @param possibleValues The possible candidates that will be iterated. 100 * @return The value that needs the largest display height on screen. 101 * @since 5558 102 */ 103 protected final E findPrototypeDisplayValue(Collection<E> possibleValues) { 104 E result = null; 105 int maxHeight = -1; 106 if (possibleValues != null) { 107 // Remind old prototype to restore it later 108 E oldPrototype = getPrototypeDisplayValue(); 109 // Get internal JList to directly call the renderer 110 @SuppressWarnings("rawtypes") 111 JList list = getList(); 112 try { 113 // Index to give to renderer 114 int i = 0; 115 for (E value : possibleValues) { 116 if (value != null) { 117 // With a "classic" renderer, we could call setPrototypeDisplayValue(value) + getPreferredSize() 118 // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1 119 // So we explicitely call the renderer by simulating a correct index for the current value 120 @SuppressWarnings("unchecked") 121 Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true); 122 if (c != null) { 123 // Get the real preferred size for the current value 124 Dimension dim = c.getPreferredSize(); 125 if (dim.height > maxHeight) { 126 // Larger ? This is our new prototype 127 maxHeight = dim.height; 128 result = value; 129 } 130 } 131 } 132 i++; 133 } 134 } finally { 135 // Restore original prototype 136 setPrototypeDisplayValue(oldPrototype); 137 } 138 } 139 return result; 140 } 141 142 @SuppressWarnings("unchecked") 143 protected final JList<Object> getList() { 144 for (int i = 0; i < getUI().getAccessibleChildrenCount(this); i++) { 145 Accessible child = getUI().getAccessibleChild(this, i); 146 if (child instanceof ComboPopup) { 147 return ((ComboPopup) child).getList(); 148 } 149 } 150 return null; 151 } 152 153 protected final void init(E prototype) { 154 if (prototype != null) { 155 setPrototypeDisplayValue(prototype); 156 int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height; 157 // Compute maximum number of visible items based on the preferred size of the combo box. 158 // This assumes that items have the same height as the combo box, which is not granted by the look and feel 159 int maxsize = (screenHeight/getPreferredSize().height) / 2; 160 // If possible, adjust the maximum number of items with the real height of items 161 // It is not granted this works on every platform (tested OK on Windows) 162 JList<Object> list = getList(); 163 if (list != null) { 164 if (!prototype.equals(list.getPrototypeCellValue())) { 165 list.setPrototypeCellValue(prototype); 166 } 167 int height = list.getFixedCellHeight(); 168 if (height > 0) { 169 maxsize = (screenHeight/height) / 2; 170 } 171 } 172 setMaximumRowCount(Math.max(getMaximumRowCount(), maxsize)); 173 } 174 // Handle text contextual menus for editable comboboxes 175 ContextMenuHandler handler = new ContextMenuHandler(); 176 addPropertyChangeListener("editable", handler); 177 addPropertyChangeListener("editor", handler); 178 } 179 180 protected class ContextMenuHandler extends MouseAdapter implements PropertyChangeListener { 181 182 private JTextComponent component; 183 private PopupMenuLauncher launcher; 184 185 @Override 186 public void propertyChange(PropertyChangeEvent evt) { 187 if ("editable".equals(evt.getPropertyName())) { 188 if (evt.getNewValue().equals(Boolean.TRUE)) { 189 enableMenu(); 190 } else { 191 disableMenu(); 192 } 193 } else if ("editor".equals(evt.getPropertyName())) { 194 disableMenu(); 195 if (isEditable()) { 196 enableMenu(); 197 } 198 } 199 } 200 201 private void enableMenu() { 202 if (launcher == null && editor != null) { 203 Component editorComponent = editor.getEditorComponent(); 204 if (editorComponent instanceof JTextComponent) { 205 component = (JTextComponent) editorComponent; 206 component.addMouseListener(this); 207 launcher = TextContextualPopupMenu.enableMenuFor(component, true); 208 } 209 } 210 } 211 212 private void disableMenu() { 213 if (launcher != null) { 214 TextContextualPopupMenu.disableMenuFor(component, launcher); 215 launcher = null; 216 component.removeMouseListener(this); 217 component = null; 218 } 219 } 220 221 @Override 222 public void mousePressed(MouseEvent e) { 223 processEvent(e); 224 } 225 226 @Override 227 public void mouseReleased(MouseEvent e) { 228 processEvent(e); 229 } 230 231 private void processEvent(MouseEvent e) { 232 if (launcher != null && !e.isPopupTrigger() && launcher.getMenu().isShowing()) { 233 launcher.getMenu().setVisible(false); 234 } 235 } 236 } 237 238 /** 239 * Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used. 240 * @param values The values displayed in the combo box. 241 * @since 5558 242 */ 243 public final void reinitialize(Collection<E> values) { 244 init(findPrototypeDisplayValue(values)); 245 } 246}