001 // License: GPL. For details, see LICENSE file. 002 package org.openstreetmap.josm.gui.widgets; 003 004 import java.awt.Component; 005 import java.awt.Dimension; 006 import java.awt.Toolkit; 007 import java.util.ArrayList; 008 import java.util.Arrays; 009 import java.util.Collection; 010 import java.util.Vector; 011 012 import javax.accessibility.Accessible; 013 import javax.swing.ComboBoxModel; 014 import javax.swing.DefaultComboBoxModel; 015 import javax.swing.JComboBox; 016 import javax.swing.JList; 017 import javax.swing.plaf.basic.ComboPopup; 018 019 /** 020 * Class overriding each {@link JComboBox} in JOSM to control consistently the number of displayed items at once.<br/> 021 * This is needed because of the default Java behaviour that may display the top-down list off the screen (see #7917). 022 * 023 * @since 5429 024 */ 025 public class JosmComboBox extends JComboBox { 026 027 /** 028 * The default prototype value used to compute the maximum number of elements to be displayed at once before 029 * displaying a scroll bar 030 */ 031 public static final String DEFAULT_PROTOTYPE_DISPLAY_VALUE = "Prototype display value"; 032 033 /** 034 * Creates a <code>JosmComboBox</code> with a default data model. 035 * The default data model is an empty list of objects. 036 * Use <code>addItem</code> to add items. By default the first item 037 * in the data model becomes selected. 038 * 039 * @see DefaultComboBoxModel 040 */ 041 public JosmComboBox() { 042 this(DEFAULT_PROTOTYPE_DISPLAY_VALUE); 043 } 044 045 /** 046 * Creates a <code>JosmComboBox</code> with a default data model and 047 * the specified prototype display value. 048 * The default data model is an empty list of objects. 049 * Use <code>addItem</code> to add items. By default the first item 050 * in the data model becomes selected. 051 * 052 * @param prototypeDisplayValue the <code>Object</code> used to compute 053 * the maximum number of elements to be displayed at once before 054 * displaying a scroll bar 055 * 056 * @see DefaultComboBoxModel 057 * @since 5450 058 */ 059 public JosmComboBox(Object prototypeDisplayValue) { 060 super(); 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 aModel) { 076 super(aModel); 077 ArrayList<Object> list = new ArrayList<Object>(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(Object[] items) { 093 super(items); 094 init(findPrototypeDisplayValue(Arrays.asList(items))); 095 } 096 097 /** 098 * Creates a <code>JosmComboBox</code> that contains the elements 099 * in the specified Vector. By default the first item in the vector 100 * (and therefore the data model) becomes selected. 101 * 102 * @param items an array of vectors to insert into the combo box 103 * @see DefaultComboBoxModel 104 */ 105 public JosmComboBox(Vector<?> items) { 106 super(items); 107 init(findPrototypeDisplayValue(items)); 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 Object findPrototypeDisplayValue(Collection<?> possibleValues) { 117 Object result = null; 118 int maxHeight = -1; 119 if (possibleValues != null) { 120 // Remind old prototype to restore it later 121 Object oldPrototype = getPrototypeDisplayValue(); 122 // Get internal JList to directly call the renderer 123 JList list = getList(); 124 try { 125 // Index to give to renderer 126 int i = 0; 127 for (Object value : possibleValues) { 128 if (value != null) { 129 // These two lines work with a "classic" renderer, 130 // but not with TaggingPreset custom renderer that return a dummy height if index is equal to -1 131 //setPrototypeDisplayValue(value); 132 //Dimension dim = getPreferredSize(); 133 134 // So we explicitely call the renderer by simulating a correct index for the current value 135 Component c = getRenderer().getListCellRendererComponent(list, value, i, true, true); 136 if (c != null) { 137 // Get the real preferred size for the current value 138 Dimension dim = c.getPreferredSize(); 139 if (dim.height > maxHeight) { 140 // Larger ? This is our new prototype 141 maxHeight = dim.height; 142 result = value; 143 } 144 } 145 } 146 i++; 147 } 148 } finally { 149 // Restore original prototype 150 setPrototypeDisplayValue(oldPrototype); 151 } 152 } 153 return result; 154 } 155 156 protected final JList 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 void init(Object prototype) { 167 if (prototype != null) { 168 setPrototypeDisplayValue(prototype); 169 int screenHeight = Toolkit.getDefaultToolkit().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 list = getList(); 176 if (list != null) { 177 if (list.getPrototypeCellValue() != prototype) { 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 } 188 189 /** 190 * Reinitializes this {@link JosmComboBox} to the specified values. This may needed if a custom renderer is used. 191 * @param values The values displayed in the combo box. 192 * @since 5558 193 */ 194 public final void reinitialize(Collection<?> values) { 195 init(findPrototypeDisplayValue(values)); 196 } 197 }