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